• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

MichaelJ2324 / PHP-REST-Client / 13080782723

31 Jan 2025 09:06PM UTC coverage: 94.359% (+2.5%) from 91.889%
13080782723

push

github

MichaelJ2324
Test Coverage + Auth Request Logging/Handling

67 of 73 new or added lines in 11 files covered. (91.78%)

1 existing line in 1 file now uncovered.

1037 of 1099 relevant lines covered (94.36%)

1.48 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

97.81
/src/Endpoint/Abstracts/AbstractEndpoint.php
1
<?php
2

3
namespace MRussell\REST\Endpoint\Abstracts;
4

5
use GuzzleHttp\Promise\PromiseInterface;
6
use GuzzleHttp\Exception\GuzzleException;
7
use GuzzleHttp\Client;
8
use GuzzleHttp\Exception\InvalidArgumentException;
9
use GuzzleHttp\Exception\RequestException;
10
use GuzzleHttp\Psr7\Stream;
11
use GuzzleHttp\Psr7\Utils;
12
use MRussell\REST\Client\ClientAwareTrait;
13
use MRussell\REST\Endpoint\Data\DataInterface;
14
use MRussell\REST\Endpoint\Event\EventTriggerInterface;
15
use MRussell\REST\Endpoint\Event\Stack;
16
use MRussell\REST\Endpoint\Interfaces\EndpointInterface;
17
use MRussell\REST\Endpoint\Traits\EventsTrait;
18
use MRussell\REST\Endpoint\Traits\PropertiesTrait;
19
use MRussell\REST\Exception\Endpoint\InvalidUrl;
20
use GuzzleHttp\Psr7\Request;
21
use GuzzleHttp\Psr7\Response;
22
use MRussell\REST\Endpoint\Traits\JsonHandlerTrait;
23

24
/**
25
 * Class AbstractEndpoint
26
 * @package MRussell\REST\Endpoint\Abstracts
27
 */
28
abstract class AbstractEndpoint implements EndpointInterface, EventTriggerInterface
29
{
30
    use EventsTrait;
31
    use ClientAwareTrait;
32
    use JsonHandlerTrait;
33
    use PropertiesTrait {
34
        setProperties as rawSetProperties;
35
    }
36

37
    public const PROPERTY_URL = 'url';
38

39
    public const PROPERTY_HTTP_METHOD = 'httpMethod';
40

41
    public const PROPERTY_AUTH = 'auth';
42

43
    public const EVENT_CONFIGURE_METHOD = 'configure_method';
44

45
    public const EVENT_CONFIGURE_URL = 'configure_url';
46

47
    public const EVENT_CONFIGURE_PAYLOAD = 'configure_payload';
48

49
    public const EVENT_AFTER_CONFIGURED_REQUEST = 'after_configure_req';
50

51
    public const EVENT_AFTER_RESPONSE = 'after_response';
52

53
    public const AUTH_NOAUTH = 0;
54

55
    public const AUTH_EITHER = 1;
56

57
    public const AUTH_REQUIRED = 2;
58

59
    protected static array $_DEFAULT_PROPERTIES = [
60
        self::PROPERTY_URL => '',
61
        self::PROPERTY_HTTP_METHOD => '',
62
        self::PROPERTY_AUTH => self::AUTH_EITHER,
63
    ];
64

65
    private PromiseInterface $promise;
66

67
    /**
68
     * The Variable Identifier to parse Endpoint URL
69
     */
70
    protected static string $_URL_VAR_CHARACTER = '$';
71

72
    /**
73
     * The initial URL passed into the Endpoint
74
     */
75
    protected string $_baseUrl = '';
76

77
    /**
78
     * The passed in Options for the Endpoint, mainly used for parsing URL Variables
79
     */
80
    protected array $_urlArgs = [];
81

82
    /**
83
     * The data being passed to the API Endpoint.
84
     * Defaults to Array, but can be mixed based on how you want to use Endpoint.
85
     */
86
    protected string|array|\ArrayAccess|null $_data;
87

88
    /**
89
     * The Request Object used by the Endpoint to submit the data
90
     */
91
    protected Request $_request;
92

93
    /**
94
     * The Response Object used by the Endpoint
95
     */
96
    protected Response $_response;
97

98
    protected bool $_catchNon200Responses = false;
99

100
    public function __construct(array $properties = [], array $urlArgs = [])
2✔
101
    {
102
        $this->_eventStack = new Stack();
2✔
103
        $this->_eventStack->setEndpoint($this);
2✔
104
        $this->setProperties(static::$_DEFAULT_PROPERTIES);
2✔
105
        if (!empty($urlArgs)) {
2✔
106
            $this->setUrlArgs($urlArgs);
1✔
107
        }
108

109
        foreach ($properties as $key => $value) {
2✔
110
            $this->setProperty($key, $value);
1✔
111
        }
112
    }
113

114
    public function catchNon200Responses(bool $catch = true): static
×
115
    {
NEW
116
        $this->_catchNon200Responses = $catch;
×
117
        return $this;
×
118
    }
119

120
    /**
121
     * @inheritdoc
122
     */
123
    public function setUrlArgs(array $args): static
2✔
124
    {
125
        $this->_urlArgs = $args;
2✔
126
        return $this;
2✔
127
    }
128

129
    /**
130
     * @inheritdoc
131
     */
132
    public function getUrlArgs(): array
3✔
133
    {
134
        return $this->_urlArgs;
3✔
135
    }
136

137
    /**
138
     * @inheritdoc
139
     */
140
    public function setBaseUrl($url): static
3✔
141
    {
142
        $this->_baseUrl = $url;
3✔
143
        return $this;
3✔
144
    }
145

146
    /**
147
     * @inheritdoc
148
     */
149
    public function getBaseUrl(): string
2✔
150
    {
151
        if (empty($this->_baseUrl) && isset($this->_client)) {
2✔
152
            return $this->getClient()->getAPIUrl();
1✔
153
        }
154

155
        return $this->_baseUrl;
2✔
156
    }
157

158
    /**
159
     * @inheritdoc
160
     */
161
    public function getEndPointUrl(bool $full = false): string
3✔
162
    {
163
        $url = $this->getProperty(self::PROPERTY_URL) ?? "";
3✔
164

165
        if ($full) {
3✔
166
            $url = rtrim($this->getBaseUrl(), '/') . ('/' . $url);
1✔
167
        }
168

169
        return $url;
3✔
170
    }
171

172
    /**
173
     * @inheritdoc
174
     */
175
    public function setData(string|array|\ArrayAccess|null $data): static
1✔
176
    {
177
        $this->_data = $data;
1✔
178
        return $this;
1✔
179
    }
180

181
    /**
182
     * @inheritdoc
183
     */
184
    public function getData(): string|array|\ArrayAccess|null
2✔
185
    {
186
        return $this->_data ?? null;
2✔
187
    }
188

189
    /**
190
     * @return $this|EndpointInterface
191
     */
192
    protected function setResponse(Response $_response): static
2✔
193
    {
194
        $this->_response = $_response;
2✔
195
        $this->_respContent = null;
2✔
196
        $this->triggerEvent(self::EVENT_AFTER_RESPONSE, $_response);
2✔
197
        return $this;
2✔
198
    }
199

200
    /**
201
     * @inheritdoc
202
     */
203
    public function getResponse(): Response
1✔
204
    {
205
        return $this->_response;
1✔
206
    }
207

208
    public function getResponseBody(bool $associative = true): mixed
2✔
209
    {
210
        $response = $this->getResponse();
2✔
211
        return $response ? $this->getResponseContent($response, $associative) : null;
2✔
212
    }
213

214
    public function getHttpClient(): Client
2✔
215
    {
216
        if (isset($this->_client)) {
2✔
217
            return $this->getClient()->getHttpClient();
1✔
218
        }
219

220
        return new Client();
2✔
221
    }
222

223
    /**
224
     *
225
     * @inheritdoc
226
     * @param array $options Guzzle Send Options
227
     * @return $this
228
     * @throws GuzzleException
229
     */
230
    public function execute(array $options = []): static
3✔
231
    {
232
        try {
233
            $response = $this->getHttpClient()->send($this->buildRequest(), $options);
3✔
234
            $this->setResponse($response);
1✔
235
        } catch (RequestException $requestException) {
2✔
236
            $response = $requestException->getResponse();
1✔
237
            if ($response instanceof Response) {
1✔
238
                $this->setResponse($requestException->getResponse());
×
239
            }
240

241
            if (!$this->_catchNon200Responses) {
1✔
242
                throw $requestException;
1✔
243
            }
244
        }
245

246
        return $this;
1✔
247
    }
248

249
    /**
250
     * @inheritdoc
251
     * @param null $data - short form data for Endpoint, which is configure by configureData method
252
     * @return $this
253
     */
254
    public function asyncExecute(array $options = []): EndpointInterface
1✔
255
    {
256
        $request = $this->buildRequest();
1✔
257
        $this->promise = $this->getHttpClient()->sendAsync($request, $options);
1✔
258
        $this->promise->then(
1✔
259
            function (Response $res) use ($options): void {
1✔
260
                $this->setResponse($res);
1✔
261
                if (isset($options['success']) && is_callable($options['success'])) {
1✔
262
                    $options['success']($res);
1✔
263
                }
264
            },
1✔
265
            function (RequestException $e) use ($options): void {
1✔
266
                $this->setResponse($e->getResponse());
1✔
267
                if (isset($options['error']) && is_callable($options['error'])) {
1✔
268
                    $options['error']($e);
1✔
269
                }
270
            },
1✔
271
        );
1✔
272
        return $this;
1✔
273
    }
274

275
    public function getPromise(): ?PromiseInterface
1✔
276
    {
277
        return $this->promise ?? null;
1✔
278
    }
279

280
    /**
281
     * @inheritdoc
282
     */
283
    public function useAuth(): int
1✔
284
    {
285
        $auth = self::AUTH_EITHER;
1✔
286
        if (isset($this->_properties[self::PROPERTY_AUTH])) {
1✔
287
            $auth = intval($this->_properties[self::PROPERTY_AUTH]);
1✔
288
        }
289

290
        return $auth;
1✔
291
    }
292

293
    public function getMethod(): string
2✔
294
    {
295
        $this->triggerEvent(self::EVENT_CONFIGURE_METHOD);
2✔
296
        if (
297
            isset($this->_properties[self::PROPERTY_HTTP_METHOD]) &&
2✔
298
            $this->_properties[self::PROPERTY_HTTP_METHOD] !== ''
2✔
299
        ) {
300
            return $this->_properties[self::PROPERTY_HTTP_METHOD];
1✔
301
        }
302

303
        return "GET";
2✔
304
    }
305

306
    /**
307
     * Verifies URL and Data are setup, then sets them on the Request Object
308
     */
309
    public function buildRequest(): Request
2✔
310
    {
311
        $method = $this->getMethod();
2✔
312
        $url = $this->configureURL($this->getUrlArgs());
2✔
313
        if ($this->verifyUrl($url)) {
2✔
314
            $url = rtrim($this->getBaseUrl(), "/") . "/" . $url;
1✔
315
        }
316

317
        $data = $this->configurePayload();
1✔
318
        $request = new Request($method, $url);
1✔
319
        $request = $this->configureJsonRequest($request);
1✔
320
        $this->_request = $this->configureRequest($request, $data);
1✔
321
        return $this->_request;
1✔
322
    }
323

324
    /**
325
     * Configures Data on the Endpoint to be set on the Request.
326
     * @return string|array|DataInterface|null|Stream
327
     */
328
    protected function configurePayload(): mixed
2✔
329
    {
330
        $data = $this->getData() ?? null;
2✔
331
        $this->triggerEvent(self::EVENT_CONFIGURE_PAYLOAD, $data);
2✔
332
        return $data;
2✔
333
    }
334

335
    /**
336
     * @param $data
337
     */
338
    protected function configureRequest(Request $request, $data): Request
3✔
339
    {
340
        if ($data !== null) {
3✔
341
            switch ($request->getMethod()) {
2✔
342
                case "GET":
2✔
343
                    if (!empty($data)) {
2✔
344
                        $value = $data;
2✔
345
                        if (\is_array($value)) {
2✔
346
                            $value = \http_build_query($value, '', '&', \PHP_QUERY_RFC3986);
1✔
347
                        }
348

349
                        if (!\is_string($value)) {
2✔
350
                            throw new InvalidArgumentException('query must be a string or array');
1✔
351
                        }
352

353
                        $uri = $request->getUri()->withQuery($value);
1✔
354
                        $request = $request->withUri($uri);
1✔
355
                    }
356

357
                    break;
1✔
358
                default:
359
                    if (is_array($data)) {
1✔
360
                        $data = json_encode($data);
1✔
361
                    }
362

363
                    $request = $request->withBody(Utils::streamFor($data));
1✔
364
            }
365
        }
366

367
        $args = ['request' => $request, 'data' => $data];
2✔
368
        $this->triggerEvent(self::EVENT_AFTER_CONFIGURED_REQUEST, $args);
2✔
369
        return $args['request'];
2✔
370
    }
371

372
    /**
373
     * Configures the URL, by updating any variable placeholders in the URL property on the Endpoint
374
     * - Replaces $var $options['var']
375
     * - If $options['var'] doesn't exist, replaces with next numeric option in array
376
     */
377
    protected function configureURL(array $urlArgs): string
2✔
378
    {
379
        $url = $this->getEndPointUrl();
2✔
380
        $this->triggerEvent(self::EVENT_CONFIGURE_URL, $urlArgs);
2✔
381
        if ($this->hasUrlArgs()) {
2✔
382
            $urlArr = explode("/", $url);
2✔
383
            $optional = false;
2✔
384
            $optionNum = 0;
2✔
385
            $keys = array_keys($urlArgs);
2✔
386
            sort($keys);
2✔
387
            foreach ($keys as $key) {
2✔
388
                if (is_numeric($key)) {
1✔
389
                    $optionNum = $key;
1✔
390
                    break;
1✔
391
                }
392
            }
393

394
            foreach ($urlArr as $key => $urlPart) {
2✔
395
                $replace = null;
2✔
396
                if (str_contains($urlPart, static::$_URL_VAR_CHARACTER)) {
2✔
397
                    if (str_contains($urlPart, ':')) {
2✔
398
                        $optional = true;
1✔
399
                        $replace = '';
1✔
400
                    }
401

402
                    $opt = str_replace([static::$_URL_VAR_CHARACTER, ':'], '', $urlPart);
2✔
403
                    if (isset($urlArgs[$opt])) {
2✔
404
                        $replace = $urlArgs[$opt];
1✔
405
                    }
406

407
                    if (isset($urlArgs[$optionNum]) && ($replace == '' || $replace == null)) {
2✔
408
                        $replace = $urlArgs[$optionNum];
1✔
409
                        $optionNum += 1;
1✔
410
                    }
411

412
                    if ($optional && $replace == '') {
2✔
413
                        $urlArr = array_slice($urlArr, 0, $key);
1✔
414
                        break;
1✔
415
                    }
416

417
                    if ($replace !== null) {
2✔
418
                        $urlArr[$key] = $replace;
1✔
419
                    }
420
                }
421
            }
422

423
            $url = implode("/", $urlArr);
2✔
424
            $url = rtrim($url, "/");
2✔
425
        }
426

427
        return $url;
2✔
428
    }
429

430
    /**
431
     * Verify if URL is configured properly
432
     * @throws InvalidUrl
433
     */
434
    private function verifyUrl(string $url): bool
3✔
435
    {
436
        if (str_contains($url, static::$_URL_VAR_CHARACTER)) {
3✔
437
            throw new InvalidUrl([static::class, $url]);
1✔
438
        }
439

440
        return true;
2✔
441
    }
442

443
    /**
444
     * Checks if Endpoint URL requires Arguments
445
     */
446
    protected function hasUrlArgs(): bool
1✔
447
    {
448
        $url = $this->getEndPointUrl();
1✔
449
        $variables = $this->extractUrlVariables($url);
1✔
450
        return !empty($variables);
1✔
451
    }
452

453
    /**
454
     * Helper method for extracting variables via Regex from a passed in URL
455
     * @param $url
456
     */
457
    protected function extractUrlVariables($url): array
1✔
458
    {
459
        $variables = [];
1✔
460
        $pattern = "/(" . preg_quote(static::$_URL_VAR_CHARACTER) . ".*?[^\\/]*)/";
1✔
461
        if (preg_match_all($pattern, $url, $matches)) {
1✔
462
            foreach ($matches as $match) {
1✔
463
                $variables[] = $match[0];
1✔
464
            }
465
        }
466

467
        return $variables;
1✔
468
    }
469

470
    /**
471
     * @return $this
472
     */
473
    public function reset(): static
1✔
474
    {
475
        unset($this->_request);
1✔
476
        unset($this->_response);
1✔
477
        $this->_urlArgs = [];
1✔
478
        $this->setData(null);
1✔
479
        $this->setProperties([]);
1✔
480
        return $this;
1✔
481
    }
482

483
    /**
484
     * @inheritdoc
485
     */
486
    public function setProperties(array $properties): static
2✔
487
    {
488
        if (!isset($properties[self::PROPERTY_HTTP_METHOD])) {
2✔
489
            $properties[self::PROPERTY_HTTP_METHOD] = '';
1✔
490
        }
491

492
        if (!isset($properties[self::PROPERTY_URL])) {
2✔
493
            $properties[self::PROPERTY_URL] = '';
1✔
494
        }
495

496
        if (!isset($properties[self::PROPERTY_AUTH])) {
2✔
497
            $properties[self::PROPERTY_AUTH] = self::AUTH_EITHER;
1✔
498
        } else {
499
            $properties[self::PROPERTY_AUTH] = intval($properties[self::PROPERTY_AUTH]);
2✔
500
        }
501

502
        return $this->rawSetProperties($properties);
2✔
503
    }
504
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc