From f8b91c9304af548e6d588318b821b2c7b91ac549 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andre=CC=81=20Pimpa=CC=83o?= Date: Sun, 5 Oct 2025 13:25:46 +0100 Subject: [PATCH] chore: optional setBaseUrl --- README.md | 47 ++++++-------- src/Api.php | 26 +++----- src/Builder/ClientBuilder.php | 4 +- src/Builder/Listener/CacheLoggerListener.php | 6 +- src/Exception/ConfigException.php | 5 -- src/Helper/StringHelper.php | 11 ++++ src/Helper/StringHelperTrait.php | 11 ---- tests/Integration/ApiTest.php | 66 +++++++------------- tests/Unit/Builder/ClientBuilderTest.php | 2 +- tests/Unit/Helper/StringHelperTest.php | 17 +++++ tests/Unit/Helper/StringHelperTraitTest.php | 30 --------- 11 files changed, 82 insertions(+), 143 deletions(-) delete mode 100644 src/Exception/ConfigException.php create mode 100644 src/Helper/StringHelper.php delete mode 100644 src/Helper/StringHelperTrait.php create mode 100644 tests/Unit/Helper/StringHelperTest.php delete mode 100644 tests/Unit/Helper/StringHelperTraitTest.php diff --git a/README.md b/README.md index 18df2fa..5bb596c 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ class YourApi extends Api { parent::__construct(); - // minimum required config + // recommended config $this->setBaseUrl('https://api.example.com/v1'); } @@ -77,11 +77,11 @@ Getter and setter for the base URL. Base URL is the common part of the API URL and will be used in all requests. ```php -$this->setBaseUrl(string $baseUrl): self +$this->setBaseUrl(?string $baseUrl): self ``` ```php -$this->getBaseUrl(): string +$this->getBaseUrl(): ?string ``` ### Requests @@ -99,16 +99,12 @@ use Psr\Http\Message\StreamInterface; $this->request( string $method, string $path, - array $query [], + array $query = [], array $headers = [], StreamInterface|string $body = null ): mixed ``` -> [!NOTE] -> A `ConfigException` will be thrown if a base URL is not set (this is, if it is empty). -> Check the [`setBaseUrl`](#base-url) method for more information. - > [!NOTE] > A `ClientException` will be thrown if there is an error while processing the request. @@ -123,7 +119,7 @@ class YourApi extends Api { parent::__construct(); - // minimum required config + // recommended config $this->setBaseUrl('https://api.example.com/v1'); } @@ -165,6 +161,7 @@ class YourApi extends Api { parent::__construct(); + // recommended config $this->setBaseUrl('https://api.example.com/v1'); } @@ -316,18 +313,11 @@ use Http\Message\Authentication; $this->getAuthentication(): ?Authentication; ``` -Available authentication methods: -- [`BasicAuth`](https://docs.php-http.org/en/latest/message/authentication.html#id1) Username and password -- [`Bearer`](https://docs.php-http.org/en/latest/message/authentication.html#bearer) Token -- [`Wsse`](https://docs.php-http.org/en/latest/message/authentication.html#id2) Username and password -- [`QueryParam`](https://docs.php-http.org/en/latest/message/authentication.html#query-params) Array of query parameter values -- [`Header`](https://docs.php-http.org/en/latest/message/authentication.html#header) Header name and value -- [`Chain`](https://docs.php-http.org/en/latest/message/authentication.html#chain) Array of authentication instances -- `RequestConditional` A request matcher and authentication instances +Check all available authentication methods in the [PHP HTTP documentation](https://docs.php-http.org/en/latest/message/authentication.html#authentication-methods). You can also [implement your own](https://docs.php-http.org/en/latest/message/authentication.html#implement-your-own) authentication method. -For example, if you have an API that is authenticated with a query parameter: +For example, if you have an API authenticated with a query parameter: ```php use ProgrammatorDev\Api\Api; @@ -367,7 +357,7 @@ class YourApi extends Api #### `addPreRequestListener` -The `addPreRequestListener` method is used to add a function that is called before a request has been made. +The `addPreRequestListener` method is used to add a function called before a request has been made. This event listener will be applied to every API request. ```php @@ -413,7 +403,7 @@ $this->addPreRequestListener(function(PreRequestEvent $event) { #### `addPostRequestListener` -The `addPostRequestListener` method is used to add a function that is called after a request has been made. +The `addPostRequestListener` method is used to add a function called after a request has been made. This function can be used to inspect the request and response data that was sent to, and received from, the API. This event listener will be applied to every API request. @@ -468,7 +458,7 @@ $this->addPostRequestListener(function(PostRequestEvent $event) { #### `addResponseContentsListener` -The `addResponseContentsListener` method is used to manipulate the response that was received from the API. +The `addResponseContentsListener` method is used to manipulate the response received from the API. This event listener will be applied to every API request. ```php @@ -648,7 +638,7 @@ class YourApi extends Api This library enables attaching plugins to the HTTP client. A plugin modifies the behavior of the client by intercepting the request and response flow. -Since plugin order matters, a plugin is added with a priority level, and are executed in descending order from highest to lowest. +Since plugin order matters, a plugin is added with a priority level and is executed in descending order from highest to lowest. Check all the [available plugins](https://docs.php-http.org/en/latest/plugins/index.html) or [create your own](https://docs.php-http.org/en/latest/plugins/build-your-own.html). @@ -673,7 +663,7 @@ The following list has all the implemented plugins with the respective priority | [`LoggerPlugin`](https://docs.php-http.org/en/latest/plugins/logger.html) | 8 | only if logger is enabled | For example, if you wanted the client to automatically attempt to re-send a request that failed -(due to unreliable connections and servers, for example) you can add the [RetryPlugin](https://docs.php-http.org/en/latest/plugins/retry.html): +(due to unreliable connections and servers, for example), you can add the [RetryPlugin](https://docs.php-http.org/en/latest/plugins/retry.html): ```php use ProgrammatorDev\Api\Api; @@ -686,7 +676,7 @@ class YourApi extends Api // ... // if a request fails, it will retry at least 3 times - // priority is 20 to execute before the cache plugin + // the priority is 20 to execute before the cache plugin // (check the above plugin order list for more information) $this->getClientBuilder()->addPlugin( plugin: new RetryPlugin(['retries' => 3]), @@ -709,12 +699,11 @@ use Psr\Cache\CacheItemPoolInterface; new CacheBuilder( // a PSR-6 cache adapter CacheItemPoolInterface $pool, - // default lifetime (in seconds) of cache items + // default lifetime (in seconds) of cached items ?int $ttl = 60, // An array of HTTP methods for which caching should be applied $methods = ['GET', 'HEAD'], - // An array of cache directives to be compared with the headers of the HTTP response, - // in order to determine cacheability + // An array of cache directives to be compared with the headers of the HTTP response to determine cacheability $responseCacheDirectives = ['max-age'] ); ``` @@ -854,7 +843,7 @@ class YourApi extends Api private function configureOptions(array $options): array { - // set defaults values, if none were provided + // set defaults values if none were provided $this->optionsResolver->setDefault('timezone', 'UTC'); $this->optionsResolver->setDefault('language', 'en'); @@ -872,7 +861,7 @@ class YourApi extends Api private function configureApi(): void { - // set required base url + // set the base url $this->setBaseUrl('https://api.example.com/v1'); // set options as query defaults (will be included in all requests) diff --git a/src/Api.php b/src/Api.php index 9508649..f477949 100644 --- a/src/Api.php +++ b/src/Api.php @@ -15,8 +15,7 @@ use ProgrammatorDev\Api\Event\PostRequestEvent; use ProgrammatorDev\Api\Event\PreRequestEvent; use ProgrammatorDev\Api\Event\ResponseContentsEvent; -use ProgrammatorDev\Api\Exception\ConfigException; -use ProgrammatorDev\Api\Helper\StringHelperTrait; +use ProgrammatorDev\Api\Helper\StringHelper; use Psr\Http\Client\ClientExceptionInterface as ClientException; use Psr\Http\Message\RequestInterface; use Psr\Http\Message\StreamInterface; @@ -25,8 +24,6 @@ class Api { - use StringHelperTrait; - private ?string $baseUrl = null; private array $queryDefaults = []; @@ -53,7 +50,6 @@ public function __construct() } /** - * @throws ConfigException If a base URL has not been set. * @throws ClientException */ public function request( @@ -64,18 +60,14 @@ public function request( string|StreamInterface $body = null ): mixed { - if (!$this->baseUrl) { - throw new ConfigException('A base URL must be set.'); - } - $this->configurePlugins(); if (!empty($this->queryDefaults)) { - $query = \array_merge($this->queryDefaults, $query); + $query = array_merge($this->queryDefaults, $query); } if (!empty($this->headerDefaults)) { - $headers = \array_merge($this->headerDefaults, $headers); + $headers = array_merge($this->headerDefaults, $headers); } $uri = $this->buildUri($path, $query); @@ -161,7 +153,7 @@ public function getBaseUrl(): ?string return $this->baseUrl; } - public function setBaseUrl(string $baseUrl): self + public function setBaseUrl(?string $baseUrl): self { $this->baseUrl = $baseUrl; @@ -278,8 +270,8 @@ public function addResponseContentsListener(callable $listener, int $priority = public function buildPath(string $path, array $parameters): string { foreach ($parameters as $parameter => $value) { - $path = \str_replace( - \sprintf('{%s}', $parameter), + $path = str_replace( + sprintf('{%s}', $parameter), $value, $path ); @@ -290,10 +282,10 @@ public function buildPath(string $path, array $parameters): string private function buildUri(string $path, array $query = []): string { - $uri = $this->reduceDuplicateSlashes($this->baseUrl . $path); + $uri = StringHelper::reduceDuplicateSlashes($this->baseUrl . $path); if (!empty($query)) { - $uri = \sprintf('%s?%s', $uri, \http_build_query($query)); + $uri = sprintf('%s?%s', $uri, http_build_query($query)); } return $uri; @@ -314,7 +306,7 @@ private function createRequest( if ($body !== null && $body !== '') { $request = $request->withBody( - \is_string($body) ? $this->clientBuilder->getStreamFactory()->createStream($body) : $body + is_string($body) ? $this->clientBuilder->getStreamFactory()->createStream($body) : $body ); } diff --git a/src/Builder/ClientBuilder.php b/src/Builder/ClientBuilder.php index 7c20c93..2a636f3 100644 --- a/src/Builder/ClientBuilder.php +++ b/src/Builder/ClientBuilder.php @@ -75,13 +75,13 @@ public function addPlugin(Plugin $plugin, int $priority): self { if (isset($this->plugins[$priority])) { throw new PluginException( - \sprintf('A plugin with priority %d already exists.', $priority) + sprintf('A plugin with priority %d already exists.', $priority) ); } $this->plugins[$priority] = $plugin; // sort plugins by priority (key) in descending order - \krsort($this->plugins); + krsort($this->plugins); return $this; } diff --git a/src/Builder/Listener/CacheLoggerListener.php b/src/Builder/Listener/CacheLoggerListener.php index b865dbe..74a3180 100644 --- a/src/Builder/Listener/CacheLoggerListener.php +++ b/src/Builder/Listener/CacheLoggerListener.php @@ -26,7 +26,7 @@ public function onCacheResponse( if ($fromCache) { /** @var $cacheItem CacheItemInterface */ $logger->info( - \sprintf("Cache hit:\n%s", $formatter->formatRequest($request)), + sprintf("Cache hit:\n%s", $formatter->formatRequest($request)), [ 'expires' => $cacheItem->get()['expiresAt'], 'key' => $cacheItem->getKey() @@ -36,12 +36,12 @@ public function onCacheResponse( // if response is a cache miss (and was cached) else if ($cacheItem instanceof CacheItemInterface) { // handle future deprecation - $formattedResponse = \method_exists($formatter, 'formatResponseForRequest') + $formattedResponse = method_exists($formatter, 'formatResponseForRequest') ? $formatter->formatResponseForRequest($response, $request) : $formatter->formatResponse($response); $logger->info( - \sprintf("Cached response:\n%s", $formattedResponse), + sprintf("Cached response:\n%s", $formattedResponse), [ 'expires' => $cacheItem->get()['expiresAt'], 'key' => $cacheItem->getKey() diff --git a/src/Exception/ConfigException.php b/src/Exception/ConfigException.php deleted file mode 100644 index b74a501..0000000 --- a/src/Exception/ConfigException.php +++ /dev/null @@ -1,5 +0,0 @@ -api->setClientBuilder(new ClientBuilder($this->mockClient)); } - public function testSetters() - { - $pool = $this->createMock(CacheItemPoolInterface::class); - $logger = $this->createMock(LoggerInterface::class); - $authentication = $this->createConfiguredMock(Authentication::class, [ - 'authenticate' => $this->createMock(RequestInterface::class) - ]); - - $this->api->setBaseUrl(self::BASE_URL); - $this->api->setClientBuilder(new ClientBuilder()); - $this->api->setCacheBuilder(new CacheBuilder($pool)); - $this->api->setLoggerBuilder(new LoggerBuilder($logger)); - $this->api->setAuthentication($authentication); - - $this->assertSame(self::BASE_URL, $this->api->getBaseUrl()); - $this->assertInstanceOf(ClientBuilder::class, $this->api->getClientBuilder()); - $this->assertInstanceOf(CacheBuilder::class, $this->api->getCacheBuilder()); - $this->assertInstanceOf(LoggerBuilder::class, $this->api->getLoggerBuilder()); - $this->assertInstanceOf(Authentication::class, $this->api->getAuthentication()); - } - public function testRequest() { $this->mockClient->addResponse(new Response(body: MockResponse::SUCCESS)); - $this->api->setBaseUrl(self::BASE_URL); - $response = $this->api->request( method: 'GET', path: '/path' @@ -72,15 +48,13 @@ public function testRequest() $this->assertSame(MockResponse::SUCCESS, $response); } - public function testMissingBaseUrl() + public function testBaseUrl() { - $this->expectException(ConfigException::class); - $this->expectExceptionMessage('A base URL must be set.'); + $this->assertNull($this->api->getBaseUrl()); - $this->api->request( - method: 'GET', - path: '/path' - ); + $this->api->setBaseUrl(self::BASE_URL); + + $this->assertSame(self::BASE_URL, $this->api->getBaseUrl()); } public function testQueryDefaults() @@ -103,12 +77,13 @@ public function testHeaderDefaults() public function testCache() { - $pool = $this->createMock(CacheItemPoolInterface::class); + $this->assertNull($this->api->getCacheBuilder()); - $this->api->setBaseUrl(self::BASE_URL); - $this->api->setCacheBuilder(new CacheBuilder($pool)); + $cachePool = $this->createMock(CacheItemPoolInterface::class); - $pool->expects($this->once())->method('save'); + $this->api->setCacheBuilder(new CacheBuilder($cachePool)); + + $cachePool->expects($this->once())->method('save'); $this->api->request( method: 'GET', @@ -118,12 +93,13 @@ public function testCache() public function testLogger() { + $this->assertNull($this->api->getLoggerBuilder()); + $logger = $this->createMock(LoggerInterface::class); - $this->api->setBaseUrl(self::BASE_URL); $this->api->setLoggerBuilder(new LoggerBuilder($logger)); - // request + response log + // count equals 2 because of the request and response log $logger->expects($this->exactly(2))->method('info'); $this->api->request( @@ -134,14 +110,16 @@ public function testLogger() public function testCacheWithLogger() { - $pool = $this->createMock(CacheItemPoolInterface::class); + $this->assertNull($this->api->getCacheBuilder()); + $this->assertNull($this->api->getLoggerBuilder()); + + $cachePool = $this->createMock(CacheItemPoolInterface::class); $logger = $this->createMock(LoggerInterface::class); - $this->api->setBaseUrl(self::BASE_URL); - $this->api->setCacheBuilder(new CacheBuilder($pool)); + $this->api->setCacheBuilder(new CacheBuilder($cachePool)); $this->api->setLoggerBuilder(new LoggerBuilder($logger)); - // request + response + cache log + // count equals 3 because of the request, response and cache log $logger->expects($this->exactly(3))->method('info'); // error suppression to hide expected warning of null cache item in CacheLoggerListener @@ -155,11 +133,12 @@ public function testCacheWithLogger() public function testAuthentication() { + $this->assertNull($this->api->getAuthentication()); + $authentication = $this->createConfiguredMock(Authentication::class, [ 'authenticate' => $this->createMock(RequestInterface::class) ]); - $this->api->setBaseUrl(self::BASE_URL); $this->api->setAuthentication($authentication); $authentication->expects($this->once())->method('authenticate'); @@ -172,7 +151,6 @@ public function testAuthentication() public function testPreRequestListener() { - $this->api->setBaseUrl(self::BASE_URL); $this->api->addPreRequestListener(fn() => throw new \Exception('TestMessage')); $this->expectException(\Exception::class); @@ -186,7 +164,6 @@ public function testPreRequestListener() public function testPostRequestListener() { - $this->api->setBaseUrl(self::BASE_URL); $this->api->addPostRequestListener(fn() => throw new \Exception('TestMessage')); $this->expectException(\Exception::class); @@ -202,7 +179,6 @@ public function testResponseContentsListener() { $this->mockClient->addResponse(new Response(body: MockResponse::SUCCESS)); - $this->api->setBaseUrl(self::BASE_URL); $this->api->addResponseContentsListener(function(ResponseContentsEvent $event) { $contents = json_decode($event->getContents(), true); $event->setContents($contents); diff --git a/tests/Unit/Builder/ClientBuilderTest.php b/tests/Unit/Builder/ClientBuilderTest.php index 7738487..9a3ca0c 100644 --- a/tests/Unit/Builder/ClientBuilderTest.php +++ b/tests/Unit/Builder/ClientBuilderTest.php @@ -61,7 +61,7 @@ public function testAddPlugin() $this->assertCount(3, $clientBuilder->getPlugins()); // plugins array keys are used as priority [priority => plugin] - // so check if order of keys (priority) is sorted + // so check if the order of keys (priority) is sorted $this->assertSame( [ 0 => 3, diff --git a/tests/Unit/Helper/StringHelperTest.php b/tests/Unit/Helper/StringHelperTest.php new file mode 100644 index 0000000..706c8de --- /dev/null +++ b/tests/Unit/Helper/StringHelperTest.php @@ -0,0 +1,17 @@ +assertSame( + 'https://example.com/path/test', + StringHelper::reduceDuplicateSlashes('https://example.com////path//test') + ); + } +} \ No newline at end of file diff --git a/tests/Unit/Helper/StringHelperTraitTest.php b/tests/Unit/Helper/StringHelperTraitTest.php deleted file mode 100644 index d52eec1..0000000 --- a/tests/Unit/Helper/StringHelperTraitTest.php +++ /dev/null @@ -1,30 +0,0 @@ -class = new class { - use StringHelperTrait { - reduceDuplicateSlashes as public; - } - }; - } - - public function testReduceDuplicateSlashes() - { - $this->assertSame( - 'https://example.com/path/test', - $this->class->reduceDuplicateSlashes('https://example.com////path//test') - ); - } -} \ No newline at end of file