1: | <?php |
2: | |
3: | /* |
4: | * Library to use PortBilling events with PSR-14 event dispatch |
5: | */ |
6: | |
7: | namespace Porta\Psr14Event; |
8: | |
9: | use Psr\Http\Message\RequestInterface; |
10: | use Psr\EventDispatcher\StoppableEventInterface; |
11: | |
12: | /** |
13: | * PSR-14 Event object/class |
14: | * |
15: | * This obect is mutable and will pass over all handlers, assuming |
16: | * handlers will process event and register зrocessing result with onSuccess() |
17: | * or onProcessed() methods. |
18: | * |
19: | * Event vars are read-only and may be accessed either as array keyed values |
20: | * and as properties. |
21: | * |
22: | * @api |
23: | */ |
24: | class Event implements StoppableEventInterface, \ArrayAccess |
25: | { |
26: | |
27: | protected const EVENT_TYPE_KEY = 'event_type'; |
28: | protected const EVENT_VARS_KEY = 'variables'; |
29: | |
30: | protected string $type; |
31: | protected array $vars; |
32: | protected RequestInterface $request; |
33: | protected bool $stopOnFirstGood = false; |
34: | protected array $result = []; |
35: | |
36: | /** |
37: | * Creates event object from RequestInterface object |
38: | * |
39: | * Parse the request, check the format, detect all event parts. |
40: | * |
41: | * @param RequestInterface $request |
42: | * @return void |
43: | * @throws EventException with code 400 in a case it can't detect JSON body |
44: | * or required fields. |
45: | * |
46: | * @api |
47: | */ |
48: | public function __construct(RequestInterface $request) |
49: | { |
50: | $this->request = $request; |
51: | $this->parseData(); |
52: | } |
53: | |
54: | /** |
55: | * Returns event type |
56: | * |
57: | * Returns ESPF event type like 'Account/BalanceChanged' |
58: | * |
59: | * @return string |
60: | * @api |
61: | */ |
62: | public function getType(): string |
63: | { |
64: | return $this->type; |
65: | } |
66: | |
67: | /** |
68: | * Return all event vars as associative array |
69: | * |
70: | * @return array<string, mixed> |
71: | * @api |
72: | */ |
73: | public function getVars(): array |
74: | { |
75: | return $this->vars; |
76: | } |
77: | |
78: | /** |
79: | * Set event to stop propagate after first success (200) happen |
80: | * |
81: | * If methos is called once, the Event is set to stop propagate after first |
82: | * succes code (200) registered by a handler. |
83: | * |
84: | * May be called either before dispatch or by handler. In the second case, |
85: | * the hahdler will be the last one and Event will stop it's propagation. |
86: | * |
87: | * @return self For chaining |
88: | * @api |
89: | */ |
90: | public function stopOnFirstGood(): self |
91: | { |
92: | $this->stopOnFirstGood = true; |
93: | return $this; |
94: | } |
95: | |
96: | /** |
97: | * Indicate to stop future processing by Dispatcher |
98: | * |
99: | * @return bool |
100: | */ |
101: | public function isPropagationStopped(): bool |
102: | { |
103: | return $this->stopOnFirstGood && in_array(200, $this->result); |
104: | } |
105: | |
106: | /** |
107: | * Event handler must call this to register processing success |
108: | * |
109: | * The same meaning like to call onProcessed(200), as 200 is the success code |
110: | * |
111: | * @return void |
112: | * @api |
113: | */ |
114: | public function onSuccess(): void |
115: | { |
116: | $this->result[] = 200; |
117: | } |
118: | |
119: | /** |
120: | * Event handler must call this to register processing result |
121: | * |
122: | * The code given will be passed to PortaOne as http result code |
123: | * |
124: | * See [Portaone documentation](https://docs.portaone.com/docs/mr105-receiving-provisional-events) |
125: | * about return code meaning and ESPF action on it: |
126: | * |
127: | * Once a response is received, the ESPF acts as follows: |
128: | * - 200 OK – the event has been received. The ESPF removes the event from |
129: | * the provisioning queue. |
130: | * - 4xx Client Error (e.g., 400 Bad Request) – the event must not be |
131: | * provisioned. The ESPF removes the event from the provisioning queue. |
132: | * - Other status code – an issue appeared during provisioning. The ESPF |
133: | * re-sends the event. |
134: | * - If no response is received from the Application during the timeout (300 |
135: | * seconds by default) – the ESPF re-sends the event to the Application. |
136: | * Please make sure your application can accept the same provisioning event |
137: | * multiple times. |
138: | * |
139: | * @param int $code HTTP error code to return. |
140: | * @return void |
141: | * @api |
142: | */ |
143: | public function onProcessed(int $code): void |
144: | { |
145: | $this->result[] = $code; |
146: | } |
147: | |
148: | /** |
149: | * Returns original request object |
150: | * |
151: | * @return RequestInterface |
152: | * @api |
153: | */ |
154: | public function getRequest(): RequestInterface |
155: | { |
156: | return $this->request; |
157: | } |
158: | |
159: | /** |
160: | * Returns the best (lowest code) of registered results |
161: | * |
162: | * If there was no handlers return gived notfound code or default 404 |
163: | * |
164: | * @param int $notFoundCode |
165: | * @return int The best (lowest) code of all registered |
166: | * @api |
167: | */ |
168: | public function getBestResult(int $notFoundCode = 404): int |
169: | { |
170: | return [] == $this->result ? $notFoundCode : min($this->result); |
171: | } |
172: | |
173: | /** |
174: | * Returns the worst (highest code) of registered results |
175: | * |
176: | * If there was no handlers return gived notfound value or default 404 |
177: | * |
178: | * @param int $notFoundCode |
179: | * @return int The worst (highest)code of all registered |
180: | * @api |
181: | */ |
182: | public function getWorstResult(int $notFoundCode = 404): int |
183: | { |
184: | return [] == $this->result ? $notFoundCode : max($this->result); |
185: | } |
186: | |
187: | /** |
188: | * Checks event type against array of patterns |
189: | * |
190: | * Each pattern in the array may be a valid event type with wildcard `*` means |
191: | * any count of **letters, but not `/`**. |
192: | * |
193: | * Examples: |
194: | * - '*/BalanceChanged' will match 'Customer/BalanceChanged' and 'Account/BalanceChanged' |
195: | * - 'Account/*locked' will match 'Account/Blocked' and 'Account/Unblocked' |
196: | * |
197: | * @param string[] $patterns Array of patterns to match |
198: | * @return bool return true if match any of patterns |
199: | * @api |
200: | */ |
201: | public function isMatchPatterns(array $patterns): bool |
202: | { |
203: | foreach ($patterns as $pattern) { |
204: | $regexp = str_replace('*', '[a-zA-Z]+', $pattern); |
205: | if (1 === preg_match('|^' . $regexp . '$|', $this->type)) { |
206: | return true; |
207: | } |
208: | } |
209: | return false; |
210: | } |
211: | |
212: | // Protected methods |
213: | protected function parseData(): void |
214: | { |
215: | if (null === ($requestData = json_decode((string) $this->request->getBody(), true))) { |
216: | throw new EventException("Can't decode request body as JSON, body: '" . (string) $this->request->getBody() . "'", 400); |
217: | } |
218: | if (!key_exists(self::EVENT_TYPE_KEY, $requestData) || |
219: | !key_exists(self::EVENT_VARS_KEY, $requestData)) { |
220: | throw new EventException("Mailformed JSON data, 'event_type' or 'variables' does not exist", 400); |
221: | } |
222: | $this->type = $requestData[self::EVENT_TYPE_KEY]; |
223: | $this->vars = $requestData[self::EVENT_VARS_KEY]; |
224: | } |
225: | |
226: | // Access vars as array elements |
227: | public function offsetExists($offset): bool |
228: | { |
229: | return isset($this->vars[$offset]); |
230: | } |
231: | |
232: | public function offsetGet($offset) |
233: | { |
234: | return $this->vars[$offset]; |
235: | } |
236: | |
237: | public function offsetSet($offset, $value): void |
238: | { |
239: | // Do nothing, read only |
240: | } |
241: | |
242: | public function offsetUnset($offset): void |
243: | { |
244: | // Do nothing, read only |
245: | } |
246: | |
247: | // Access vars as properties |
248: | public function __get($name) |
249: | { |
250: | return $this->offsetGet($name); |
251: | } |
252: | |
253: | public function __isset($name) |
254: | { |
255: | return $this->offsetExists($name); |
256: | } |
257: | |
258: | public function __set($name, $value) |
259: | { |
260: | // Do nothing, read only |
261: | } |
262: | |
263: | public function __unset($name) |
264: | { |
265: | // Do nothing, read only |
266: | } |
267: | } |
268: |