From 35829b10227ad5c5f27fc19ad9f3cf6c6548126a Mon Sep 17 00:00:00 2001 From: Michael Schmetter Date: Thu, 18 Jul 2024 11:49:45 +0200 Subject: [PATCH] Feat: Added possibility to create the Repository in the Builder (#214) --- README.md | 20 ++ src/Helper/UnleashBuilderContainer.php | 41 ++++ src/UnleashBuilder.php | 305 +++++++++++++++---------- tests/UnleashBuilderTest.php | 81 +++++++ 4 files changed, 326 insertions(+), 121 deletions(-) diff --git a/README.md b/README.md index 31cbf77c..4cee2eb3 100755 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ * [Builder](#builder) * [Required parameters](#required-parameters) * [Optional parameters](#optional-parameters) + * [Returning intermediate objects](#returning-intermediate-objects) * [Proxy SDK](#proxy-sdk) * [Caching](#caching) * [Bootstrapping](#bootstrapping) @@ -326,7 +327,26 @@ $builder = UnleashBuilder::create() 'Yet-Another-Header' => 'and-another-value', ]); ``` +#### Returning intermediate objects +For some use cases the builder can return intermediate objects, for example the `UnleashRepository` object. This can be +useful if you need to directly interact with the repository, to refresh the cache manually for example. + +```php +withAppName('Some app name') + ->withAppUrl(new Url('https://some-app-url.com', namePrefix: 'somePrefix.', tags: [ + 'myTag' => 'myValue', + ])) + ->withInstanceId('Some instance id') + ->buildRepository(); +$repository->refreshCache(); +``` ## Proxy SDK diff --git a/src/Helper/UnleashBuilderContainer.php b/src/Helper/UnleashBuilderContainer.php index 80bd346d..3925245f 100644 --- a/src/Helper/UnleashBuilderContainer.php +++ b/src/Helper/UnleashBuilderContainer.php @@ -5,7 +5,12 @@ use Psr\Http\Client\ClientInterface; use Psr\Http\Message\RequestFactoryInterface; use Psr\SimpleCache\CacheInterface; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; +use Unleash\Client\Bootstrap\BootstrapHandler; +use Unleash\Client\Bootstrap\BootstrapProvider; use Unleash\Client\Configuration\UnleashConfiguration; +use Unleash\Client\ContextProvider\UnleashContextProvider; +use Unleash\Client\Metrics\MetricsBucketSerializer; use Unleash\Client\Metrics\MetricsSender; use Unleash\Client\Stickiness\StickinessCalculator; @@ -19,9 +24,15 @@ public function __construct( private CacheInterface $staleCache, private ClientInterface $httpClient, private ?MetricsSender $metricsSender, + private CacheInterface $metricsCache, private RequestFactoryInterface $requestFactory, private StickinessCalculator $stickinessCalculator, private ?UnleashConfiguration $configuration, + private UnleashContextProvider $contextProvider, + private BootstrapHandler $bootstrapHandler, + private BootstrapProvider $bootstrapProvider, + private EventDispatcherInterface $eventDispatcher, + private MetricsBucketSerializer $metricsBucketSerializer, ) { } @@ -59,4 +70,34 @@ public function getConfiguration(): ?UnleashConfiguration { return $this->configuration; } + + public function getMetricsCache(): CacheInterface + { + return $this->metricsCache; + } + + public function getContextProvider(): UnleashContextProvider + { + return $this->contextProvider; + } + + public function getBootstrapHandler(): BootstrapHandler + { + return $this->bootstrapHandler; + } + + public function getBootstrapProvider(): BootstrapProvider + { + return $this->bootstrapProvider; + } + + public function getEventDispatcher(): EventDispatcherInterface + { + return $this->eventDispatcher; + } + + public function getMetricsBucketSerializer(): MetricsBucketSerializer + { + return $this->metricsBucketSerializer; + } } diff --git a/src/UnleashBuilder.php b/src/UnleashBuilder.php index 8208b2ce..25713833 100755 --- a/src/UnleashBuilder.php +++ b/src/UnleashBuilder.php @@ -22,7 +22,6 @@ use Unleash\Client\Bootstrap\JsonSerializableBootstrapProvider; use Unleash\Client\Client\DefaultRegistrationService; use Unleash\Client\Client\RegistrationService; -use Unleash\Client\Configuration\Context; use Unleash\Client\Configuration\UnleashConfiguration; use Unleash\Client\ContextProvider\DefaultUnleashContextProvider; use Unleash\Client\ContextProvider\UnleashContextProvider; @@ -42,8 +41,10 @@ use Unleash\Client\Metrics\DefaultMetricsSender; use Unleash\Client\Metrics\MetricsBucketSerializer; use Unleash\Client\Metrics\MetricsHandler; +use Unleash\Client\Metrics\MetricsSender; use Unleash\Client\Repository\DefaultUnleashProxyRepository; use Unleash\Client\Repository\DefaultUnleashRepository; +use Unleash\Client\Repository\UnleashRepository; use Unleash\Client\Stickiness\MurmurHashCalculator; use Unleash\Client\Strategy\ApplicationHostnameStrategyHandler; use Unleash\Client\Strategy\DefaultStrategyHandler; @@ -90,8 +91,6 @@ final class UnleashBuilder private ?int $metricsInterval = null; - private ?Context $defaultContext = null; - private ?UnleashContextProvider $contextProvider = null; private ?BootstrapProvider $bootstrapProvider = null; @@ -376,7 +375,80 @@ public function withMetricsBucketSerializer(?MetricsBucketSerializer $metricsBuc return $this->with('metricsBucketSerializer', $metricsBucketSerializer); } + public function buildRepository(): UnleashRepository + { + $dependencyContainer = $this->initializeContainerWithConfiguration(); + + return $this->createRepository($dependencyContainer); + } + public function build(): Unleash + { + $dependencyContainer = $this->initializeContainerWithConfiguration(); + $repository = $this->createRepository($dependencyContainer); + + assert($dependencyContainer->getConfiguration() !== null); + $metricsSender = new DefaultMetricsSender($dependencyContainer->getHttpClient(), $dependencyContainer->getRequestFactory(), $dependencyContainer->getConfiguration()); + + $dependencyContainer = $this->createContainer( + cache: $dependencyContainer->getCache(), + staleCache: $dependencyContainer->getStaleCache(), + httpClient: $dependencyContainer->getHttpClient(), + requestFactory: $dependencyContainer->getRequestFactory(), + contextProvider: $dependencyContainer->getContextProvider(), + bootstrapHandler: $dependencyContainer->getBootstrapHandler(), + bootstrapProvider: $dependencyContainer->getBootstrapProvider(), + eventDispatcher: $dependencyContainer->getEventDispatcher(), + metricsBucketSerializer: $dependencyContainer->getMetricsBucketSerializer(), + metricsCache: $dependencyContainer->getMetricsCache(), + metricsSender: $metricsSender, + configuration: $dependencyContainer->getConfiguration(), + ); + + assert($dependencyContainer->getConfiguration() !== null); + $registrationService = $this->registrationService ?? new DefaultRegistrationService($dependencyContainer->getHttpClient(), $dependencyContainer->getRequestFactory(), $dependencyContainer->getConfiguration()); + $metricsHandler = $this->metricsHandler ?? new DefaultMetricsHandler($metricsSender, $dependencyContainer->getConfiguration()); + $variantHandler = $this->variantHandler ?? new DefaultVariantHandler(new MurmurHashCalculator()); + + // initialization of dependencies + foreach ($this->strategies as $strategy) { + $this->initializeServices($strategy, $dependencyContainer); + } + $this->initializeServices($repository, $dependencyContainer); + $this->initializeServices($registrationService, $dependencyContainer); + $this->initializeServices($metricsHandler, $dependencyContainer); + $this->initializeServices($variantHandler, $dependencyContainer); + + assert($dependencyContainer->getConfiguration() !== null); + if ($this->proxyKey !== null) { + $dependencyContainer->getConfiguration()->setProxyKey($this->proxyKey); + $dependencyContainer->getConfiguration()->setHeaders(array_merge($this->headers, ['Authorization' => $this->proxyKey])); + $proxyRepository = new DefaultUnleashProxyRepository( + $dependencyContainer->getConfiguration(), + $dependencyContainer->getHttpClient(), + $dependencyContainer->getRequestFactory(), + ); + + return new DefaultProxyUnleash( + $proxyRepository, + $metricsHandler, + ); + } + + return new DefaultUnleash( + $this->strategies, + $repository, + $registrationService, + $dependencyContainer->getConfiguration(), + $metricsHandler, + $variantHandler, + ); + } + + /** + * @return array + */ + public function initializeBasicAttributes(): array { // basic scalar attributes $appUrl = $this->appUrl; @@ -401,73 +473,83 @@ public function build(): Unleash ); } - // PSR services - $cache = $this->cache; - if ($cache === null) { - $cache = $this->defaultImplementationLocator->findCache(); - if ($cache === null) { - throw new InvalidValueException( - sprintf( - "No cache implementation provided, please use 'withCacheHandler()' method or install one of officially supported clients: '%s'", - implode("', '", $this->defaultImplementationLocator->getCachePackages()) - ) - ); - } - } - assert($cache instanceof CacheInterface); - $staleCache = $this->staleCache ?? $cache; - $metricsCache = $this->metricsCache ?? $cache; - - $httpClient = $this->httpClient; - if ($httpClient === null) { - $httpClient = $this->defaultImplementationLocator->findHttpClient(); - if ($httpClient === null) { - throw new InvalidValueException( - "No http client provided, please use 'withHttpClient()' method or install a package providing 'psr/http-client-implementation'.", - ); - } - } - assert($httpClient instanceof ClientInterface); - - $requestFactory = $this->requestFactory; - if ($requestFactory === null) { - $requestFactory = $this->defaultImplementationLocator->findRequestFactory(); - /** - * This will only be thrown if a HTTP client was found, but a request factory is not. - * Due to how php-http/discovery works, this scenario is unlikely to happen. - * See linked comment for more info. - * - * https://github.com/Unleash/unleash-client-php/pull/27#issuecomment-920764416 - */ - // @codeCoverageIgnoreStart - if ($requestFactory === null) { - throw new InvalidValueException( - "No request factory provided, please use 'withRequestFactory()' method or install a package providing 'psr/http-factory-implementation'.", - ); - } - // @codeCoverageIgnoreEnd - } - assert($requestFactory instanceof RequestFactoryInterface); - - // internal shared services needed for UnleashConfiguration - - $hashCalculator = new MurmurHashCalculator(); - - $dependencyContainer = new UnleashBuilderContainer( + return [$appUrl, $instanceId, $appName]; + } + + private function createContainer( + CacheInterface $cache, + CacheInterface $staleCache, + ClientInterface $httpClient, + RequestFactoryInterface $requestFactory, + UnleashContextProvider $contextProvider, + BootstrapHandler $bootstrapHandler, + BootstrapProvider $bootstrapProvider, + EventDispatcherInterface $eventDispatcher, + MetricsBucketSerializer $metricsBucketSerializer, + CacheInterface $metricsCache, + ?MetricsSender $metricsSender = null, + ?UnleashConfiguration $configuration = null, + ): UnleashBuilderContainer { + return new UnleashBuilderContainer( cache: $cache, staleCache: $staleCache, httpClient: $httpClient, - metricsSender: null, + metricsSender: $metricsSender, + metricsCache: $metricsCache, requestFactory: $requestFactory, - stickinessCalculator: $hashCalculator, - configuration: null, + stickinessCalculator: new MurmurHashCalculator(), + configuration: $configuration, + contextProvider: $contextProvider, + bootstrapHandler: $bootstrapHandler, + bootstrapProvider: $bootstrapProvider, + eventDispatcher: $eventDispatcher, + metricsBucketSerializer: $metricsBucketSerializer, ); + } - $contextProvider = $this->contextProvider; - if ($contextProvider === null) { - $contextProvider = new DefaultUnleashContextProvider(); + private function initializeContainerWithConfiguration(): UnleashBuilderContainer + { + [$appUrl, $instanceId, $appName] = $this->initializeBasicAttributes(); + $cache = $this->cache ?? $this->defaultImplementationLocator->findCache(); + $httpClient = $this->httpClient ?? $this->defaultImplementationLocator->findHttpClient(); + $requestFactory = $this->requestFactory ?? $this->defaultImplementationLocator->findRequestFactory(); + + if (!$cache) { + throw new InvalidValueException( + sprintf( + "No cache implementation provided, please use 'withCacheHandler()' method or install one of officially supported clients: '%s'", + implode("', '", $this->defaultImplementationLocator->getCachePackages()) + ) + ); + } + if (!$httpClient) { + throw new InvalidValueException( + "No http client provided, please use 'withHttpClient()' method or install a package providing 'psr/http-client-implementation'." + ); + } + /** + * This will only be thrown if a HTTP client was found, but a request factory is not. + * Due to how php-http/discovery works, this scenario is unlikely to happen. + * See linked comment for more info. + * + * https://github.com/Unleash/unleash-client-php/pull/27#issuecomment-920764416 + */ + // @codeCoverageIgnoreStart + if (!$requestFactory) { + throw new InvalidValueException( + "No request factory provided, please use 'withRequestFactory()' method or install a package providing 'psr/http-factory-implementation'." + ); } + // @codeCoverageIgnoreEnd + + assert($cache instanceof CacheInterface); + assert($httpClient instanceof ClientInterface); + assert($requestFactory instanceof RequestFactoryInterface); + + $staleCache = $this->staleCache ?? $cache; + $metricsCache = $this->metricsCache ?? $cache; + $contextProvider = $this->contextProvider ?? new DefaultUnleashContextProvider(); $bootstrapHandler = $this->bootstrapHandler ?? new DefaultBootstrapHandler(); $bootstrapProvider = $this->bootstrapProvider ?? new EmptyBootstrapProvider(); $eventDispatcher = $this->eventDispatcher ?? new EventDispatcher(); @@ -476,6 +558,19 @@ public function build(): Unleash } $metricsBucketSerializer = $this->metricsBucketSerializer ?? new DefaultMetricsBucketSerializer(); + $dependencyContainer = $this->createContainer( + cache: $cache, + staleCache: $staleCache, + httpClient: $httpClient, + requestFactory: $requestFactory, + contextProvider: $contextProvider, + bootstrapHandler: $bootstrapHandler, + bootstrapProvider: $bootstrapProvider, + eventDispatcher: $eventDispatcher, + metricsBucketSerializer: $metricsBucketSerializer, + metricsCache: $metricsCache + ); + // initialize services $this->initializeServices($contextProvider, $dependencyContainer); $this->initializeServices($bootstrapHandler, $dependencyContainer); @@ -489,79 +584,35 @@ public function build(): Unleash $configuration = new UnleashConfiguration($appUrl, $appName, $instanceId); $configuration - ->setCache($cache) - ->setStaleCache($staleCache) - ->setMetricsCache($metricsCache) + ->setCache($dependencyContainer->getCache()) + ->setStaleCache($dependencyContainer->getStaleCache()) + ->setMetricsCache($dependencyContainer->getMetricsCache()) ->setTtl($this->cacheTtl ?? $configuration->getTtl()) ->setStaleTtl($this->staleTtl ?? $configuration->getStaleTtl()) ->setMetricsEnabled($this->metricsEnabled ?? $configuration->isMetricsEnabled()) ->setMetricsInterval($this->metricsInterval ?? $configuration->getMetricsInterval()) ->setHeaders($this->headers) ->setAutoRegistrationEnabled($this->autoregister) - ->setContextProvider($contextProvider) - ->setBootstrapHandler($bootstrapHandler) - ->setBootstrapProvider($bootstrapProvider) + ->setContextProvider($dependencyContainer->getContextProvider()) + ->setBootstrapHandler($dependencyContainer->getBootstrapHandler()) + ->setBootstrapProvider($dependencyContainer->getBootstrapProvider()) ->setFetchingEnabled($this->fetchingEnabled) - ->setEventDispatcher($eventDispatcher) - ->setMetricsBucketSerializer($metricsBucketSerializer) - ; - - // runtime-unchangeable blocks of Unleash + ->setEventDispatcher($dependencyContainer->getEventDispatcher()) + ->setMetricsBucketSerializer($dependencyContainer->getMetricsBucketSerializer()); - $repository = new DefaultUnleashRepository($httpClient, $requestFactory, $configuration); - $metricsSender = new DefaultMetricsSender($httpClient, $requestFactory, $configuration); - - $dependencyContainer = new UnleashBuilderContainer( + return $this->createContainer( cache: $cache, staleCache: $staleCache, httpClient: $httpClient, - metricsSender: $metricsSender, requestFactory: $requestFactory, - stickinessCalculator: $hashCalculator, + contextProvider: $contextProvider, + bootstrapHandler: $bootstrapHandler, + bootstrapProvider: $bootstrapProvider, + eventDispatcher: $eventDispatcher, + metricsBucketSerializer: $metricsBucketSerializer, + metricsCache: $metricsCache, configuration: $configuration, ); - - $registrationService = $this->registrationService; - if ($registrationService === null) { - $registrationService = new DefaultRegistrationService($httpClient, $requestFactory, $configuration); - } - - $metricsHandler = $this->metricsHandler ?? new DefaultMetricsHandler($metricsSender, $configuration); - $variantHandler = $this->variantHandler ?? new DefaultVariantHandler($hashCalculator); - - // initialization of dependencies - - foreach ($this->strategies as $strategy) { - $this->initializeServices($strategy, $dependencyContainer); - } - $this->initializeServices($repository, $dependencyContainer); - $this->initializeServices($registrationService, $dependencyContainer); - $this->initializeServices($metricsHandler, $dependencyContainer); - $this->initializeServices($variantHandler, $dependencyContainer); - - if ($this->proxyKey !== null) { - $configuration->setProxyKey($this->proxyKey); - $configuration->setHeaders(array_merge($this->headers, ['Authorization' => $this->proxyKey])); - $proxyRepository = new DefaultUnleashProxyRepository( - $configuration, - $httpClient, - $requestFactory, - ); - - return new DefaultProxyUnleash( - $proxyRepository, - $metricsHandler, - ); - } else { - return new DefaultUnleash( - $this->strategies, - $repository, - $registrationService, - $configuration, - $metricsHandler, - $variantHandler, - ); - } } private function with(string $property, mixed $value): self @@ -610,4 +661,16 @@ private function initializeServices(object $target, UnleashBuilderContainer $con $target->setStickinessCalculator($container->getStickinessCalculator()); } } + + /** + * @param UnleashBuilderContainer $dependencyContainer + * + * @return DefaultUnleashRepository + */ + private function createRepository(UnleashBuilderContainer $dependencyContainer): DefaultUnleashRepository + { + assert($dependencyContainer->getConfiguration() !== null); + + return new DefaultUnleashRepository($dependencyContainer->getHttpClient(), $dependencyContainer->getRequestFactory(), $dependencyContainer->getConfiguration()); + } } diff --git a/tests/UnleashBuilderTest.php b/tests/UnleashBuilderTest.php index 0309559e..03927c65 100755 --- a/tests/UnleashBuilderTest.php +++ b/tests/UnleashBuilderTest.php @@ -928,6 +928,87 @@ public function deserialize(string $serialized): MetricsBucket self::assertSame($serializer, $this->getConfiguration($unleash)->getMetricsBucketSerializer()); } + public function testBuildRepository() + { + try { + $this->instance->buildRepository(); + self::fail('Expected exception: ' . InvalidValueException::class); + } catch (InvalidValueException $e) { + } + + try { + $this->instance + ->withAppUrl('https://example.com') + ->buildRepository(); + self::fail('Expected exception: ' . InvalidValueException::class); + } catch (InvalidValueException $e) { + } + + try { + $this->instance + ->withAppUrl('https://example.com') + ->withInstanceId('test') + ->buildRepository(); + self::fail('Expected exception: ' . InvalidValueException::class); + } catch (InvalidValueException $e) { + } + + $repository = $this->instance + ->withAppUrl('https://example.com') + ->withAppName('Test App') + ->withInstanceId('test') + ->withAutomaticRegistrationEnabled(false) + ->buildRepository(); + $reflection = new ReflectionObject($repository); + $configurationProperty = $reflection->getProperty('configuration'); + $configurationProperty->setAccessible(true); + $configuration = $configurationProperty->getValue($repository); + assert($configuration instanceof UnleashConfiguration); + + self::assertEquals('https://example.com', $configuration->getUrl()); + self::assertEquals('Test App', $configuration->getAppName()); + self::assertEquals('test', $configuration->getInstanceId()); + self::assertNotNull($configuration->getCache()); + self::assertIsInt($configuration->getTtl()); + + $requestFactory = $this->newRequestFactory(); + $httpClient = $this->newHttpClient(); + + $repository = $this->instance + ->withAppUrl('https://example.com') + ->withAppName('Test App') + ->withInstanceId('test') + ->withAutomaticRegistrationEnabled(false) + ->withRequestFactory($requestFactory) + ->withHttpClient($httpClient) + ->buildRepository(); + $reflection = new ReflectionObject($repository); + $httpClientProperty = $reflection->getProperty('httpClient'); + $httpClientProperty->setAccessible(true); + $requestFactoryProperty = $reflection->getProperty('requestFactory'); + $requestFactoryProperty->setAccessible(true); + self::assertEquals($httpClient, $httpClientProperty->getValue($repository)); + self::assertEquals($requestFactory, $requestFactoryProperty->getValue($repository)); + + $cacheHandler = $this->getCache(); + $repository = $this->instance + ->withAppUrl('https://example.com') + ->withAppName('Test App') + ->withInstanceId('test') + ->withAutomaticRegistrationEnabled(false) + ->withCacheHandler($cacheHandler) + ->withCacheTimeToLive(359) + ->buildRepository(); + + $reflection = new ReflectionObject($repository); + $configurationProperty = $reflection->getProperty('configuration'); + $configurationProperty->setAccessible(true); + $configuration = $configurationProperty->getValue($repository); + assert($configuration instanceof UnleashConfiguration); + self::assertEquals($cacheHandler, $configuration->getCache()); + self::assertEquals(359, $configuration->getTtl()); + } + private function getConfiguration(DefaultUnleash $unleash): UnleashConfiguration { $configurationProperty = (new ReflectionObject($unleash))->getProperty('configuration');