Skip to content

Commit

Permalink
Merge pull request shlinkio#13 from acelaya-forks/feature/client-builder
Browse files Browse the repository at this point in the history
Feature/client builder
  • Loading branch information
acelaya authored Dec 6, 2021
2 parents bfb694a + 1f191e8 commit 03ac1f8
Show file tree
Hide file tree
Showing 8 changed files with 344 additions and 10 deletions.
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
68 changes: 58 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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)
Expand All @@ -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).

Expand Down Expand Up @@ -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`.

Expand Down Expand Up @@ -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

Expand Down
57 changes: 57 additions & 0 deletions src/Builder/ShlinkClientBuilder.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<?php

declare(strict_types=1);

namespace Shlinkio\Shlink\SDK\Builder;

use Psr\Http\Client\ClientInterface;
use Psr\Http\Message\RequestFactoryInterface;
use Psr\Http\Message\StreamFactoryInterface;
use Shlinkio\Shlink\SDK\Config\ShlinkConfigInterface;
use Shlinkio\Shlink\SDK\Domains\DomainsClient;
use Shlinkio\Shlink\SDK\Domains\DomainsClientInterface;
use Shlinkio\Shlink\SDK\Http\Debug\HttpDebuggerInterface;
use Shlinkio\Shlink\SDK\Http\HttpClient;
use Shlinkio\Shlink\SDK\Http\HttpClientInterface;
use Shlinkio\Shlink\SDK\ShortUrls\ShortUrlsClient;
use Shlinkio\Shlink\SDK\ShortUrls\ShortUrlsClientInterface;
use Shlinkio\Shlink\SDK\Tags\TagsClient;
use Shlinkio\Shlink\SDK\Tags\TagsClientInterface;
use Shlinkio\Shlink\SDK\Visits\VisitsClient;
use Shlinkio\Shlink\SDK\Visits\VisitsClientInterface;

class ShlinkClientBuilder implements ShlinkClientBuilderInterface
{
public function __construct(
private ClientInterface $client,
private RequestFactoryInterface $requestFactory,
private StreamFactoryInterface $streamFactory,
private ?HttpDebuggerInterface $debugger = null,
) {
}

public function buildShortUrlsClient(ShlinkConfigInterface $config): ShortUrlsClientInterface
{
return new ShortUrlsClient($this->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);
}
}
22 changes: 22 additions & 0 deletions src/Builder/ShlinkClientBuilderInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php

declare(strict_types=1);

namespace Shlinkio\Shlink\SDK\Builder;

use Shlinkio\Shlink\SDK\Config\ShlinkConfigInterface;
use Shlinkio\Shlink\SDK\Domains\DomainsClientInterface;
use Shlinkio\Shlink\SDK\ShortUrls\ShortUrlsClientInterface;
use Shlinkio\Shlink\SDK\Tags\TagsClientInterface;
use Shlinkio\Shlink\SDK\Visits\VisitsClientInterface;

interface ShlinkClientBuilderInterface
{
public function buildShortUrlsClient(ShlinkConfigInterface $config): ShortUrlsClientInterface;

public function buildVisitsClient(ShlinkConfigInterface $config): VisitsClientInterface;

public function buildTagsClient(ShlinkConfigInterface $config): TagsClientInterface;

public function buildDomainsClient(ShlinkConfigInterface $config): DomainsClientInterface;
}
59 changes: 59 additions & 0 deletions src/Builder/SingletonShlinkClientBuilder.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<?php

declare(strict_types=1);

namespace Shlinkio\Shlink\SDK\Builder;

use Shlinkio\Shlink\SDK\Config\ShlinkConfigInterface;
use Shlinkio\Shlink\SDK\Domains\DomainsClientInterface;
use Shlinkio\Shlink\SDK\ShortUrls\ShortUrlsClientInterface;
use Shlinkio\Shlink\SDK\Tags\TagsClientInterface;
use Shlinkio\Shlink\SDK\Visits\VisitsClientInterface;

use function sprintf;

class SingletonShlinkClientBuilder implements ShlinkClientBuilderInterface
{
private array $instances = [];

public function __construct(private ShlinkClientBuilderInterface $wrapped)
{
}

public function buildShortUrlsClient(ShlinkConfigInterface $config): ShortUrlsClientInterface
{
$key = $this->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());
}
}
21 changes: 21 additions & 0 deletions test/Builder/ClientBuilderMethodsProviderTrait.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

declare(strict_types=1);

namespace ShlinkioTest\Shlink\SDK\Builder;

use ReflectionClass;
use Shlinkio\Shlink\SDK\Builder\ShlinkClientBuilderInterface;

trait ClientBuilderMethodsProviderTrait
{
public function provideMethods(): iterable
{
$ref = new ReflectionClass(ShlinkClientBuilderInterface::class);

foreach ($ref->getMethods() as $method) {
$name = $method->getName();
yield $name => [$name];
}
}
}
54 changes: 54 additions & 0 deletions test/Builder/ShlinkClientBuilderTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<?php

declare(strict_types=1);

namespace ShlinkioTest\Shlink\SDK\Builder;

use PHPUnit\Framework\TestCase;
use Prophecy\PhpUnit\ProphecyTrait;
use Prophecy\Prophecy\ObjectProphecy;
use Psr\Http\Client\ClientInterface;
use Psr\Http\Message\RequestFactoryInterface;
use Psr\Http\Message\StreamFactoryInterface;
use Shlinkio\Shlink\SDK\Builder\ShlinkClientBuilder;
use Shlinkio\Shlink\SDK\Config\ShlinkConfig;
use Shlinkio\Shlink\SDK\Config\ShlinkConfigInterface;

class ShlinkClientBuilderTest extends TestCase
{
use ClientBuilderMethodsProviderTrait;
use ProphecyTrait;

private ShlinkClientBuilder $builder;
private ObjectProphecy $client;
private ObjectProphecy $requestFactory;
private ObjectProphecy $streamFactory;
private ShlinkConfigInterface $config;

public function setUp(): void
{
$this->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);
}
}
Loading

0 comments on commit 03ac1f8

Please sign in to comment.