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: * - '&#x2a;/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: