From dd7280496068e4f17b7214dc26d0cd9155476957 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Mon, 6 Dec 2021 10:24:23 +0100 Subject: [PATCH 1/2] Created ClientBuilders to simplify creating client instances at runtime from dynamic configs --- README.md | 68 +++++++++++++++++--- src/Builder/ShlinkClientBuilder.php | 57 ++++++++++++++++ src/Builder/ShlinkClientBuilderInterface.php | 22 +++++++ src/Builder/SingletonShlinkClientBuilder.php | 59 +++++++++++++++++ 4 files changed, 196 insertions(+), 10 deletions(-) create mode 100644 src/Builder/ShlinkClientBuilder.php create mode 100644 src/Builder/ShlinkClientBuilderInterface.php create mode 100644 src/Builder/SingletonShlinkClientBuilder.php diff --git a/README.md b/README.md index a616233..19a65b5 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ A PHP SDK to consume Shlink's REST API in a very convenient and robust way. * Very expressive API. -* Decoupled from implementations. Depending only on PSR-11, PSR-17 and PSR-18 interfaces. +* Decoupled from implementations. Depending only on PSR-17 and PSR-18 interfaces. * Dependency injection. Every service can be composed out of a set of pieces. * Statically typed and immutable DTOs, with meaningful named constructors. * Generator-based iterable collections, to abstract pagination and reduce resource consumption. @@ -42,9 +42,9 @@ use Shlinkio\Shlink\SDK\ShortUrls\ShortUrlsClient; use function count; $httpClient = new HttpClient( - new Client(), // Any object implementing Psr\Http\Client\ClientInterface from PSR-18 - new HttpFactory(), // Any object implementing Psr\Http\Message\RequestFactoryInterface from PSR-17 - new HttpFactory(), // Any object implementing Psr\Http\Message\StreamFactoryInterface from PSR-17 + new Client(), // Any object implementing PSR-18's Psr\Http\Client\ClientInterface + new HttpFactory(), // Any object implementing PSR-17's Psr\Http\Message\RequestFactoryInterface + new HttpFactory(), // Any object implementing PSR-17's Psr\Http\Message\StreamFactoryInterface ShlinkConfig::fromEnv() ) $client = new ShortUrlsClient($httpClient) @@ -63,7 +63,7 @@ foreach ($shortUrls as $shortUrl) { } ``` -### Shlink configuration +## Shlink configuration This SDK provides a couple of ways to provide Shlink's config (mainly base URL and API key). @@ -115,7 +115,7 @@ use Shlinkio\Shlink\SDK\Config\ShlinkConfig; $config = ShlinkConfig::fromBaseUrlAndApiKey('https://my-domain.com', 'cec2f62c-b119-452a-b351-a416a2f5f45a'); ``` -### Shlink "Clients" +## Shlink "Clients" As mentioned above, the SDK provides different services to consume every context of the API, `ShortUrlsClient`, `VisitsClient`, `TagsClient` and `DomainsClient`. @@ -144,13 +144,61 @@ $domainsClient = new DomainsClient($httpClient); $client = new ShlinkClient($shortUrlsClient, $visitsClient, $tagsClient, $domainsClient); ``` -## PSR-11 container integration +## Client Builder -In the examples above you have seen the dependency injection graph is slightly complex, with a couple of dependency levels until you get a usable API client ready. +Sometimes you may not know the Shlink config params before runtime, for example, if they are going to be dynamically provided. -In order to simplify creating objects, this SDK provides some basic factories for PSR-11 containers. +When this happens, it's not possible to predefine the clients creation as in the examples above. -[TODO] +For those cases, the `ShlinkClientBuilder` is provided. It depends on PSR-17 and 18 adapters, and exposes methods to build client instances from a `ShlinkConfig` instance. + +```php +use GuzzleHttp\Client; +use GuzzleHttp\Psr7\HttpFactory; +use Shlinkio\Shlink\SDK\Builder\ShlinkClientBuilder; +use Shlinkio\Shlink\SDK\Config\ShlinkConfig; +use Shlinkio\Shlink\SDK\ShortUrls\Model\ShortUrlIdentifier; + +$builder = new ShlinkClientBuilder( + new Client(), // Any object implementing PSR-18's Psr\Http\Client\ClientInterface + new HttpFactory(), // Any object implementing PSR-17's Psr\Http\Message\RequestFactoryInterface + new HttpFactory(), // Any object implementing PSR-17's Psr\Http\Message\StreamFactoryInterface +); +$config = ShlinkConfig::fromBaseUrlAndApiKey( + // Get base URL and API Key from somewhere... +); + +$visitsClient = $builder->buildVisitsClient($config); +$visitsClient->listTagVisits('foo'); + +$shortUrlsClient = $builder->buildShortUrlsClient($config); +$shortUrlsClient->deleteShortUrl(ShortUrlIdentifier::fromShortCode('bar')); +``` + +### Singleton instances + +In the example above, the `ShlinkClientBuilder` will return a new client instance every time any of the `build` methods is invoked. + +If you want to make sure the same instance is always returned for a set of base URL + API key, you can wrap it into a `SingletonShlinkClientBuilder` instance, which also implements `ShlinkClientBuilderInterface` and thus, it can be safely replace the regular `ShlinkClientBuilder`. + +```php +use GuzzleHttp\Client; +use GuzzleHttp\Psr7\HttpFactory; +use Shlinkio\Shlink\SDK\Builder\ShlinkClientBuilder; +use Shlinkio\Shlink\SDK\Builder\SingletonShlinkClientBuilder; +use Shlinkio\Shlink\SDK\Config\ShlinkConfig; +use Shlinkio\Shlink\SDK\ShortUrls\Model\ShortUrlIdentifier; + +$builder = new SingletonShlinkClientBuilder( + new ShlinkClientBuilder(new Client(), new HttpFactory(), new HttpFactory()), +); +$config = ShlinkConfig::fromBaseUrlAndApiKey(...); + +$client1 = $builder->buildTagsClient($config); +$client2 = $builder->buildTagsClient($config); + +var_dump($client1 === $client2); // This is true +``` ## Error handling diff --git a/src/Builder/ShlinkClientBuilder.php b/src/Builder/ShlinkClientBuilder.php new file mode 100644 index 0000000..ab85110 --- /dev/null +++ b/src/Builder/ShlinkClientBuilder.php @@ -0,0 +1,57 @@ +createHttpClient($config)); + } + + public function buildVisitsClient(ShlinkConfigInterface $config): VisitsClientInterface + { + return new VisitsClient($this->createHttpClient($config)); + } + + public function buildTagsClient(ShlinkConfigInterface $config): TagsClientInterface + { + return new TagsClient($this->createHttpClient($config)); + } + + public function buildDomainsClient(ShlinkConfigInterface $config): DomainsClientInterface + { + return new DomainsClient($this->createHttpClient($config)); + } + + private function createHttpClient(ShlinkConfigInterface $config): HttpClientInterface + { + return new HttpClient($this->client, $this->requestFactory, $this->streamFactory, $config, $this->debugger); + } +} diff --git a/src/Builder/ShlinkClientBuilderInterface.php b/src/Builder/ShlinkClientBuilderInterface.php new file mode 100644 index 0000000..ebb8d31 --- /dev/null +++ b/src/Builder/ShlinkClientBuilderInterface.php @@ -0,0 +1,22 @@ +configToKey($config); + return $this->instances[ShortUrlsClientInterface::class][$key] ?? ( + $this->instances[ShortUrlsClientInterface::class][$key] = $this->wrapped->buildShortUrlsClient($config) + ); + } + + public function buildVisitsClient(ShlinkConfigInterface $config): VisitsClientInterface + { + $key = $this->configToKey($config); + return $this->instances[VisitsClientInterface::class][$key] ?? ( + $this->instances[VisitsClientInterface::class][$key] = $this->wrapped->buildVisitsClient($config) + ); + } + + public function buildTagsClient(ShlinkConfigInterface $config): TagsClientInterface + { + $key = $this->configToKey($config); + return $this->instances[TagsClientInterface::class][$key] ?? ( + $this->instances[TagsClientInterface::class][$key] = $this->wrapped->buildTagsClient($config) + ); + } + + public function buildDomainsClient(ShlinkConfigInterface $config): DomainsClientInterface + { + $key = $this->configToKey($config); + return $this->instances[DomainsClientInterface::class][$key] ?? ( + $this->instances[DomainsClientInterface::class][$key] = $this->wrapped->buildDomainsClient($config) + ); + } + + private function configToKey(ShlinkConfigInterface $config): string + { + return sprintf('%s_%s', $config->baseUrl(), $config->apiKey()); + } +} From 1f191e847242e711305705c0293e8b25cf30846b Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Mon, 6 Dec 2021 10:44:00 +0100 Subject: [PATCH 2/2] Added tests for Client builders --- CHANGELOG.md | 16 ++++++ .../ClientBuilderMethodsProviderTrait.php | 21 +++++++ test/Builder/ShlinkClientBuilderTest.php | 54 ++++++++++++++++++ .../SingletonShlinkClientBuilderTest.php | 57 +++++++++++++++++++ 4 files changed, 148 insertions(+) create mode 100644 test/Builder/ClientBuilderMethodsProviderTrait.php create mode 100644 test/Builder/ShlinkClientBuilderTest.php create mode 100644 test/Builder/SingletonShlinkClientBuilderTest.php diff --git a/CHANGELOG.md b/CHANGELOG.md index db67b37..9c4a112 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,22 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com), and this project adheres to [Semantic Versioning](https://semver.org). +## [Unreleased] +### Added +* [#12](https://github.com/shlinkio/shlink-php-client/issues/12) Created `ShlinkClientBuilder` and `SingletonShlinkClientBuilder`, which can be used to create client instances at runtime. + +### Changed +* *Nothing* + +### Deprecated +* *Nothing* + +### Removed +* *Nothing* + +### Fixed +* *Nothing* + ## [0.1.0] - 2021-12-04 ### Added * First release diff --git a/test/Builder/ClientBuilderMethodsProviderTrait.php b/test/Builder/ClientBuilderMethodsProviderTrait.php new file mode 100644 index 0000000..2dae82e --- /dev/null +++ b/test/Builder/ClientBuilderMethodsProviderTrait.php @@ -0,0 +1,21 @@ +getMethods() as $method) { + $name = $method->getName(); + yield $name => [$name]; + } + } +} diff --git a/test/Builder/ShlinkClientBuilderTest.php b/test/Builder/ShlinkClientBuilderTest.php new file mode 100644 index 0000000..da25ba8 --- /dev/null +++ b/test/Builder/ShlinkClientBuilderTest.php @@ -0,0 +1,54 @@ +client = $this->prophesize(ClientInterface::class); + $this->requestFactory = $this->prophesize(RequestFactoryInterface::class); + $this->streamFactory = $this->prophesize(StreamFactoryInterface::class); + + $this->builder = new ShlinkClientBuilder( + $this->client->reveal(), + $this->requestFactory->reveal(), + $this->streamFactory->reveal(), + ); + $this->config = ShlinkConfig::fromBaseUrlAndApiKey('foo', 'bar'); + } + + /** + * @test + * @dataProvider provideMethods + */ + public function buildClientReturnsAlwaysNewInstances(string $method): void + { + $instance1 = $this->builder->{$method}($this->config); + $instance2 = $this->builder->{$method}($this->config); + + self::assertEquals($instance1, $instance2); + self::assertNotSame($instance1, $instance2); + } +} diff --git a/test/Builder/SingletonShlinkClientBuilderTest.php b/test/Builder/SingletonShlinkClientBuilderTest.php new file mode 100644 index 0000000..ec8325b --- /dev/null +++ b/test/Builder/SingletonShlinkClientBuilderTest.php @@ -0,0 +1,57 @@ +wrapped = $this->prophesize(ShlinkClientBuilderInterface::class); + $this->builder = new SingletonShlinkClientBuilder($this->wrapped->reveal()); + $this->config = ShlinkConfig::fromBaseUrlAndApiKey('foo', 'bar'); + } + + /** + * @test + * @dataProvider provideMethods + */ + public function buildClientReturnsAlwaysNewInstances(string $method): void + { + $call = $this->wrapped->__call($method, [Argument::type(ShlinkConfigInterface::class)])->willReturn( + $this->prophesize(ShlinkClient::class)->reveal(), + ); + + $configOne = ShlinkConfig::fromBaseUrlAndApiKey('foo', 'bar'); + $instance1 = $this->builder->{$method}($configOne); + $instance2 = $this->builder->{$method}($configOne); + self::assertSame($instance1, $instance2); + + $configTwo = ShlinkConfig::fromBaseUrlAndApiKey('bar', 'foo'); + $instance1 = $this->builder->{$method}($configTwo); + $instance2 = $this->builder->{$method}($configTwo); + $instance3 = $this->builder->{$method}($configTwo); + self::assertSame($instance1, $instance2); + self::assertSame($instance1, $instance3); + self::assertSame($instance2, $instance3); + + $call->shouldHaveBeenCalledTimes(2); + } +}