From 467ec6dff3adbf575ffd83e0b9c928a10270c9a0 Mon Sep 17 00:00:00 2001 From: Arthur Mogliev Date: Sun, 4 Apr 2021 16:02:31 +0400 Subject: [PATCH] - support PHP attributes as metadata source - changes for PSR-11 container integration to simplify metadata provider switch --- .gitattributes | 3 +- .github/workflows/run-tests.yml | 7 +- phpspec.yml | 11 +- phpspec.yml.8.0 | 16 + .../Attribute/Factory/PluginManagerSpec.php | 61 ++ .../PathHandler/Consumer/Factory/JsonSpec.php | 25 + .../Consumer/Factory/PluginManagerSpec.php | 61 ++ .../Articus/PathHandler/Consumer/JsonSpec.php | 24 +- .../Handler/Factory/PluginManagerSpec.php | 61 ++ .../Factory/AnnotationSpec.php | 146 +++ .../Factory/PhpAttributeSpec.php | 156 +++ .../MetadataProvider/PhpAttributeSpec.php | 907 ++++++++++++++++++ .../Producer/Factory/PluginManagerSpec.php | 61 ++ .../RouteInjection/FactorySpec.php | 489 ---------- .../PathHandler/RouteInjectionFactorySpec.php | 339 +++++++ .../Handler/NoMethodsHandlingHttpMethods.php | 19 + spec/ExampleForPhp8/Handler/NoRoutes.php | 15 + .../SeveralMethodsForSingleHttpMethod.php | 21 + .../SeveralRequiredParametersMethod.php | 16 + .../Handler/ValidAttributes.php | 28 + .../Handler/ValidCommonAttributes.php | 35 + .../Handler/ValidCommonConsumers.php | 37 + .../Handler/ValidCommonProducers.php | 35 + .../ExampleForPhp8/Handler/ValidConsumers.php | 29 + .../Handler/ValidHttpMethods.php | 45 + spec/ExampleForPhp8/Handler/ValidMethod.php | 16 + .../ExampleForPhp8/Handler/ValidProducers.php | 28 + spec/ExampleForPhp8/Handler/ValidRoutes.php | 23 + spec/Matcher/PropertyValue.php | 78 ++ spec/Matcher/PropertyValueType.php | 81 ++ .../Attribute/Factory/PluginManager.php | 20 + .../PathHandler/CacheKeyAwareTrait.php | 33 + .../PathHandler/Consumer/Factory/Json.php | 17 + .../Consumer/Factory/PluginManager.php | 20 + src/Articus/PathHandler/Consumer/Json.php | 16 +- .../PathHandler/Consumer/PluginManager.php | 2 +- .../Handler/Factory/PluginManager.php | 20 + .../{ => Handler}/PluginManager.php | 2 +- .../MetadataProvider/Annotation.php | 2 +- .../MetadataProvider/Factory/Annotation.php | 30 + .../MetadataProvider/Factory/PhpAttribute.php | 30 + .../MetadataProvider/PhpAttribute.php | 360 +++++++ .../PathHandler/PhpAttribute/Attribute.php | 29 + .../PathHandler/PhpAttribute/Consumer.php | 33 + .../PathHandler/PhpAttribute/Delete.php | 16 + src/Articus/PathHandler/PhpAttribute/Get.php | 16 + .../PathHandler/PhpAttribute/HttpMethod.php | 20 + .../PathHandler/PhpAttribute/Patch.php | 16 + src/Articus/PathHandler/PhpAttribute/Post.php | 16 + .../PathHandler/PhpAttribute/Producer.php | 33 + src/Articus/PathHandler/PhpAttribute/Put.php | 16 + .../PathHandler/PhpAttribute/Route.php | 34 + .../Producer/Factory/PluginManager.php | 20 + .../PathHandler/RouteInjection/Factory.php | 282 ------ .../PathHandler/RouteInjection/Options.php | 86 -- .../PathHandler/RouteInjectionFactory.php | 130 +++ 56 files changed, 3302 insertions(+), 870 deletions(-) create mode 100644 phpspec.yml.8.0 create mode 100644 spec/Articus/PathHandler/Attribute/Factory/PluginManagerSpec.php create mode 100644 spec/Articus/PathHandler/Consumer/Factory/JsonSpec.php create mode 100644 spec/Articus/PathHandler/Consumer/Factory/PluginManagerSpec.php create mode 100644 spec/Articus/PathHandler/Handler/Factory/PluginManagerSpec.php create mode 100644 spec/Articus/PathHandler/MetadataProvider/Factory/AnnotationSpec.php create mode 100644 spec/Articus/PathHandler/MetadataProvider/Factory/PhpAttributeSpec.php create mode 100644 spec/Articus/PathHandler/MetadataProvider/PhpAttributeSpec.php create mode 100644 spec/Articus/PathHandler/Producer/Factory/PluginManagerSpec.php delete mode 100644 spec/Articus/PathHandler/RouteInjection/FactorySpec.php create mode 100644 spec/Articus/PathHandler/RouteInjectionFactorySpec.php create mode 100644 spec/ExampleForPhp8/Handler/NoMethodsHandlingHttpMethods.php create mode 100644 spec/ExampleForPhp8/Handler/NoRoutes.php create mode 100644 spec/ExampleForPhp8/Handler/SeveralMethodsForSingleHttpMethod.php create mode 100644 spec/ExampleForPhp8/Handler/SeveralRequiredParametersMethod.php create mode 100644 spec/ExampleForPhp8/Handler/ValidAttributes.php create mode 100644 spec/ExampleForPhp8/Handler/ValidCommonAttributes.php create mode 100644 spec/ExampleForPhp8/Handler/ValidCommonConsumers.php create mode 100644 spec/ExampleForPhp8/Handler/ValidCommonProducers.php create mode 100644 spec/ExampleForPhp8/Handler/ValidConsumers.php create mode 100644 spec/ExampleForPhp8/Handler/ValidHttpMethods.php create mode 100644 spec/ExampleForPhp8/Handler/ValidMethod.php create mode 100644 spec/ExampleForPhp8/Handler/ValidProducers.php create mode 100644 spec/ExampleForPhp8/Handler/ValidRoutes.php create mode 100644 spec/Matcher/PropertyValue.php create mode 100644 spec/Matcher/PropertyValueType.php create mode 100644 src/Articus/PathHandler/Attribute/Factory/PluginManager.php create mode 100644 src/Articus/PathHandler/CacheKeyAwareTrait.php create mode 100644 src/Articus/PathHandler/Consumer/Factory/Json.php create mode 100644 src/Articus/PathHandler/Consumer/Factory/PluginManager.php create mode 100644 src/Articus/PathHandler/Handler/Factory/PluginManager.php rename src/Articus/PathHandler/{ => Handler}/PluginManager.php (81%) create mode 100644 src/Articus/PathHandler/MetadataProvider/Factory/Annotation.php create mode 100644 src/Articus/PathHandler/MetadataProvider/Factory/PhpAttribute.php create mode 100644 src/Articus/PathHandler/MetadataProvider/PhpAttribute.php create mode 100644 src/Articus/PathHandler/PhpAttribute/Attribute.php create mode 100644 src/Articus/PathHandler/PhpAttribute/Consumer.php create mode 100644 src/Articus/PathHandler/PhpAttribute/Delete.php create mode 100644 src/Articus/PathHandler/PhpAttribute/Get.php create mode 100644 src/Articus/PathHandler/PhpAttribute/HttpMethod.php create mode 100644 src/Articus/PathHandler/PhpAttribute/Patch.php create mode 100644 src/Articus/PathHandler/PhpAttribute/Post.php create mode 100644 src/Articus/PathHandler/PhpAttribute/Producer.php create mode 100644 src/Articus/PathHandler/PhpAttribute/Put.php create mode 100644 src/Articus/PathHandler/PhpAttribute/Route.php create mode 100644 src/Articus/PathHandler/Producer/Factory/PluginManager.php delete mode 100644 src/Articus/PathHandler/RouteInjection/Factory.php delete mode 100644 src/Articus/PathHandler/RouteInjection/Options.php create mode 100644 src/Articus/PathHandler/RouteInjectionFactory.php diff --git a/.gitattributes b/.gitattributes index f04af34..e1c854d 100644 --- a/.gitattributes +++ b/.gitattributes @@ -8,4 +8,5 @@ composer.lock export-ignore composer.lock.* export-ignore composer.phar export-ignore mkdocs.yml export-ignore -phpspec.yml export-ignore \ No newline at end of file +phpspec.yml export-ignore +phpspec.yml.* export-ignore \ No newline at end of file diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index 4f5e631..ae63d86 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -18,7 +18,8 @@ jobs: # ext-uopz does not support PHP 8 officially yet, so have to use custom build https://github.com/krakjoe/uopz/pull/138 - php: '8.0' upload_coverage: true - container: ghcr.io/articus/phpdbg-coveralls:${{ matrix.php }}_2.4.3_2021-01-16 + has_unique_phpspec_tests: true + container: ghcr.io/articus/phpdbg-coveralls:${{ matrix.php }}_2.4.3_2021-03-21 steps: - name: Checkout code uses: actions/checkout@v2 @@ -33,6 +34,10 @@ jobs: - name: Install dependencies via Composer run: php ./composer.phar install --no-interaction --no-progress --prefer-dist --classmap-authoritative + - name: Use unique phpspec.yml + if: matrix.has_unique_phpspec_tests + run: cp ./phpspec.yml.${{ matrix.php }} ./phpspec.yml + - name: Run PhpSpec tests run: phpdbg -qrr ./vendor/phpspec/phpspec/bin/phpspec run diff --git a/phpspec.yml b/phpspec.yml index 7b4264f..2a8993d 100644 --- a/phpspec.yml +++ b/phpspec.yml @@ -1,5 +1,8 @@ bootstrap: spec/bootstrap.php formatter.name: pretty +matchers: + - spec\Matcher\PropertyValue + - spec\Matcher\PropertyValueType extensions: FriendsOfPhpSpec\PhpSpec\CodeCoverage\CodeCoverageExtension: format: @@ -10,4 +13,10 @@ extensions: #html: spec_output/phpspec.coverage clover: spec_output/phpspec.coverage.xml whitelist: - - src \ No newline at end of file + - src + #Exclude files with PHP 8+ code + blacklist: + - src/Articus/PathHandler/PhpAttribute + blacklist_files: + - src/Articus/PathHandler/MetadataProvider/PhpAttribute.php + - src/Articus/PathHandler/MetadataProvider/Factory/PhpAttribute.php \ No newline at end of file diff --git a/phpspec.yml.8.0 b/phpspec.yml.8.0 new file mode 100644 index 0000000..269a9b3 --- /dev/null +++ b/phpspec.yml.8.0 @@ -0,0 +1,16 @@ +bootstrap: spec/bootstrap.php +formatter.name: pretty +matchers: + - spec\Matcher\PropertyValue + - spec\Matcher\PropertyValueType +extensions: + FriendsOfPhpSpec\PhpSpec\CodeCoverage\CodeCoverageExtension: + format: + #- html + - text + - clover + output: + #html: spec_output/phpspec.coverage + clover: spec_output/phpspec.coverage.xml + whitelist: + - src diff --git a/spec/Articus/PathHandler/Attribute/Factory/PluginManagerSpec.php b/spec/Articus/PathHandler/Attribute/Factory/PluginManagerSpec.php new file mode 100644 index 0000000..30f9a97 --- /dev/null +++ b/spec/Articus/PathHandler/Attribute/Factory/PluginManagerSpec.php @@ -0,0 +1,61 @@ +get('config')->shouldBeCalledOnce()->willReturn($config); + $config->offsetExists($configKey)->shouldBeCalledOnce()->willReturn(true); + $config->offsetGet($configKey)->shouldBeCalledOnce(); + + $service = $this->__invoke($container, ''); + $service->shouldBeAnInstanceOf(PH\Attribute\PluginManager::class); + } + + public function it_gets_configuration_from_custom_config_key(ContainerInterface $container, \ArrayAccess $config) + { + $configKey = 'test_config_key'; + $container->get('config')->shouldBeCalledOnce()->willReturn($config); + $config->offsetExists($configKey)->shouldBeCalledOnce()->willReturn(true); + $config->offsetGet($configKey)->shouldBeCalledOnce(); + + $this->beConstructedWith($configKey); + $service = $this->__invoke($container, ''); + $service->shouldBeAnInstanceOf(PH\Attribute\PluginManager::class); + } + + public function it_constructs_itself_and_gets_configuration_from_custom_config_key(ContainerInterface $container, \ArrayAccess $config) + { + $configKey = 'test_config_key'; + $container->get('config')->shouldBeCalledOnce()->willReturn($config); + $config->offsetExists($configKey)->shouldBeCalledOnce()->willReturn(true); + $config->offsetGet($configKey)->shouldBeCalledOnce(); + + $service = $this::__callStatic($configKey, [$container, '', null]); + $service->shouldBeAnInstanceOf(PH\Attribute\PluginManager::class); + } + + public function it_throws_on_too_few_arguments_during_self_construct(ContainerInterface $container) + { + $configKey = 'test_config_key'; + $error = new \InvalidArgumentException(\sprintf( + 'To invoke %s with custom configuration key statically 3 arguments are required: container, service name and options.', + PH\Attribute\Factory\PluginManager::class + )); + + $this::shouldThrow($error)->during('__callStatic', [$configKey, []]); + $this::shouldThrow($error)->during('__callStatic', [$configKey, [$container]]); + $this::shouldThrow($error)->during('__callStatic', [$configKey, [$container, '']]); + } +} diff --git a/spec/Articus/PathHandler/Consumer/Factory/JsonSpec.php b/spec/Articus/PathHandler/Consumer/Factory/JsonSpec.php new file mode 100644 index 0000000..7587538 --- /dev/null +++ b/spec/Articus/PathHandler/Consumer/Factory/JsonSpec.php @@ -0,0 +1,25 @@ +__invoke($container, 'test'); + $service->shouldBeAnInstanceOf(PH\Consumer\Json::class); + $service->shouldHaveProperty('parseAsStdClass', false); + } + + public function it_builds_json_consumer_with_specified_options(ContainerInterface $container) + { + $service = $this->__invoke($container, 'test', ['parse_as_std_class' => true]); + $service->shouldBeAnInstanceOf(PH\Consumer\Json::class); + $service->shouldHaveProperty('parseAsStdClass', true); + } +} diff --git a/spec/Articus/PathHandler/Consumer/Factory/PluginManagerSpec.php b/spec/Articus/PathHandler/Consumer/Factory/PluginManagerSpec.php new file mode 100644 index 0000000..0556cd7 --- /dev/null +++ b/spec/Articus/PathHandler/Consumer/Factory/PluginManagerSpec.php @@ -0,0 +1,61 @@ +get('config')->shouldBeCalledOnce()->willReturn($config); + $config->offsetExists($configKey)->shouldBeCalledOnce()->willReturn(true); + $config->offsetGet($configKey)->shouldBeCalledOnce(); + + $service = $this->__invoke($container, ''); + $service->shouldBeAnInstanceOf(PH\Consumer\PluginManager::class); + } + + public function it_gets_configuration_from_custom_config_key(ContainerInterface $container, \ArrayAccess $config) + { + $configKey = 'test_config_key'; + $container->get('config')->shouldBeCalledOnce()->willReturn($config); + $config->offsetExists($configKey)->shouldBeCalledOnce()->willReturn(true); + $config->offsetGet($configKey)->shouldBeCalledOnce(); + + $this->beConstructedWith($configKey); + $service = $this->__invoke($container, ''); + $service->shouldBeAnInstanceOf(PH\Consumer\PluginManager::class); + } + + public function it_constructs_itself_and_gets_configuration_from_custom_config_key(ContainerInterface $container, \ArrayAccess $config) + { + $configKey = 'test_config_key'; + $container->get('config')->shouldBeCalledOnce()->willReturn($config); + $config->offsetExists($configKey)->shouldBeCalledOnce()->willReturn(true); + $config->offsetGet($configKey)->shouldBeCalledOnce(); + + $service = $this::__callStatic($configKey, [$container, '', null]); + $service->shouldBeAnInstanceOf(PH\Consumer\PluginManager::class); + } + + public function it_throws_on_too_few_arguments_during_self_construct(ContainerInterface $container) + { + $configKey = 'test_config_key'; + $error = new \InvalidArgumentException(\sprintf( + 'To invoke %s with custom configuration key statically 3 arguments are required: container, service name and options.', + PH\Consumer\Factory\PluginManager::class + )); + + $this::shouldThrow($error)->during('__callStatic', [$configKey, []]); + $this::shouldThrow($error)->during('__callStatic', [$configKey, [$container]]); + $this::shouldThrow($error)->during('__callStatic', [$configKey, [$container, '']]); + } +} diff --git a/spec/Articus/PathHandler/Consumer/JsonSpec.php b/spec/Articus/PathHandler/Consumer/JsonSpec.php index 0d3a1f1..44b194f 100644 --- a/spec/Articus/PathHandler/Consumer/JsonSpec.php +++ b/spec/Articus/PathHandler/Consumer/JsonSpec.php @@ -9,13 +9,9 @@ class JsonSpec extends ObjectBehavior { - public function let() - { - $this->shouldImplement(PH\Consumer\ConsumerInterface::class); - } - public function it_parses_valid_json_null_from_body(StreamInterface $body) { + $this->beConstructedWith(false); $data = null; $json = 'null'; $body->getContents()->shouldBeCalledOnce()->willReturn($json); @@ -24,6 +20,7 @@ public function it_parses_valid_json_null_from_body(StreamInterface $body) public function it_parses_valid_json_int_in_body(StreamInterface $body) { + $this->beConstructedWith(false); $data = 123; $json = '123'; $body->getContents()->shouldBeCalledOnce()->willReturn($json); @@ -32,6 +29,7 @@ public function it_parses_valid_json_int_in_body(StreamInterface $body) public function it_parses_valid_json_float_in_body(StreamInterface $body) { + $this->beConstructedWith(false); $data = 123.456; $json = '123.456'; $body->getContents()->shouldBeCalledOnce()->willReturn($json); @@ -40,6 +38,7 @@ public function it_parses_valid_json_float_in_body(StreamInterface $body) public function it_parses_valid_json_string_in_body(StreamInterface $body) { + $this->beConstructedWith(false); $data = 'qwer'; $json = '"qwer"'; $body->getContents()->shouldBeCalledOnce()->willReturn($json); @@ -48,22 +47,35 @@ public function it_parses_valid_json_string_in_body(StreamInterface $body) public function it_parses_valid_json_array_from_body(StreamInterface $body) { + $this->beConstructedWith(false); $data = [null, 123, 123.456, 'qwer']; $json = '[null,123,123.456,"qwer"]'; $body->getContents()->shouldBeCalledOnce()->willReturn($json); $this->parse($body, null, 'mime/test', [])->shouldBe($data); } - public function it_parses_valid_json_object_from_body(StreamInterface $body) + public function it_parses_valid_json_object_from_body_as_array(StreamInterface $body) { + $this->beConstructedWith(false); $data = ['test' => 123]; $json = '{"test": 123}'; $body->getContents()->shouldBeCalledOnce()->willReturn($json); $this->parse($body, null, 'mime/test', [])->shouldBe($data); } + public function it_parses_valid_json_object_from_body_as_std_class(StreamInterface $body) + { + $this->beConstructedWith(true); + $data = new \stdClass(); + $data->test = 123; + $json = '{"test": 123}'; + $body->getContents()->shouldBeCalledOnce()->willReturn($json); + $this->parse($body, null, 'mime/test', [])->shouldBeLike($data); + } + public function it_throws_on_invalid_json_in_body(StreamInterface $body) { + $this->beConstructedWith(false); $json = '{'; $body->getContents()->shouldBeCalledOnce()->willReturn($json); $this->shouldThrow(PH\Exception\BadRequest::class)->during('parse', [$body, null, 'mime/test', []]); diff --git a/spec/Articus/PathHandler/Handler/Factory/PluginManagerSpec.php b/spec/Articus/PathHandler/Handler/Factory/PluginManagerSpec.php new file mode 100644 index 0000000..21cd37c --- /dev/null +++ b/spec/Articus/PathHandler/Handler/Factory/PluginManagerSpec.php @@ -0,0 +1,61 @@ +get('config')->shouldBeCalledOnce()->willReturn($config); + $config->offsetExists($configKey)->shouldBeCalledOnce()->willReturn(true); + $config->offsetGet($configKey)->shouldBeCalledOnce(); + + $service = $this->__invoke($container, ''); + $service->shouldBeAnInstanceOf(PH\Handler\PluginManager::class); + } + + public function it_gets_configuration_from_custom_config_key(ContainerInterface $container, \ArrayAccess $config) + { + $configKey = 'test_config_key'; + $container->get('config')->shouldBeCalledOnce()->willReturn($config); + $config->offsetExists($configKey)->shouldBeCalledOnce()->willReturn(true); + $config->offsetGet($configKey)->shouldBeCalledOnce(); + + $this->beConstructedWith($configKey); + $service = $this->__invoke($container, ''); + $service->shouldBeAnInstanceOf(PH\Handler\PluginManager::class); + } + + public function it_constructs_itself_and_gets_configuration_from_custom_config_key(ContainerInterface $container, \ArrayAccess $config) + { + $configKey = 'test_config_key'; + $container->get('config')->shouldBeCalledOnce()->willReturn($config); + $config->offsetExists($configKey)->shouldBeCalledOnce()->willReturn(true); + $config->offsetGet($configKey)->shouldBeCalledOnce(); + + $service = $this::__callStatic($configKey, [$container, '', null]); + $service->shouldBeAnInstanceOf(PH\Handler\PluginManager::class); + } + + public function it_throws_on_too_few_arguments_during_self_construct(ContainerInterface $container) + { + $configKey = 'test_config_key'; + $error = new \InvalidArgumentException(\sprintf( + 'To invoke %s with custom configuration key statically 3 arguments are required: container, service name and options.', + PH\Handler\Factory\PluginManager::class + )); + + $this::shouldThrow($error)->during('__callStatic', [$configKey, []]); + $this::shouldThrow($error)->during('__callStatic', [$configKey, [$container]]); + $this::shouldThrow($error)->during('__callStatic', [$configKey, [$container, '']]); + } +} diff --git a/spec/Articus/PathHandler/MetadataProvider/Factory/AnnotationSpec.php b/spec/Articus/PathHandler/MetadataProvider/Factory/AnnotationSpec.php new file mode 100644 index 0000000..a829eca --- /dev/null +++ b/spec/Articus/PathHandler/MetadataProvider/Factory/AnnotationSpec.php @@ -0,0 +1,146 @@ +get(PH\Handler\PluginManager::class)->shouldBeCalledOnce()->willReturn($handlerManager); + + $configKey = PH\MetadataProvider\Annotation::class; + $container->get('config')->shouldBeCalledOnce()->willReturn($config); + $config->offsetExists($configKey)->shouldBeCalledOnce()->willReturn(true); + $config->offsetGet($configKey)->shouldBeCalledOnce(); + + $service = $this->__invoke($container, ''); + $service->shouldBeAnInstanceOf(PH\MetadataProvider\Annotation::class); + } + + public function it_gets_configuration_from_custom_config_key( + ContainerInterface $container, PH\Handler\PluginManager $handlerManager, \ArrayAccess $config + ) + { + $container->get(PH\Handler\PluginManager::class)->shouldBeCalledOnce()->willReturn($handlerManager); + + $configKey = 'test_config_key'; + $container->get('config')->shouldBeCalledOnce()->willReturn($config); + $config->offsetExists($configKey)->shouldBeCalledOnce()->willReturn(true); + $config->offsetGet($configKey)->shouldBeCalledOnce(); + + $this->beConstructedWith($configKey); + $service = $this->__invoke($container, ''); + $service->shouldBeAnInstanceOf(PH\MetadataProvider\Annotation::class); + } + + public function it_constructs_itself_and_gets_configuration_from_custom_config_key( + ContainerInterface $container, PH\Handler\PluginManager $handlerManager, \ArrayAccess $config + ) + { + $container->get(PH\Handler\PluginManager::class)->shouldBeCalledOnce()->willReturn($handlerManager); + + $configKey = 'test_config_key'; + $container->get('config')->shouldBeCalledOnce()->willReturn($config); + $config->offsetExists($configKey)->shouldBeCalledOnce()->willReturn(true); + $config->offsetGet($configKey)->shouldBeCalledOnce(); + + $service = $this::__callStatic($configKey, [$container, '', null]); + $service->shouldBeAnInstanceOf(PH\MetadataProvider\Annotation::class); + } + + public function it_throws_on_too_few_arguments_during_self_construct(ContainerInterface $container) + { + $configKey = 'test_config_key'; + $error = new \InvalidArgumentException(\sprintf( + 'To invoke %s with custom configuration key statically 3 arguments are required: container, service name and options.', + PH\MetadataProvider\Factory\Annotation::class + )); + + $this::shouldThrow($error)->during('__callStatic', [$configKey, []]); + $this::shouldThrow($error)->during('__callStatic', [$configKey, [$container]]); + $this::shouldThrow($error)->during('__callStatic', [$configKey, [$container, '']]); + } + + public function it_creates_service_with_empty_configuration(ContainerInterface $container, PH\Handler\PluginManager $handlerManager) + { + $container->get(PH\Handler\PluginManager::class)->shouldBeCalledOnce()->willReturn($handlerManager); + + $container->get('config')->shouldBeCalledOnce()->willReturn([]); + + $service = $this->__invoke($container, ''); + $service->shouldBeAnInstanceOf(PH\MetadataProvider\Annotation::class); + $service->shouldHavePropertyOfType('cache', PH\Cache\DataFilePerKey::class); + } + + public function it_creates_service_with_cache_configuration(ContainerInterface $container, PH\Handler\PluginManager $handlerManager) + { + $container->get(PH\Handler\PluginManager::class)->shouldBeCalledOnce()->willReturn($handlerManager); + + $config = [ + PH\MetadataProvider\Annotation::class => ['cache' => ['directory' => 'data/cache']] + ]; + $container->get('config')->shouldBeCalledOnce()->willReturn($config); + + $service = $this->__invoke($container, ''); + $service->shouldBeAnInstanceOf(PH\MetadataProvider\Annotation::class); + $service->shouldHavePropertyOfType('cache', PH\Cache\DataFilePerKey::class); + } + + public function it_creates_service_with_cache_from_container( + ContainerInterface $container, PH\Handler\PluginManager $handlerManager, CacheInterface $cache + ) + { + $container->get(PH\Handler\PluginManager::class)->shouldBeCalledOnce()->willReturn($handlerManager); + + $cacheServiceName = 'test_cache_service'; + $config = [ + PH\MetadataProvider\Annotation::class => ['cache' => $cacheServiceName] + ]; + $container->get('config')->shouldBeCalledOnce()->willReturn($config); + $container->has($cacheServiceName)->shouldBeCalledOnce()->willReturn(true); + $container->get($cacheServiceName)->shouldBeCalledOnce()->willReturn($cache); + + $service = $this->__invoke($container, ''); + $service->shouldBeAnInstanceOf(PH\MetadataProvider\Annotation::class); + $service->shouldHaveProperty('cache', $cache); + } + + public function it_throws_on_invalid_cache_service_in_container( + ContainerInterface $container, PH\Handler\PluginManager $handlerManager, $cache + ) + { + $container->get(PH\Handler\PluginManager::class)->shouldBeCalledOnce()->willReturn($handlerManager); + + $cacheServiceName = 'test_cache_service'; + $config = [ + PH\MetadataProvider\Annotation::class => ['cache' => $cacheServiceName] + ]; + $container->get('config')->shouldBeCalledOnce()->willReturn($config); + $container->has($cacheServiceName)->shouldBeCalledOnce()->willReturn(true); + $container->get($cacheServiceName)->shouldBeCalledOnce()->willReturn($cache); + + $this->shouldThrow(\LogicException::class)->during('__invoke', [$container, '']); + } + + public function it_throws_on_invalid_cache_configuration(ContainerInterface $container, PH\Handler\PluginManager $handlerManager) + { + $container->get(PH\Handler\PluginManager::class)->shouldBeCalledOnce()->willReturn($handlerManager); + + $config = [ + PH\MetadataProvider\Annotation::class => ['cache' => new \stdClass()] + ]; + $container->get('config')->shouldBeCalledOnce()->willReturn($config); + $this->shouldThrow(\LogicException::class)->during('__invoke', [$container, '']); + } +} diff --git a/spec/Articus/PathHandler/MetadataProvider/Factory/PhpAttributeSpec.php b/spec/Articus/PathHandler/MetadataProvider/Factory/PhpAttributeSpec.php new file mode 100644 index 0000000..5f2e0d6 --- /dev/null +++ b/spec/Articus/PathHandler/MetadataProvider/Factory/PhpAttributeSpec.php @@ -0,0 +1,156 @@ +get(PH\Handler\PluginManager::class)->shouldBeCalledOnce()->willReturn($handlerManager); + + $configKey = PH\MetadataProvider\PhpAttribute::class; + $container->get('config')->shouldBeCalledOnce()->willReturn($config); + $config->offsetExists($configKey)->shouldBeCalledOnce()->willReturn(true); + $config->offsetGet($configKey)->shouldBeCalledOnce(); + + $service = $this->__invoke($container, ''); + $service->shouldBeAnInstanceOf(PH\MetadataProvider\PhpAttribute::class); + } + + public function it_gets_configuration_from_custom_config_key( + ContainerInterface $container, PH\Handler\PluginManager $handlerManager, \ArrayAccess $config + ) + { + $container->get(PH\Handler\PluginManager::class)->shouldBeCalledOnce()->willReturn($handlerManager); + + $configKey = 'test_config_key'; + $container->get('config')->shouldBeCalledOnce()->willReturn($config); + $config->offsetExists($configKey)->shouldBeCalledOnce()->willReturn(true); + $config->offsetGet($configKey)->shouldBeCalledOnce(); + + $this->beConstructedWith($configKey); + $service = $this->__invoke($container, ''); + $service->shouldBeAnInstanceOf(PH\MetadataProvider\PhpAttribute::class); + } + + public function it_constructs_itself_and_gets_configuration_from_custom_config_key( + ContainerInterface $container, PH\Handler\PluginManager $handlerManager, \ArrayAccess $config + ) + { + $container->get(PH\Handler\PluginManager::class)->shouldBeCalledOnce()->willReturn($handlerManager); + + $configKey = 'test_config_key'; + $container->get('config')->shouldBeCalledOnce()->willReturn($config); + $config->offsetExists($configKey)->shouldBeCalledOnce()->willReturn(true); + $config->offsetGet($configKey)->shouldBeCalledOnce(); + + $service = $this::__callStatic($configKey, [$container, '', null]); + $service->shouldBeAnInstanceOf(PH\MetadataProvider\PhpAttribute::class); + } + + public function it_throws_on_too_few_arguments_during_self_construct(ContainerInterface $container) + { + $configKey = 'test_config_key'; + $error = new \InvalidArgumentException(\sprintf( + 'To invoke %s with custom configuration key statically 3 arguments are required: container, service name and options.', + PH\MetadataProvider\Factory\PhpAttribute::class + )); + + $this::shouldThrow($error)->during('__callStatic', [$configKey, []]); + $this::shouldThrow($error)->during('__callStatic', [$configKey, [$container]]); + $this::shouldThrow($error)->during('__callStatic', [$configKey, [$container, '']]); + } + + public function it_creates_service_with_empty_configuration(ContainerInterface $container, PH\Handler\PluginManager $handlerManager) + { + $container->get(PH\Handler\PluginManager::class)->shouldBeCalledOnce()->willReturn($handlerManager); + + $container->get('config')->shouldBeCalledOnce()->willReturn([]); + + $service = $this->__invoke($container, ''); + $service->shouldBeAnInstanceOf(PH\MetadataProvider\PhpAttribute::class); + $service->shouldHavePropertyOfType('cache', PH\Cache\DataFilePerKey::class); + } + + public function it_creates_service_with_cache_configuration(ContainerInterface $container, PH\Handler\PluginManager $handlerManager) + { + $container->get(PH\Handler\PluginManager::class)->shouldBeCalledOnce()->willReturn($handlerManager); + + $config = [ + PH\MetadataProvider\Annotation::class => ['cache' => ['directory' => 'data/cache']] + ]; + $container->get('config')->shouldBeCalledOnce()->willReturn($config); + + $service = $this->__invoke($container, ''); + $service->shouldBeAnInstanceOf(PH\MetadataProvider\PhpAttribute::class); + $service->shouldHavePropertyOfType('cache', PH\Cache\DataFilePerKey::class); + } + + public function it_creates_service_with_cache_from_container( + ContainerInterface $container, PH\Handler\PluginManager $handlerManager, CacheInterface $cache + ) + { + $container->get(PH\Handler\PluginManager::class)->shouldBeCalledOnce()->willReturn($handlerManager); + + $cacheServiceName = 'test_cache_service'; + $config = [ + PH\MetadataProvider\PhpAttribute::class => ['cache' => $cacheServiceName] + ]; + $container->get('config')->shouldBeCalledOnce()->willReturn($config); + $container->has($cacheServiceName)->shouldBeCalledOnce()->willReturn(true); + $container->get($cacheServiceName)->shouldBeCalledOnce()->willReturn($cache); + + $service = $this->__invoke($container, ''); + $service->shouldBeAnInstanceOf(PH\MetadataProvider\PhpAttribute::class); + $service->shouldHaveProperty('cache', $cache); + } + + public function it_throws_on_invalid_cache_service_in_container( + ContainerInterface $container, PH\Handler\PluginManager $handlerManager, $cache + ) + { + $container->get(PH\Handler\PluginManager::class)->shouldBeCalledOnce()->willReturn($handlerManager); + + $cacheServiceName = 'test_cache_service'; + $config = [ + PH\MetadataProvider\PhpAttribute::class => ['cache' => $cacheServiceName] + ]; + $container->get('config')->shouldBeCalledOnce()->willReturn($config); + $container->has($cacheServiceName)->shouldBeCalledOnce()->willReturn(true); + $container->get($cacheServiceName)->shouldBeCalledOnce()->willReturn($cache); + + $this->shouldThrow(\LogicException::class)->during('__invoke', [$container, '']); + } + + public function it_throws_on_invalid_cache_configuration(ContainerInterface $container, PH\Handler\PluginManager $handlerManager) + { + $container->get(PH\Handler\PluginManager::class)->shouldBeCalledOnce()->willReturn($handlerManager); + + $config = [ + PH\MetadataProvider\PhpAttribute::class => ['cache' => new \stdClass()] + ]; + $container->get('config')->shouldBeCalledOnce()->willReturn($config); + + $this->shouldThrow(\LogicException::class)->during('__invoke', [$container, '']); + } +} diff --git a/spec/Articus/PathHandler/MetadataProvider/PhpAttributeSpec.php b/spec/Articus/PathHandler/MetadataProvider/PhpAttributeSpec.php new file mode 100644 index 0000000..57d40a3 --- /dev/null +++ b/spec/Articus/PathHandler/MetadataProvider/PhpAttributeSpec.php @@ -0,0 +1,907 @@ + $handlerClassName], + [], + [$handlerClassName => [$httpMethods[0] => 'test1', $httpMethods[1] => 'test2']], + [], + [], + [], + ]; + + $cache->get(PH\MetadataProvider\PhpAttribute::CACHE_KEY)->shouldBeCalledOnce()->willReturn($cacheData); + + $this->beConstructedWith($handlerManager, $cache); + $this->shouldImplement(PH\MetadataProviderInterface::class); + $this->getHttpMethods($handlerName)->shouldBe($httpMethods); + $this->__destruct(); + } + + public function it_returns_http_methods_for_handler_and_saves_them_to_cache_on_destruct_if_cache_is_empty( + PluginManagerInterface $handlerManager, + CacheInterface $cache + ) + { + $handlerName = 'test'; + $handlerClassName = Example\Handler\ValidHttpMethods::class; + $handler = new Example\Handler\ValidHttpMethods(); + $httpMethods = ['GET', 'HEAD', 'POST', 'PATCH', 'PUT', 'DELETE']; + $cacheChecker = function (array $cacheData) use ($handlerClassName, $httpMethods) + { + return ((!empty($cacheData[2][$handlerClassName])) + && \is_array($cacheData[2][$handlerClassName]) + && (\array_keys($cacheData[2][$handlerClassName]) == $httpMethods) + ); + }; + + $handlerManager->get($handlerName)->shouldBeCalledOnce()->willReturn($handler); + $cache->get(PH\MetadataProvider\PhpAttribute::CACHE_KEY)->shouldBeCalledOnce()->willReturn(null); + $cache->set(PH\MetadataProvider\PhpAttribute::CACHE_KEY, Argument::that($cacheChecker))->shouldBeCalledOnce(); + + $this->beConstructedWith($handlerManager, $cache); + $this->shouldImplement(PH\MetadataProviderInterface::class); + $this->getHttpMethods($handlerName)->shouldBe($httpMethods); + $this->__destruct(); + } + + public function it_throws_on_http_methods_return_for_invalid_handler( + PluginManagerInterface $handlerManager, + CacheInterface $cache + ) + { + $handlerName = 'test'; + + $handlerManager->get($handlerName)->shouldBeCalledOnce()->willReturn(null); + $cache->get(PH\MetadataProvider\PhpAttribute::CACHE_KEY)->shouldBeCalledOnce()->willReturn(null); + + $this->beConstructedWith($handlerManager, $cache); + $this->shouldImplement(PH\MetadataProviderInterface::class); + $this->shouldThrow(\InvalidArgumentException::class)->during('getHttpMethods', [$handlerName]); + $this->__destruct(); + } + + public function it_throws_on_http_methods_return_for_handler_with_several_methods_handling_same_http_method( + PluginManagerInterface $handlerManager, + CacheInterface $cache + ) + { + $handlerName = 'test'; + $handler = new Example\Handler\SeveralMethodsForSingleHttpMethod(); + + $handlerManager->get($handlerName)->shouldBeCalledOnce()->willReturn($handler); + $cache->get(PH\MetadataProvider\PhpAttribute::CACHE_KEY)->shouldBeCalledOnce()->willReturn(null); + + $this->beConstructedWith($handlerManager, $cache); + $this->shouldImplement(PH\MetadataProviderInterface::class); + $this->shouldThrow(\LogicException::class)->during('getHttpMethods', [$handlerName]); + $this->__destruct(); + } + + public function it_throws_on_http_methods_return_for_handler_without_methods_handling_http_methods( + PluginManagerInterface $handlerManager, + CacheInterface $cache + ) + { + $handlerName = 'test'; + $handler = new Example\Handler\NoMethodsHandlingHttpMethods(); + + $handlerManager->get($handlerName)->shouldBeCalledOnce()->willReturn($handler); + $cache->get(PH\MetadataProvider\PhpAttribute::CACHE_KEY)->shouldBeCalledOnce()->willReturn(null); + + $this->beConstructedWith($handlerManager, $cache); + $this->shouldImplement(PH\MetadataProviderInterface::class); + $this->shouldThrow(\LogicException::class)->during('getHttpMethods', [$handlerName]); + $this->__destruct(); + } + + public function it_returns_cached_routes_for_handler_if_cache_exists( + PluginManagerInterface $handlerManager, + CacheInterface $cache + ) + { + $handlerName = 'test'; + $handlerClassName = 'test_class'; + $routes = [ + ['test_1', '/test_1', ['test_1' => 123]], + ['test_2', '/test_2', ['test_2' => 123]], + ['test_3', '/test_3', ['test_3' => 123]], + ]; + $cacheData = [ + [$handlerName => $handlerClassName], + [$handlerClassName => $routes], + [$handlerClassName => []], + [$handlerClassName => []], + [$handlerClassName => []], + [$handlerClassName => []], + ]; + + $cache->get(PH\MetadataProvider\PhpAttribute::CACHE_KEY)->shouldBeCalledOnce()->willReturn($cacheData); + + $this->beConstructedWith($handlerManager, $cache); + $this->shouldImplement(PH\MetadataProviderInterface::class); + $this->getRoutes($handlerName)->shouldIterateAs($routes); + $this->__destruct(); + } + + public function it_returns_routes_for_handler_and_saves_them_to_cache_on_destruct_if_cache_is_empty( + PluginManagerInterface $handlerManager, + CacheInterface $cache + ) + { + $handlerName = 'test'; + $handlerClassName = Example\Handler\ValidRoutes::class; + $handler = new Example\Handler\ValidRoutes(); + $routes = [ + [null, '/1', []], + [null, '/2', ['test_2' => 123]], + ['test_3', '/3', []], + ['test_4', '/4', ['test_4' => 123]], + [null, '/5', []], + ['test_6', '/6', []], + [null, '/7', ['test_7' => 123]], + ['test_8', '/8', ['test_8' => 123]], + ]; + $cacheChecker = function (array $cacheData) use ($handlerClassName, $routes) + { + return ($cacheData[1][$handlerClassName] == $routes); + }; + + $handlerManager->get($handlerName)->shouldBeCalledOnce()->willReturn($handler); + $cache->get(PH\MetadataProvider\PhpAttribute::CACHE_KEY)->shouldBeCalledOnce()->willReturn(null); + $cache->set(PH\MetadataProvider\PhpAttribute::CACHE_KEY, Argument::that($cacheChecker))->shouldBeCalledOnce(); + + $this->beConstructedWith($handlerManager, $cache); + $this->shouldImplement(PH\MetadataProviderInterface::class); + $this->getRoutes($handlerName)->shouldIterateAs($routes); + $this->__destruct(); + } + + public function it_throws_on_routes_return_for_invalid_handler( + PluginManagerInterface $handlerManager, + CacheInterface $cache + ) + { + $handlerName = 'test'; + + $handlerManager->get($handlerName)->shouldBeCalledOnce()->willReturn(null); + $cache->get(PH\MetadataProvider\PhpAttribute::CACHE_KEY)->shouldBeCalledOnce()->willReturn(null); + + $this->beConstructedWith($handlerManager, $cache); + $this->shouldImplement(PH\MetadataProviderInterface::class); + $this->getRoutes($handlerName)->shouldThrow(\InvalidArgumentException::class)->during('current', []); + $this->__destruct(); + } + + public function it_throws_on_routes_return_for_handler_without_routes( + PluginManagerInterface $handlerManager, + CacheInterface $cache + ) + { + $handlerName = 'test'; + $handler = new Example\Handler\NoRoutes(); + + $handlerManager->get($handlerName)->shouldBeCalledOnce()->willReturn($handler); + $cache->get(PH\MetadataProvider\PhpAttribute::CACHE_KEY)->shouldBeCalledOnce()->willReturn(null); + + $this->beConstructedWith($handlerManager, $cache); + $this->shouldImplement(PH\MetadataProviderInterface::class); + $this->getRoutes($handlerName)->shouldThrow(\LogicException::class)->during('current', []); + $this->__destruct(); + } + + public function it_checks_and_returns_cached_consumers_for_handler_method_if_cache_exists( + PluginManagerInterface $handlerManager, + CacheInterface $cache + ) + { + $handlerName = ['test_1', 'test_2']; + $httpMethod = ['TEST1', 'TEST2']; + $handlerClassName = ['test_class_1', 'test_class_2']; + $handlerMethod = ['test_method_1', 'test_method_2']; + $consumers = [ + ['test_1/mime', 'test_1', ['test_1' => 123]], + ['test_2/mime', 'test_2', ['test_2' => 123]], + ['test_3/mime', 'test_3', ['test_3' => 123]], + ]; + $cacheData = [ + [ + $handlerName[0] => $handlerClassName[0], + $handlerName[1] => $handlerClassName[1], + ], + [ + $handlerClassName[0] => [], + $handlerClassName[1] => [], + ], + [ + $handlerClassName[0] => [$httpMethod[0] => $handlerMethod[0]], + $handlerClassName[1] => [$httpMethod[1] => $handlerMethod[1]], + ], + [ + $handlerClassName[0] => [$handlerMethod[0] => $consumers], + $handlerClassName[1] => [$handlerMethod[1] => []], + ], + [ + $handlerClassName[0] => [], + $handlerClassName[1] => [], + ], + [ + $handlerClassName[0] => [], + $handlerClassName[1] => [], + ], + ]; + + $cache->get(PH\MetadataProvider\PhpAttribute::CACHE_KEY)->shouldBeCalledOnce()->willReturn($cacheData); + + $this->beConstructedWith($handlerManager, $cache); + $this->shouldImplement(PH\MetadataProviderInterface::class); + $this->hasConsumers($handlerName[0], $httpMethod[0])->shouldBe(true); + $this->getConsumers($handlerName[0], $httpMethod[0])->shouldIterateAs($consumers); + $this->hasConsumers($handlerName[1], $httpMethod[1])->shouldBe(false); + $this->getConsumers($handlerName[1], $httpMethod[1])->shouldIterateAs([]); + $this->__destruct(); + } + + public function it_checks_and_returns_consumers_for_handler_method_and_saves_them_to_cache_on_destruct_if_cache_is_empty( + PluginManagerInterface $handlerManager, + CacheInterface $cache + ) + { + $handlerNames = ['consumers', 'common_consumers']; + $handlerClassNames = [Example\Handler\ValidConsumers::class, Example\Handler\ValidCommonConsumers::class]; + $handlers = [new Example\Handler\ValidConsumers(), new Example\Handler\ValidCommonConsumers()]; + $httpMethods = ['NO_CONSUMERS', 'SEVERAL_CONSUMERS']; + $handlerMethods = ['noConsumers', 'severalConsumers']; + $consumers = [ + $handlerClassNames[0] => [ + $handlerMethods[0] => [], + $handlerMethods[1] => [ + ['*/*', 'test_1', null], + ['*/*', 'test_2', ['test_2' => 123]], + ['test/3', 'test_3', null], + ['test/4', 'test_4', ['test_4' => 123]], + ['*/*', 'test_5', null], + ['test/6', 'test_6', null], + ['*/*', 'test_7', ['test_7' => 123]], + ['test/8', 'test_8', ['test_8' => 123]], + ], + ], + $handlerClassNames[1] => [ + $handlerMethods[0] => [ + ['*/*', 'test_c1', null], + ['*/*', 'test_c2', ['test_c2' => 123]], + ['test/c3', 'test_c3', null], + ['test/c4', 'test_c4', ['test_c4' => 123]], + ['*/*', 'test_c5', null], + ['test/c6', 'test_c6', null], + ['*/*', 'test_c7', ['test_c7' => 123]], + ['test/c8', 'test_c8', ['test_c8' => 123]], + ], + $handlerMethods[1] => [ + ['*/*', 'test_c1', null], + ['*/*', 'test_c2', ['test_c2' => 123]], + ['*/*', 'test_1', null], + ['*/*', 'test_2', ['test_2' => 123]], + ['test/c3', 'test_c3', null], + ['test/c4', 'test_c4', ['test_c4' => 123]], + ['test/3', 'test_3', null], + ['test/4', 'test_4', ['test_4' => 123]], + ['*/*', 'test_c5', null], + ['test/c6', 'test_c6', null], + ['*/*', 'test_c7', ['test_c7' => 123]], + ['test/c8', 'test_c8', ['test_c8' => 123]], + ['*/*', 'test_5', null], + ['test/6', 'test_6', null], + ['*/*', 'test_7', ['test_7' => 123]], + ['test/8', 'test_8', ['test_8' => 123]], + ], + ], + ]; + $cacheChecker = function (array $cacheData) use ($handlerClassNames, $handlerMethods, $consumers) + { + $result = true; + foreach ([0, 1] as $i) + { + foreach ([0, 1] as $j) + { + $result = ($result + && ($cacheData[3][$handlerClassNames[$i]][$handlerMethods[$j]] == $consumers[$handlerClassNames[$i]][$handlerMethods[$j]]) + ); + } + } + return $result; + }; + + $cache->get(PH\MetadataProvider\PhpAttribute::CACHE_KEY)->shouldBeCalledOnce()->willReturn(null); + $cache->set(PH\MetadataProvider\PhpAttribute::CACHE_KEY, Argument::that($cacheChecker))->shouldBeCalledOnce(); + + $this->beConstructedWith($handlerManager, $cache); + $this->shouldImplement(PH\MetadataProviderInterface::class); + + foreach ([0, 1] as $i) + { + $handlerManager->get($handlerNames[$i])->shouldBeCalledOnce()->willReturn($handlers[$i]); + + foreach ([0, 1] as $j) + { + $this->hasConsumers($handlerNames[$i], $httpMethods[$j])->shouldBe(!empty($consumers[$handlerClassNames[$i]][$handlerMethods[$j]])); + $this->getConsumers($handlerNames[$i], $httpMethods[$j])->shouldIterateAs($consumers[$handlerClassNames[$i]][$handlerMethods[$j]]); + } + } + + $this->__destruct(); + } + + public function it_throws_on_consumers_check_and_return_for_invalid_handler( + PluginManagerInterface $handlerManager, + CacheInterface $cache + ) + { + $handlerName = 'test'; + $httpMethod = 'TEST'; + + $handlerManager->get($handlerName)->shouldBeCalledTimes(2)->willReturn(null); + $cache->get(PH\MetadataProvider\PhpAttribute::CACHE_KEY)->shouldBeCalledOnce()->willReturn(null); + + $this->beConstructedWith($handlerManager, $cache); + $this->shouldImplement(PH\MetadataProviderInterface::class); + $this->shouldThrow(\InvalidArgumentException::class)->during('hasConsumers', [$handlerName, $httpMethod]); + $this->getConsumers($handlerName, $httpMethod)->shouldThrow(\InvalidArgumentException::class)->during('current', []); + $this->__destruct(); + } + + public function it_throws_on_consumers_check_and_return_for_invalid_handler_method( + PluginManagerInterface $handlerManager, + CacheInterface $cache + ) + { + $handlerName = 'test'; + $handler = new Example\Handler\ValidConsumers(); + $httpMethod = 'UNKNOWN'; + + $handlerManager->get($handlerName)->shouldBeCalledOnce()->willReturn($handler); + $cache->get(PH\MetadataProvider\PhpAttribute::CACHE_KEY)->shouldBeCalledOnce()->willReturn(null); + $cache->set(PH\MetadataProvider\PhpAttribute::CACHE_KEY, Argument::any())->shouldBeCalledOnce(); + + $this->beConstructedWith($handlerManager, $cache); + $this->shouldImplement(PH\MetadataProviderInterface::class); + $this->shouldThrow(\InvalidArgumentException::class)->during('hasConsumers', [$handlerName, $httpMethod]); + $this->getConsumers($handlerName, $httpMethod)->shouldThrow(\InvalidArgumentException::class)->during('current', []); + + $this->__destruct(); + } + + + public function it_returns_cached_attributes_for_handler_method_if_cache_exists( + PluginManagerInterface $handlerManager, + CacheInterface $cache + ) + { + $handlerName = ['test_1', 'test_2']; + $httpMethod = ['TEST1', 'TEST2']; + $handlerClassName = ['test_class_1', 'test_class_2']; + $handlerMethod = ['test_method_1', 'test_method_2']; + $attributes = [ + ['test_1', ['test_1' => 123]], + ['test_2', ['test_2' => 123]], + ['test_3', ['test_3' => 123]], + ]; + $cacheData = [ + [ + $handlerName[0] => $handlerClassName[0], + $handlerName[1] => $handlerClassName[1], + ], + [ + $handlerClassName[0] => [], + $handlerClassName[1] => [], + ], + [ + $handlerClassName[0] => [$httpMethod[0] => $handlerMethod[0]], + $handlerClassName[1] => [$httpMethod[1] => $handlerMethod[1]], + ], + [ + $handlerClassName[0] => [], + $handlerClassName[1] => [], + ], + [ + $handlerClassName[0] => [$handlerMethod[0] => $attributes], + $handlerClassName[1] => [$handlerMethod[1] => []], + ], + [ + $handlerClassName[0] => [], + $handlerClassName[1] => [], + ], + ]; + + $cache->get(PH\MetadataProvider\PhpAttribute::CACHE_KEY)->shouldBeCalledOnce()->willReturn($cacheData); + + $this->beConstructedWith($handlerManager, $cache); + $this->shouldImplement(PH\MetadataProviderInterface::class); + $this->getAttributes($handlerName[0], $httpMethod[0])->shouldIterateAs($attributes); + $this->getAttributes($handlerName[1], $httpMethod[1])->shouldIterateAs([]); + $this->__destruct(); + } + + public function it_returns_attributes_for_handler_method_and_saves_them_to_cache_if_cache_is_empty( + PluginManagerInterface $handlerManager, + CacheInterface $cache + ) + { + $handlerNames = ['attributes', 'common_attributes']; + $handlerClassNames = [Example\Handler\ValidAttributes::class, Example\Handler\ValidCommonAttributes::class]; + $handlers = [new Example\Handler\ValidAttributes(), new Example\Handler\ValidCommonAttributes()]; + $httpMethods = ['NO_ATTRIBUTES', 'SEVERAL_ATTRIBUTES']; + $handlerMethods = ['noAttributes', 'severalAttributes']; + $attributes = [ + $handlerClassNames[0] => [ + $handlerMethods[0] => [], + $handlerMethods[1] => [ + ['test_1', null], + ['test_2', ['test_2' => 123]], + ['test_3', null], + ['test_4', ['test_4' => 123]], + ['test_5', null], + ['test_6', null], + ['test_7', ['test_7' => 123]], + ], + ], + $handlerClassNames[1] => [ + $handlerMethods[0] => [ + ['test_c1', null], + ['test_c2', ['test_c2' => 123]], + ['test_c3', null], + ['test_c4', ['test_c4' => 123]], + ['test_c5', null], + ['test_c6', null], + ['test_c7', ['test_c7' => 123]], + ], + $handlerMethods[1] => [ + ['test_c1', null], + ['test_c2', ['test_c2' => 123]], + ['test_1', null], + ['test_2', ['test_2' => 123]], + ['test_c3', null], + ['test_c4', ['test_c4' => 123]], + ['test_3', null], + ['test_4', ['test_4' => 123]], + ['test_c5', null], + ['test_c6', null], + ['test_c7', ['test_c7' => 123]], + ['test_5', null], + ['test_6', null], + ['test_7', ['test_7' => 123]], + ], + ], + ]; + $cacheChecker = function (array $cacheData) use ($handlerClassNames, $handlerMethods, $attributes) + { + $result = true; + foreach ([0, 1] as $i) + { + foreach ([0, 1] as $j) + { + $result = ($result + && ($cacheData[4][$handlerClassNames[$i]][$handlerMethods[$j]] == $attributes[$handlerClassNames[$i]][$handlerMethods[$j]]) + ); + } + } + return $result; + }; + + $cache->get(PH\MetadataProvider\PhpAttribute::CACHE_KEY)->shouldBeCalledOnce()->willReturn(null); + $cache->set(PH\MetadataProvider\PhpAttribute::CACHE_KEY, Argument::that($cacheChecker))->shouldBeCalledOnce(); + + $this->beConstructedWith($handlerManager, $cache); + $this->shouldImplement(PH\MetadataProviderInterface::class); + + foreach ([0, 1] as $i) + { + $handlerManager->get($handlerNames[$i])->shouldBeCalledOnce()->willReturn($handlers[$i]); + + foreach ([0, 1] as $j) + { + $this->getAttributes($handlerNames[$i], $httpMethods[$j])->shouldIterateAs($attributes[$handlerClassNames[$i]][$handlerMethods[$j]]); + } + } + + $this->__destruct(); + } + + public function it_throws_on_attributes_return_for_invalid_handler( + PluginManagerInterface $handlerManager, + CacheInterface $cache + ) + { + $handlerName = 'test'; + $httpMethod = 'TEST'; + + $handlerManager->get($handlerName)->shouldBeCalledOnce()->willReturn(null); + $cache->get(PH\MetadataProvider\PhpAttribute::CACHE_KEY)->shouldBeCalledOnce()->willReturn(null); + + $this->beConstructedWith($handlerManager, $cache); + $this->shouldImplement(PH\MetadataProviderInterface::class); + $this->getAttributes($handlerName, $httpMethod)->shouldThrow(\InvalidArgumentException::class)->during('current', []); + $this->__destruct(); + } + + public function it_throws_on_attributes_return_for_invalid_handler_method( + PluginManagerInterface $handlerManager, + CacheInterface $cache + ) + { + $handlerName = 'test'; + $handler = new Example\Handler\ValidAttributes(); + $httpMethod = 'UNKNOWN'; + + $handlerManager->get($handlerName)->shouldBeCalledOnce()->willReturn($handler); + $cache->get(PH\MetadataProvider\PhpAttribute::CACHE_KEY)->shouldBeCalledOnce()->willReturn(null); + $cache->set(PH\MetadataProvider\PhpAttribute::CACHE_KEY, Argument::any())->shouldBeCalledOnce(); + + $this->beConstructedWith($handlerManager, $cache); + $this->shouldImplement(PH\MetadataProviderInterface::class); + $this->getAttributes($handlerName, $httpMethod)->shouldThrow(\InvalidArgumentException::class)->during('current', []); + + $this->__destruct(); + } + + + public function it_checks_and_returns_cached_producers_for_handler_method_if_cache_exists( + PluginManagerInterface $handlerManager, + CacheInterface $cache + ) + { + $handlerName = ['test_1', 'test_2']; + $httpMethod = ['TEST1', 'TEST2']; + $handlerClassName = ['test_class_1', 'test_class_2']; + $handlerMethod = ['test_method_1', 'test_method_2']; + $producers = [ + ['test_1/mime', 'test_1', ['test_1' => 123]], + ['test_2/mime', 'test_2', ['test_2' => 123]], + ['test_3/mime', 'test_3', ['test_3' => 123]], + ]; + $cacheData = [ + [ + $handlerName[0] => $handlerClassName[0], + $handlerName[1] => $handlerClassName[1], + ], + [ + $handlerClassName[0] => [], + $handlerClassName[1] => [], + ], + [ + $handlerClassName[0] => [$httpMethod[0] => $handlerMethod[0]], + $handlerClassName[1] => [$httpMethod[1] => $handlerMethod[1]], + ], + [ + $handlerClassName[0] => [], + $handlerClassName[1] => [], + ], + [ + $handlerClassName[0] => [], + $handlerClassName[1] => [], + ], + [ + $handlerClassName[0] => [$handlerMethod[0] => $producers], + $handlerClassName[1] => [$handlerMethod[1] => []], + ], + ]; + + $cache->get(PH\MetadataProvider\PhpAttribute::CACHE_KEY)->shouldBeCalledOnce()->willReturn($cacheData); + + $this->beConstructedWith($handlerManager, $cache); + $this->shouldImplement(PH\MetadataProviderInterface::class); + $this->hasProducers($handlerName[0], $httpMethod[0])->shouldBe(true); + $this->getProducers($handlerName[0], $httpMethod[0])->shouldIterateAs($producers); + $this->hasProducers($handlerName[1], $httpMethod[1])->shouldBe(false); + $this->getProducers($handlerName[1], $httpMethod[1])->shouldIterateAs([]); + $this->__destruct(); + } + + public function it_checks_and_returns_producers_for_handler_method_and_saves_them_to_cache_on_destruct_if_cache_is_empty( + PluginManagerInterface $handlerManager, + CacheInterface $cache + ) + { + $handlerNames = ['producers', 'common_producers']; + $handlerClassNames = [Example\Handler\ValidProducers::class, Example\Handler\ValidCommonProducers::class]; + $handlers = [new Example\Handler\ValidProducers(), new Example\Handler\ValidCommonProducers()]; + $httpMethods = ['NO_PRODUCERS', 'SEVERAL_PRODUCERS']; + $handlerMethods = ['noProducers', 'severalProducers']; + $producers = [ + $handlerClassNames[0] => [ + $handlerMethods[0] => [], + $handlerMethods[1] => [ + ['test/1', 'test_1', null], + ['test/2', 'test_2', ['test_2' => 123]], + ['test/3', 'test_3', null], + ['test/4', 'test_4', ['test_4' => 123]], + ['test/5', 'test_5', null], + ['test/6', 'test_6', null], + ['test/7', 'test_7', ['test_7' => 123]], + ], + ], + $handlerClassNames[1] => [ + $handlerMethods[0] => [ + ['test/c1', 'test_c1', null], + ['test/c2', 'test_c2', ['test_c2' => 123]], + ['test/c3', 'test_c3', null], + ['test/c4', 'test_c4', ['test_c4' => 123]], + ['test/c5', 'test_c5', null], + ['test/c6', 'test_c6', null], + ['test/c7', 'test_c7', ['test_c7' => 123]], + ], + $handlerMethods[1] => [ + ['test/c1', 'test_c1', null], + ['test/c2', 'test_c2', ['test_c2' => 123]], + ['test/1', 'test_1', null], + ['test/2', 'test_2', ['test_2' => 123]], + ['test/c3', 'test_c3', null], + ['test/c4', 'test_c4', ['test_c4' => 123]], + ['test/3', 'test_3', null], + ['test/4', 'test_4', ['test_4' => 123]], + ['test/c5', 'test_c5', null], + ['test/c6', 'test_c6', null], + ['test/c7', 'test_c7', ['test_c7' => 123]], + ['test/5', 'test_5', null], + ['test/6', 'test_6', null], + ['test/7', 'test_7', ['test_7' => 123]], + ], + ], + ]; + $cacheChecker = function (array $cacheData) use ($handlerClassNames, $handlerMethods, $producers) + { + $result = true; + foreach ([0, 1] as $i) + { + foreach ([0, 1] as $j) + { + $result = ($result + && ($cacheData[5][$handlerClassNames[$i]][$handlerMethods[$j]] == $producers[$handlerClassNames[$i]][$handlerMethods[$j]]) + ); + } + } + return $result; + }; + + $cache->get(PH\MetadataProvider\PhpAttribute::CACHE_KEY)->shouldBeCalledOnce()->willReturn(null); + $cache->set(PH\MetadataProvider\PhpAttribute::CACHE_KEY, Argument::that($cacheChecker))->shouldBeCalledOnce(); + + $this->beConstructedWith($handlerManager, $cache); + $this->shouldImplement(PH\MetadataProviderInterface::class); + + foreach ([0, 1] as $i) + { + $handlerManager->get($handlerNames[$i])->shouldBeCalledOnce()->willReturn($handlers[$i]); + + foreach ([0, 1] as $j) + { + $this->hasProducers($handlerNames[$i], $httpMethods[$j])->shouldBe(!empty($producers[$handlerClassNames[$i]][$handlerMethods[$j]])); + $this->getProducers($handlerNames[$i], $httpMethods[$j])->shouldIterateAs($producers[$handlerClassNames[$i]][$handlerMethods[$j]]); + } + } + + $this->__destruct(); + } + + public function it_throws_on_producers_check_and_return_for_invalid_handler( + PluginManagerInterface $handlerManager, + CacheInterface $cache + ) + { + $handlerName = 'test'; + $httpMethod = 'TEST'; + + $handlerManager->get($handlerName)->shouldBeCalledTimes(2)->willReturn(null); + $cache->get(PH\MetadataProvider\PhpAttribute::CACHE_KEY)->shouldBeCalledOnce()->willReturn(null); + + $this->beConstructedWith($handlerManager, $cache); + $this->shouldImplement(PH\MetadataProviderInterface::class); + $this->shouldThrow(\InvalidArgumentException::class)->during('hasProducers', [$handlerName, $httpMethod]); + $this->getProducers($handlerName, $httpMethod)->shouldThrow(\InvalidArgumentException::class)->during('current', []); + $this->__destruct(); + } + + public function it_throws_on_producers_check_and_return_for_invalid_handler_method( + PluginManagerInterface $handlerManager, + CacheInterface $cache + ) + { + $handlerName = 'test'; + $handler = new Example\Handler\ValidProducers(); + $httpMethod = 'UNKNOWN'; + + $handlerManager->get($handlerName)->shouldBeCalledOnce()->willReturn($handler); + $cache->get(PH\MetadataProvider\PhpAttribute::CACHE_KEY)->shouldBeCalledOnce()->willReturn(null); + $cache->set(PH\MetadataProvider\PhpAttribute::CACHE_KEY, Argument::any())->shouldBeCalledOnce(); + + $this->beConstructedWith($handlerManager, $cache); + $this->shouldImplement(PH\MetadataProviderInterface::class); + $this->shouldThrow(\InvalidArgumentException::class)->during('hasProducers', [$handlerName, $httpMethod]); + $this->getProducers($handlerName, $httpMethod)->shouldThrow(\InvalidArgumentException::class)->during('current', []); + + $this->__destruct(); + } + + + public function it_executes_cached_handler_method_if_cache_exists( + PluginManagerInterface $handlerManager, + CacheInterface $cache, + Example\Handler\ValidMethod $handlerObject, + Request $request, + $handlerData + ) + { + $handlerName = 'test'; + $httpMethod = 'TEST'; + $handlerClassName = Example\Handler\ValidMethod::class; + $handlerMethod = 'testMethod'; + $handlerObject->testMethod($request)->shouldBeCalledOnce()->willReturn($handlerData); + + $cacheData = [ + [$handlerName => $handlerClassName], + [$handlerClassName => []], + [$handlerClassName => [$httpMethod => $handlerMethod]], + [$handlerClassName => []], + [$handlerClassName => []], + [$handlerClassName => []], + ]; + + $cache->get(PH\MetadataProvider\PhpAttribute::CACHE_KEY)->shouldBeCalledOnce()->willReturn($cacheData); + + $this->beConstructedWith($handlerManager, $cache); + $this->shouldImplement(PH\MetadataProviderInterface::class); + $this->executeHandlerMethod($handlerName, $httpMethod, $handlerObject, $request)->shouldBe($handlerData); + $this->__destruct(); + } + + public function it_executes_handler_method_and_saves_metadata_to_cache_on_destruct_if_cache_is_empty( + PluginManagerInterface $handlerManager, + CacheInterface $cache, + Example\Handler\ValidMethod $handlerObject, + Request $request, + $handlerData + ) + { + $handlerName = 'test'; + $httpMethod = 'TEST'; + $handlerClassName = Example\Handler\ValidMethod::class; + $handler = new Example\Handler\ValidMethod(); + $handlerMethod = 'testMethod'; + $handlerObject->testMethod($request)->shouldBeCalledOnce()->willReturn($handlerData); + + $cacheChecker = function(array $cacheData) use ($handlerName, $handlerClassName, $httpMethod, $handlerMethod) + { + return (($cacheData[0] === [$handlerName => $handlerClassName]) + && ($cacheData[2] === [$handlerClassName => [$httpMethod => $handlerMethod]]) + ); + }; + + $handlerManager->get($handlerName)->shouldBeCalledOnce()->willReturn($handler); + + $cache->get(PH\MetadataProvider\PhpAttribute::CACHE_KEY)->shouldBeCalledOnce()->willReturn(null); + $cache->set(PH\MetadataProvider\PhpAttribute::CACHE_KEY, Argument::that($cacheChecker))->shouldBeCalledOnce(); + + $this->beConstructedWith($handlerManager, $cache); + $this->shouldImplement(PH\MetadataProviderInterface::class); + $this->executeHandlerMethod($handlerName, $httpMethod, $handlerObject, $request)->shouldBe($handlerData); + $this->__destruct(); + } + + public function it_throws_on_handler_method_execute_for_invalid_handler( + PluginManagerInterface $handlerManager, + CacheInterface $cache, + Example\Handler\ValidMethod $handlerObject, + Request $request + ) + { + $handlerName = 'test'; + $httpMethod = 'TEST'; + + $handlerManager->get($handlerName)->shouldBeCalledOnce()->willReturn(null); + $cache->get(PH\MetadataProvider\PhpAttribute::CACHE_KEY)->shouldBeCalledOnce()->willReturn(null); + + $this->beConstructedWith($handlerManager, $cache); + $this->shouldImplement(PH\MetadataProviderInterface::class); + $this->shouldThrow(\InvalidArgumentException::class) + ->during('executeHandlerMethod', [$handlerName, $httpMethod, $handlerObject, $request]) + ; + $this->__destruct(); + } + + public function it_throws_on_handler_method_execute_for_invalid_handler_method( + PluginManagerInterface $handlerManager, + CacheInterface $cache, + Example\Handler\ValidMethod $handlerObject, + Request $request + ) + { + $handlerName = 'test'; + $httpMethod = 'UNKNOWN'; + $handler = new Example\Handler\ValidMethod(); + + $handlerManager->get($handlerName)->shouldBeCalledOnce()->willReturn($handler); + $cache->get(PH\MetadataProvider\PhpAttribute::CACHE_KEY)->shouldBeCalledOnce()->willReturn(null); + $cache->set(PH\MetadataProvider\PhpAttribute::CACHE_KEY, Argument::any())->shouldBeCalledOnce(); + + $this->beConstructedWith($handlerManager, $cache); + $this->shouldImplement(PH\MetadataProviderInterface::class); + $this->shouldThrow(\InvalidArgumentException::class) + ->during('executeHandlerMethod', [$handlerName, $httpMethod, $handlerObject, $request]) + ; + $this->__destruct(); + } + + public function it_throws_on_handler_method_execute_for_handler_method_having_several_required_parameters( + PluginManagerInterface $handlerManager, + CacheInterface $cache, + Example\Handler\ValidMethod $handlerObject, + Request $request + ) + { + $handlerName = 'test'; + $httpMethod = 'TEST'; + $handler = new Example\Handler\SeveralRequiredParametersMethod(); + + $handlerManager->get($handlerName)->shouldBeCalledOnce()->willReturn($handler); + $cache->get(PH\MetadataProvider\PhpAttribute::CACHE_KEY)->shouldBeCalledOnce()->willReturn(null); + + $this->beConstructedWith($handlerManager, $cache); + $this->shouldImplement(PH\MetadataProviderInterface::class); + $this->shouldThrow(\LogicException::class) + ->during('executeHandlerMethod', [$handlerName, $httpMethod, $handlerObject, $request]) + ; + $this->__destruct(); + } + + public function it_throws_on_handler_method_execute_for_invalid_handler_object( + PluginManagerInterface $handlerManager, + CacheInterface $cache, + $handlerObject, + Request $request + ) + { + $handlerName = 'test'; + $httpMethod = 'TEST'; + $handler = new Example\Handler\ValidMethod(); + + $handlerManager->get($handlerName)->shouldBeCalledOnce()->willReturn($handler); + $cache->get(PH\MetadataProvider\PhpAttribute::CACHE_KEY)->shouldBeCalledOnce()->willReturn(null); + $cache->set(PH\MetadataProvider\PhpAttribute::CACHE_KEY, Argument::any())->shouldBeCalledOnce(); + + $this->beConstructedWith($handlerManager, $cache); + $this->shouldImplement(PH\MetadataProviderInterface::class); + $this->shouldThrow(\InvalidArgumentException::class) + ->during('executeHandlerMethod', [$handlerName, $httpMethod, $handlerObject, $request]) + ; + $this->__destruct(); + } +} diff --git a/spec/Articus/PathHandler/Producer/Factory/PluginManagerSpec.php b/spec/Articus/PathHandler/Producer/Factory/PluginManagerSpec.php new file mode 100644 index 0000000..841273a --- /dev/null +++ b/spec/Articus/PathHandler/Producer/Factory/PluginManagerSpec.php @@ -0,0 +1,61 @@ +get('config')->shouldBeCalledOnce()->willReturn($config); + $config->offsetExists($configKey)->shouldBeCalledOnce()->willReturn(true); + $config->offsetGet($configKey)->shouldBeCalledOnce(); + + $service = $this->__invoke($container, ''); + $service->shouldBeAnInstanceOf(PH\Producer\PluginManager::class); + } + + public function it_gets_configuration_from_custom_config_key(ContainerInterface $container, \ArrayAccess $config) + { + $configKey = 'test_config_key'; + $container->get('config')->shouldBeCalledOnce()->willReturn($config); + $config->offsetExists($configKey)->shouldBeCalledOnce()->willReturn(true); + $config->offsetGet($configKey)->shouldBeCalledOnce(); + + $this->beConstructedWith($configKey); + $service = $this->__invoke($container, ''); + $service->shouldBeAnInstanceOf(PH\Producer\PluginManager::class); + } + + public function it_constructs_itself_and_gets_configuration_from_custom_config_key(ContainerInterface $container, \ArrayAccess $config) + { + $configKey = 'test_config_key'; + $container->get('config')->shouldBeCalledOnce()->willReturn($config); + $config->offsetExists($configKey)->shouldBeCalledOnce()->willReturn(true); + $config->offsetGet($configKey)->shouldBeCalledOnce(); + + $service = $this::__callStatic($configKey, [$container, '', null]); + $service->shouldBeAnInstanceOf(PH\Producer\PluginManager::class); + } + + public function it_throws_on_too_few_arguments_during_self_construct(ContainerInterface $container) + { + $configKey = 'test_config_key'; + $error = new \InvalidArgumentException(\sprintf( + 'To invoke %s with custom configuration key statically 3 arguments are required: container, service name and options.', + PH\Producer\Factory\PluginManager::class + )); + + $this::shouldThrow($error)->during('__callStatic', [$configKey, []]); + $this::shouldThrow($error)->during('__callStatic', [$configKey, [$container]]); + $this::shouldThrow($error)->during('__callStatic', [$configKey, [$container, '']]); + } +} diff --git a/spec/Articus/PathHandler/RouteInjection/FactorySpec.php b/spec/Articus/PathHandler/RouteInjection/FactorySpec.php deleted file mode 100644 index 2c8d8ee..0000000 --- a/spec/Articus/PathHandler/RouteInjection/FactorySpec.php +++ /dev/null @@ -1,489 +0,0 @@ - [] - ]; - $responseGenerator = function () use ($response) - { - return $response; - }; - $container->get('config')->shouldBeCalledOnce()->willReturn($config); - $container->get(Response::class)->shouldBeCalledOnce()->willReturn($responseGenerator); - - $this->__invoke($container, 'router')->shouldBeAnInstanceOf(PH\Router\FastRoute::class); - } - - public function it_returns_router_with_simple_config_using_external_cache_services( - ContainerInterface $container, - Response $response, - CacheInterface $routerCache, - CacheInterface $metadataProviderCache - ) - { - $routerCacheKey = 'router_cache_service'; - $metadataProviderCacheKey = 'metadata_provider_cache_service'; - $config = [ - PH\RouteInjection\Factory::class => [ - 'router' => [ - 'cache' => $routerCacheKey, - ], - 'metadata' => [ - 'cache' => $metadataProviderCacheKey, - ], - ] - ]; - $responseGenerator = function () use ($response) - { - return $response; - }; - $container->get('config')->shouldBeCalledOnce()->willReturn($config); - $container->get(Response::class)->shouldBeCalledOnce()->willReturn($responseGenerator); - $container->has($routerCacheKey)->shouldBeCalledOnce()->willReturn(true); - $container->get($routerCacheKey)->shouldBeCalledOnce()->willReturn($routerCache); - $container->has($metadataProviderCacheKey)->shouldBeCalledOnce()->willReturn(true); - $container->get($metadataProviderCacheKey)->shouldBeCalledOnce()->willReturn($metadataProviderCache); - - $this->__invoke($container, 'router')->shouldBeAnInstanceOf(PH\Router\FastRoute::class); - //TODO check that router and metadata provider use provided cache services - } - - public function it_returns_router_using_external_services( - ContainerInterface $container, - RouterInterface $router, - PH\MetadataProviderInterface $metadata, - PluginManagerInterface $handlerManager, - PluginManagerInterface $consumerManager, - PluginManagerInterface $attributeManager, - PluginManagerInterface $producerManager, - Response $response - ) - { - $routerKey = 'router_service'; - $metadataKey = 'metadata_service'; - $handlerManagerKey = 'handler_manager_service'; - $consumerManagerKey = 'consumer_manager_service'; - $attributeManagerKey = 'attribute_manager_service'; - $producerManagerKey = 'producer_manager_service'; - - $handleNames = ['test', 'test_1', 'test_2', 'test_3']; - $httpMethods = [ - $handleNames[0] => ['GET', 'HEAD'], - $handleNames[1] => ['POST'], - $handleNames[2] => ['PUT', 'PATCH'], - $handleNames[3] => ['DELETE'], - ]; - $routes = [ - $handleNames[0] => [ - [null, '/test/1', []], - ['0_2', '/test/2', ['test' => 123]], - ], - $handleNames[1] => [ - ['1_1', '/test_1/1', []], - [null, '/test_1/2', ['test_1' => 123]], - ], - $handleNames[2] => [ - ['2', '/test_2', []], - ], - $handleNames[3] => [ - [null, '/test_3', []], - ], - ]; - $paths = [ - '' => [$handleNames[0], $handleNames[1]], - '/1' => [$handleNames[0], $handleNames[2]], - '/2' => [$handleNames[3]], - ]; - $routeGenerator = function (array $args, $object, $method) use ($routes) - { - [$handlerName] = $args; - yield from $routes[$handlerName]; - }; - $routeChecker = function (Route $routeObject) use ($paths, $httpMethods, $routes) - { - $valid = function () use ($paths, $httpMethods, $routes) - { - foreach ($paths as $prefix => $handlerNames) - { - foreach ($handlerNames as $handlerName) - { - foreach ($routes[$handlerName] as [$name, $pattern, $defaults]) - { - $path = $prefix . $pattern; - $allowedMethods = $httpMethods[$handlerName]; - $name = $name ?? ($path . '^' . \implode(':', $allowedMethods)); - $options = empty($defaults) ? [] : ['defaults' => $defaults]; - yield [$name, $path, $allowedMethods, $options]; - } - } - } - }; - $result = false; - foreach ($valid() as [$name, $path, $allowedMethods, $options]) - { - $result = ($result - || (($routeObject->getName() === $name) - && ($routeObject->getPath() === $path) - && ($routeObject->getAllowedMethods() === $allowedMethods) - && ($routeObject->getOptions() === $options) - ) - ); - } - return $result; - }; - $responseGenerator = function () use ($response) - { - return $response; - }; - - $config = [ - PH\RouteInjection\Factory::class => [ - 'router' => $routerKey, - 'metadata' => $metadataKey, - 'paths' => $paths, - 'handlers' => $handlerManagerKey, - 'consumers' => $consumerManagerKey, - 'attributes' => $attributeManagerKey, - 'producers' => $producerManagerKey, - ] - ]; - - $container->get('config')->shouldBeCalledOnce()->willReturn($config); - $container->has($routerKey)->shouldBeCalledOnce()->willReturn(true); - $container->get($routerKey)->shouldBeCalledOnce()->willReturn($router); - $container->has($metadataKey)->shouldBeCalledOnce()->willReturn(true); - $container->get($metadataKey)->shouldBeCalledOnce()->willReturn($metadata); - $container->has($handlerManagerKey)->shouldBeCalledOnce()->willReturn(true); - $container->get($handlerManagerKey)->shouldBeCalledOnce()->willReturn($handlerManager); - $container->has($consumerManagerKey)->shouldBeCalledOnce()->willReturn(true); - $container->get($consumerManagerKey)->shouldBeCalledOnce()->willReturn($consumerManager); - $container->has($attributeManagerKey)->shouldBeCalledOnce()->willReturn(true); - $container->get($attributeManagerKey)->shouldBeCalledOnce()->willReturn($attributeManager); - $container->has($producerManagerKey)->shouldBeCalledOnce()->willReturn(true); - $container->get($producerManagerKey)->shouldBeCalledOnce()->willReturn($producerManager); - $container->get(Response::class)->shouldBeCalledOnce()->willReturn($responseGenerator); - - $metadata->getHttpMethods($handleNames[0])->shouldBeCalledTimes(2)->willReturn($httpMethods[$handleNames[0]]); - $metadata->getHttpMethods($handleNames[1])->shouldBeCalledOnce()->willReturn($httpMethods[$handleNames[1]]); - $metadata->getHttpMethods($handleNames[2])->shouldBeCalledOnce()->willReturn($httpMethods[$handleNames[2]]); - $metadata->getHttpMethods($handleNames[3])->shouldBeCalledOnce()->willReturn($httpMethods[$handleNames[3]]); - - $metadata->getRoutes($handleNames[0])->shouldBeCalledTimes(2)->will($routeGenerator); - $metadata->getRoutes($handleNames[1])->shouldBeCalledOnce()->will($routeGenerator); - $metadata->getRoutes($handleNames[2])->shouldBeCalledOnce()->will($routeGenerator); - $metadata->getRoutes($handleNames[3])->shouldBeCalledOnce()->will($routeGenerator); - - $router->addRoute(Argument::that($routeChecker))->shouldBeCalledTimes(8); - - $this->__invoke($container, 'router')->shouldBe($router); - } - - - public function it_throws_on_empty_router_config(ContainerInterface $container) - { - $config = [ - PH\RouteInjection\Factory::class => [ - 'router' => [], - ] - ]; - $container->get('config')->shouldBeCalledOnce()->willReturn($config); - $this->shouldThrow(\LogicException::class)->during('__invoke', [$container, 'router']); - } - - public function it_throws_on_invalid_router_cache(ContainerInterface $container, $routerCache) - { - $routerCacheKey = 'invalid_cache'; - $config = [ - PH\RouteInjection\Factory::class => [ - 'router' => [ - 'cache' => $routerCacheKey, - ], - ], - ]; - $container->get('config')->shouldBeCalledOnce()->willReturn($config); - $container->has($routerCacheKey)->shouldBeCalledOnce()->willReturn(true); - $container->get($routerCacheKey)->shouldBeCalledOnce()->willReturn($routerCache); - $this->shouldThrow(\LogicException::class)->during('__invoke', [$container, 'router']); - } - - public function it_throws_on_invalid_router_cache_config(ContainerInterface $container) - { - $config = [ - PH\RouteInjection\Factory::class => [ - 'router' => [ - 'cache' => 123, - ], - ], - ]; - $container->get('config')->shouldBeCalledOnce()->willReturn($config); - $this->shouldThrow(\LogicException::class)->during('__invoke', [$container, 'router']); - } - - public function it_throws_on_invalid_router(ContainerInterface $container, $router) - { - $routerKey = 'invalid_router'; - $config = [ - PH\RouteInjection\Factory::class => [ - 'router' => $routerKey, - ] - ]; - $container->get('config')->shouldBeCalledOnce()->willReturn($config); - $container->has($routerKey)->shouldBeCalledOnce()->willReturn(true); - $container->get($routerKey)->shouldBeCalledOnce()->willReturn($router); - $this->shouldThrow(\LogicException::class)->during('__invoke', [$container, 'router']); - } - - public function it_throws_on_invalid_router_config(ContainerInterface $container) - { - $config = [ - PH\RouteInjection\Factory::class => [ - 'router' => 123, - ] - ]; - $container->get('config')->shouldBeCalledOnce()->willReturn($config); - $this->shouldThrow(\LogicException::class)->during('__invoke', [$container, 'router']); - } - - - public function it_throws_on_empty_metadata_provider_config(ContainerInterface $container) - { - $config = [ - PH\RouteInjection\Factory::class => [ - 'metadata' => [], - ] - ]; - $container->get('config')->shouldBeCalledOnce()->willReturn($config); - $this->shouldThrow(\LogicException::class)->during('__invoke', [$container, 'router']); - } - - public function it_throws_on_invalid_metadata_provider_cache(ContainerInterface $container, $metadataCache) - { - $metadataCacheKey = 'invalid_cache'; - $config = [ - PH\RouteInjection\Factory::class => [ - 'metadata' => [ - 'cache' => $metadataCacheKey, - ], - ], - ]; - $container->get('config')->shouldBeCalledOnce()->willReturn($config); - $container->has($metadataCacheKey)->shouldBeCalledOnce()->willReturn(true); - $container->get($metadataCacheKey)->shouldBeCalledOnce()->willReturn($metadataCache); - $this->shouldThrow(\LogicException::class)->during('__invoke', [$container, 'router']); - } - - public function it_throws_on_invalid_metadata_provider_cache_config(ContainerInterface $container) - { - $config = [ - PH\RouteInjection\Factory::class => [ - 'metadata' => [ - 'cache' => 123, - ], - ], - ]; - $container->get('config')->shouldBeCalledOnce()->willReturn($config); - $this->shouldThrow(\LogicException::class)->during('__invoke', [$container, 'router']); - } - - public function it_throws_on_invalid_metadata_provider(ContainerInterface $container, $metadata) - { - $metadataKey = 'invalid_metadata'; - $config = [ - PH\RouteInjection\Factory::class => [ - 'metadata' => $metadataKey, - ] - ]; - $container->get('config')->shouldBeCalledOnce()->willReturn($config); - $container->has($metadataKey)->shouldBeCalledOnce()->willReturn(true); - $container->get($metadataKey)->shouldBeCalledOnce()->willReturn($metadata); - $this->shouldThrow(\LogicException::class)->during('__invoke', [$container, 'router']); - } - - public function it_throws_on_invalid_metadata_provider_config(ContainerInterface $container) - { - $config = [ - PH\RouteInjection\Factory::class => [ - 'metadata' => 123, - ] - ]; - $container->get('config')->shouldBeCalledOnce()->willReturn($config); - $this->shouldThrow(\LogicException::class)->during('__invoke', [$container, 'router']); - } - - - public function it_throws_on_invalid_handler_manager(ContainerInterface $container, $handlerManager) - { - $key = 'invalid'; - $config = [ - PH\RouteInjection\Factory::class => [ - 'handlers' => $key, - ] - ]; - $container->get('config')->shouldBeCalledOnce()->willReturn($config); - $container->has($key)->shouldBeCalledOnce()->willReturn(true); - $container->get($key)->shouldBeCalledOnce()->willReturn($handlerManager); - $this->shouldThrow(\LogicException::class)->during('__invoke', [$container, 'router']); - } - - public function it_throws_on_invalid_handler_manager_config(ContainerInterface $container) - { - $config = [ - PH\RouteInjection\Factory::class => [ - 'handlers' => 123, - ] - ]; - $container->get('config')->shouldBeCalledOnce()->willReturn($config); - $this->shouldThrow(\LogicException::class)->during('__invoke', [$container, 'router']); - } - - - public function it_throws_on_invalid_consumer_manager(ContainerInterface $container, $consumerManager) - { - $key = 'invalid'; - $config = [ - PH\RouteInjection\Factory::class => [ - 'consumers' => $key, - ] - ]; - $container->get('config')->shouldBeCalledOnce()->willReturn($config); - $container->has($key)->shouldBeCalledOnce()->willReturn(true); - $container->get($key)->shouldBeCalledOnce()->willReturn($consumerManager); - $this->shouldThrow(\LogicException::class)->during('__invoke', [$container, 'router']); - } - - public function it_throws_on_invalid_consumer_manager_config(ContainerInterface $container) - { - $config = [ - PH\RouteInjection\Factory::class => [ - 'consumers' => 123, - ] - ]; - $container->get('config')->shouldBeCalledOnce()->willReturn($config); - $this->shouldThrow(\LogicException::class)->during('__invoke', [$container, 'router']); - } - - - public function it_throws_on_invalid_attribute_manager(ContainerInterface $container, $attributeManager) - { - $key = 'invalid'; - $config = [ - PH\RouteInjection\Factory::class => [ - 'attributes' => $key, - ] - ]; - $container->get('config')->shouldBeCalledOnce()->willReturn($config); - $container->has($key)->shouldBeCalledOnce()->willReturn(true); - $container->get($key)->shouldBeCalledOnce()->willReturn($attributeManager); - $this->shouldThrow(\LogicException::class)->during('__invoke', [$container, 'router']); - } - - public function it_throws_on_invalid_attribute_manager_config(ContainerInterface $container) - { - $config = [ - PH\RouteInjection\Factory::class => [ - 'attributes' => 123, - ] - ]; - $container->get('config')->shouldBeCalledOnce()->willReturn($config); - $this->shouldThrow(\LogicException::class)->during('__invoke', [$container, 'router']); - } - - - public function it_throws_on_invalid_producer_manager(ContainerInterface $container, $producerManager) - { - $key = 'invalid'; - $config = [ - PH\RouteInjection\Factory::class => [ - 'producers' => $key, - ] - ]; - $container->get('config')->shouldBeCalledOnce()->willReturn($config); - $container->has($key)->shouldBeCalledOnce()->willReturn(true); - $container->get($key)->shouldBeCalledOnce()->willReturn($producerManager); - $this->shouldThrow(\LogicException::class)->during('__invoke', [$container, 'router']); - } - - public function it_throws_on_invalid_producer_manager_config(ContainerInterface $container) - { - $config = [ - PH\RouteInjection\Factory::class => [ - 'producers' => 123, - ] - ]; - $container->get('config')->shouldBeCalledOnce()->willReturn($config); - $this->shouldThrow(\LogicException::class)->during('__invoke', [$container, 'router']); - } - - public function it_throws_on_invalid_response_generator(ContainerInterface $container) - { - $config = [ - PH\RouteInjection\Factory::class => [] - ]; - $container->get('config')->shouldBeCalledOnce()->willReturn($config); - $container->get(Response::class)->shouldBeCalledOnce()->willReturn(null); - $this->shouldThrow(\LogicException::class)->during('__invoke', [$container, 'router']); - } - - public function it_gets_configuration_from_custom_config_key(ContainerInterface $container, Response $response, \ArrayAccess $config) - { - $responseGenerator = function () use ($response) - { - return $response; - }; - - $configKey = 'test_config_key'; - $container->get('config')->shouldBeCalledOnce()->willReturn($config); - $config->offsetExists($configKey)->shouldBeCalledOnce()->willReturn(true); - $config->offsetGet($configKey)->shouldBeCalledOnce()->willReturn([]); - $container->get(Response::class)->shouldBeCalledOnce()->willReturn($responseGenerator); - - $this->beConstructedWith($configKey); - $this->__invoke($container, 'router')->shouldBeAnInstanceOf(PH\Router\FastRoute::class); - } - - public function it_constructs_itself_and_gets_configuration_from_custom_config_key(ContainerInterface $container, Response $response, \ArrayAccess $config) - { - $responseGenerator = function () use ($response) - { - return $response; - }; - - $configKey = 'test_config_key'; - $container->get('config')->shouldBeCalledOnce()->willReturn($config); - $config->offsetExists($configKey)->shouldBeCalledOnce()->willReturn(true); - $config->offsetGet($configKey)->shouldBeCalledOnce()->willReturn([]); - $container->get(Response::class)->shouldBeCalledOnce()->willReturn($responseGenerator); - - $this->__callStatic($configKey, [$container, 'router', null])->shouldBeAnInstanceOf(PH\Router\FastRoute::class); - } - - public function it_throws_on_too_few_arguments_during_self_construct(ContainerInterface $container) - { - $configKey = 'test_config_key'; - $error = new \InvalidArgumentException(\sprintf( - 'To invoke %s with custom configuration key statically 3 arguments are required: container, service name and options.', - PH\RouteInjection\Factory::class - )); - - $this::shouldThrow($error)->during('__callStatic', [$configKey, []]); - $this::shouldThrow($error)->during('__callStatic', [$configKey, [$container]]); - $this::shouldThrow($error)->during('__callStatic', [$configKey, [$container, 'router']]); - } - -} diff --git a/spec/Articus/PathHandler/RouteInjectionFactorySpec.php b/spec/Articus/PathHandler/RouteInjectionFactorySpec.php new file mode 100644 index 0000000..0f94aae --- /dev/null +++ b/spec/Articus/PathHandler/RouteInjectionFactorySpec.php @@ -0,0 +1,339 @@ + [] + ]; + $container->get('config')->shouldBeCalledOnce()->willReturn($config); + $container->get(PH\MetadataProviderInterface::class)->shouldBeCalledOnce()->willReturn($metadataProvider); + $container->get(PH\Handler\PluginManager::class)->shouldBeCalledOnce()->willReturn($handlerManager); + $container->get(PH\Consumer\PluginManager::class)->shouldBeCalledOnce()->willReturn($consumerManager); + $container->get(PH\Attribute\PluginManager::class)->shouldBeCalledOnce()->willReturn($attributeManager); + $container->get(PH\Producer\PluginManager::class)->shouldBeCalledOnce()->willReturn($producerManager); + $responseGenerator = function () use ($response) + { + return $response; + }; + $container->get(Response::class)->shouldBeCalledOnce()->willReturn($responseGenerator); + + $this->__invoke($container, 'router')->shouldBeAnInstanceOf(PH\Router\FastRoute::class); + } + + public function it_returns_router_with_simple_config_using_external_cache_service( + ContainerInterface $container, + PH\MetadataProviderInterface $metadataProvider, + PH\Handler\PluginManager $handlerManager, + PH\Consumer\PluginManager $consumerManager, + PH\Attribute\PluginManager $attributeManager, + PH\Producer\PluginManager $producerManager, + Response $response, + CacheInterface $routerCache + ) + { + $routerCacheServiceKey = 'router_cache_service'; + $config = [ + PH\RouteInjectionFactory::class => [ + 'router' => [ + 'cache' => $routerCacheServiceKey, + ], + ] + ]; + $container->get('config')->shouldBeCalledOnce()->willReturn($config); + $container->get(PH\MetadataProviderInterface::class)->shouldBeCalledOnce()->willReturn($metadataProvider); + $container->get(PH\Handler\PluginManager::class)->shouldBeCalledOnce()->willReturn($handlerManager); + $container->get(PH\Consumer\PluginManager::class)->shouldBeCalledOnce()->willReturn($consumerManager); + $container->get(PH\Attribute\PluginManager::class)->shouldBeCalledOnce()->willReturn($attributeManager); + $container->get(PH\Producer\PluginManager::class)->shouldBeCalledOnce()->willReturn($producerManager); + $responseGenerator = function () use ($response) + { + return $response; + }; + $container->get(Response::class)->shouldBeCalledOnce()->willReturn($responseGenerator); + $container->has($routerCacheServiceKey)->shouldBeCalledOnce()->willReturn(true); + $container->get($routerCacheServiceKey)->shouldBeCalledOnce()->willReturn($routerCache); + + $this->__invoke($container, 'router')->shouldBeAnInstanceOf(PH\Router\FastRoute::class); + //TODO check that router and metadata provider use provided cache services + } + + public function it_returns_router_using_external_services( + ContainerInterface $container, + RouterInterface $router, + PH\MetadataProviderInterface $metadataProvider, + PH\Handler\PluginManager $handlerManager, + PH\Consumer\PluginManager $consumerManager, + PH\Attribute\PluginManager $attributeManager, + PH\Producer\PluginManager $producerManager, + Response $response + ) + { + $routerKey = 'router_service'; + + $handleNames = ['test', 'test_1', 'test_2', 'test_3']; + $httpMethods = [ + $handleNames[0] => ['GET', 'HEAD'], + $handleNames[1] => ['POST'], + $handleNames[2] => ['PUT', 'PATCH'], + $handleNames[3] => ['DELETE'], + ]; + $routes = [ + $handleNames[0] => [ + [null, '/test/1', []], + ['0_2', '/test/2', ['test' => 123]], + ], + $handleNames[1] => [ + ['1_1', '/test_1/1', []], + [null, '/test_1/2', ['test_1' => 123]], + ], + $handleNames[2] => [ + ['2', '/test_2', []], + ], + $handleNames[3] => [ + [null, '/test_3', []], + ], + ]; + $paths = [ + '' => [$handleNames[0], $handleNames[1]], + '/1' => [$handleNames[0], $handleNames[2]], + '/2' => [$handleNames[3]], + ]; + $routeGenerator = function (array $args, $object, $method) use ($routes) + { + [$handlerName] = $args; + yield from $routes[$handlerName]; + }; + $routeChecker = function (Route $routeObject) use ($paths, $httpMethods, $routes) + { + $valid = function () use ($paths, $httpMethods, $routes) + { + foreach ($paths as $prefix => $handlerNames) + { + foreach ($handlerNames as $handlerName) + { + foreach ($routes[$handlerName] as [$name, $pattern, $defaults]) + { + $path = $prefix . $pattern; + $allowedMethods = $httpMethods[$handlerName]; + $name = $name ?? ($path . '^' . \implode(':', $allowedMethods)); + $options = empty($defaults) ? [] : ['defaults' => $defaults]; + yield [$name, $path, $allowedMethods, $options]; + } + } + } + }; + $result = false; + foreach ($valid() as [$name, $path, $allowedMethods, $options]) + { + $result = ($result + || (($routeObject->getName() === $name) + && ($routeObject->getPath() === $path) + && ($routeObject->getAllowedMethods() === $allowedMethods) + && ($routeObject->getOptions() === $options) + ) + ); + } + return $result; + }; + $responseGenerator = function () use ($response) + { + return $response; + }; + + $config = [ + PH\RouteInjectionFactory::class => [ + 'router' => $routerKey, + 'paths' => $paths, + ] + ]; + + $container->get('config')->shouldBeCalledOnce()->willReturn($config); + $container->has($routerKey)->shouldBeCalledOnce()->willReturn(true); + $container->get($routerKey)->shouldBeCalledOnce()->willReturn($router); + + + $container->get(PH\MetadataProviderInterface::class)->shouldBeCalledOnce()->willReturn($metadataProvider); + $container->get(PH\Handler\PluginManager::class)->shouldBeCalledOnce()->willReturn($handlerManager); + $container->get(PH\Consumer\PluginManager::class)->shouldBeCalledOnce()->willReturn($consumerManager); + $container->get(PH\Attribute\PluginManager::class)->shouldBeCalledOnce()->willReturn($attributeManager); + $container->get(PH\Producer\PluginManager::class)->shouldBeCalledOnce()->willReturn($producerManager); + $container->get(Response::class)->shouldBeCalledOnce()->willReturn($responseGenerator); + + $metadataProvider->getHttpMethods($handleNames[0])->shouldBeCalledTimes(2)->willReturn($httpMethods[$handleNames[0]]); + $metadataProvider->getHttpMethods($handleNames[1])->shouldBeCalledOnce()->willReturn($httpMethods[$handleNames[1]]); + $metadataProvider->getHttpMethods($handleNames[2])->shouldBeCalledOnce()->willReturn($httpMethods[$handleNames[2]]); + $metadataProvider->getHttpMethods($handleNames[3])->shouldBeCalledOnce()->willReturn($httpMethods[$handleNames[3]]); + + $metadataProvider->getRoutes($handleNames[0])->shouldBeCalledTimes(2)->will($routeGenerator); + $metadataProvider->getRoutes($handleNames[1])->shouldBeCalledOnce()->will($routeGenerator); + $metadataProvider->getRoutes($handleNames[2])->shouldBeCalledOnce()->will($routeGenerator); + $metadataProvider->getRoutes($handleNames[3])->shouldBeCalledOnce()->will($routeGenerator); + + $router->addRoute(Argument::that($routeChecker))->shouldBeCalledTimes(8); + + $this->__invoke($container, 'router')->shouldBe($router); + } + + + public function it_throws_on_empty_router_config(ContainerInterface $container) + { + $config = [ + PH\RouteInjectionFactory::class => [ + 'router' => [], + ] + ]; + $container->get('config')->shouldBeCalledOnce()->willReturn($config); + $this->shouldThrow(\LogicException::class)->during('__invoke', [$container, 'router']); + } + + public function it_throws_on_invalid_router_cache(ContainerInterface $container, $routerCache) + { + $routerCacheKey = 'invalid_cache'; + $config = [ + PH\RouteInjectionFactory::class => [ + 'router' => [ + 'cache' => $routerCacheKey, + ], + ], + ]; + $container->get('config')->shouldBeCalledOnce()->willReturn($config); + $container->has($routerCacheKey)->shouldBeCalledOnce()->willReturn(true); + $container->get($routerCacheKey)->shouldBeCalledOnce()->willReturn($routerCache); + $this->shouldThrow(\LogicException::class)->during('__invoke', [$container, 'router']); + } + + public function it_throws_on_invalid_router_cache_config(ContainerInterface $container) + { + $config = [ + PH\RouteInjectionFactory::class => [ + 'router' => [ + 'cache' => 123, + ], + ], + ]; + $container->get('config')->shouldBeCalledOnce()->willReturn($config); + $this->shouldThrow(\LogicException::class)->during('__invoke', [$container, 'router']); + } + + public function it_throws_on_invalid_router(ContainerInterface $container, $router) + { + $routerKey = 'invalid_router'; + $config = [ + PH\RouteInjectionFactory::class => [ + 'router' => $routerKey, + ] + ]; + $container->get('config')->shouldBeCalledOnce()->willReturn($config); + $container->has($routerKey)->shouldBeCalledOnce()->willReturn(true); + $container->get($routerKey)->shouldBeCalledOnce()->willReturn($router); + $this->shouldThrow(\LogicException::class)->during('__invoke', [$container, 'router']); + } + + public function it_throws_on_invalid_router_config(ContainerInterface $container) + { + $config = [ + PH\RouteInjectionFactory::class => [ + 'router' => 123, + ] + ]; + $container->get('config')->shouldBeCalledOnce()->willReturn($config); + $this->shouldThrow(\LogicException::class)->during('__invoke', [$container, 'router']); + } + + public function it_gets_configuration_from_custom_config_key( + ContainerInterface $container, + PH\MetadataProviderInterface $metadataProvider, + PH\Handler\PluginManager $handlerManager, + PH\Consumer\PluginManager $consumerManager, + PH\Attribute\PluginManager $attributeManager, + PH\Producer\PluginManager $producerManager, + Response $response, + \ArrayAccess $config + ) + { + $responseGenerator = function () use ($response) + { + return $response; + }; + + $configKey = 'test_config_key'; + $container->get('config')->shouldBeCalledOnce()->willReturn($config); + $config->offsetExists($configKey)->shouldBeCalledOnce()->willReturn(true); + $config->offsetGet($configKey)->shouldBeCalledOnce()->willReturn([]); + $container->get(PH\MetadataProviderInterface::class)->shouldBeCalledOnce()->willReturn($metadataProvider); + $container->get(PH\Handler\PluginManager::class)->shouldBeCalledOnce()->willReturn($handlerManager); + $container->get(PH\Consumer\PluginManager::class)->shouldBeCalledOnce()->willReturn($consumerManager); + $container->get(PH\Attribute\PluginManager::class)->shouldBeCalledOnce()->willReturn($attributeManager); + $container->get(PH\Producer\PluginManager::class)->shouldBeCalledOnce()->willReturn($producerManager); + $container->get(Response::class)->shouldBeCalledOnce()->willReturn($responseGenerator); + + $this->beConstructedWith($configKey); + $this->__invoke($container, 'router')->shouldBeAnInstanceOf(PH\Router\FastRoute::class); + } + + public function it_constructs_itself_and_gets_configuration_from_custom_config_key( + ContainerInterface $container, + PH\MetadataProviderInterface $metadataProvider, + PH\Handler\PluginManager $handlerManager, + PH\Consumer\PluginManager $consumerManager, + PH\Attribute\PluginManager $attributeManager, + PH\Producer\PluginManager $producerManager, + Response $response, + \ArrayAccess $config + ) + { + $responseGenerator = function () use ($response) + { + return $response; + }; + + $configKey = 'test_config_key'; + $container->get('config')->shouldBeCalledOnce()->willReturn($config); + $config->offsetExists($configKey)->shouldBeCalledOnce()->willReturn(true); + $config->offsetGet($configKey)->shouldBeCalledOnce()->willReturn([]); + $container->get(PH\MetadataProviderInterface::class)->shouldBeCalledOnce()->willReturn($metadataProvider); + $container->get(PH\Handler\PluginManager::class)->shouldBeCalledOnce()->willReturn($handlerManager); + $container->get(PH\Consumer\PluginManager::class)->shouldBeCalledOnce()->willReturn($consumerManager); + $container->get(PH\Attribute\PluginManager::class)->shouldBeCalledOnce()->willReturn($attributeManager); + $container->get(PH\Producer\PluginManager::class)->shouldBeCalledOnce()->willReturn($producerManager); + $container->get(Response::class)->shouldBeCalledOnce()->willReturn($responseGenerator); + + $this::__callStatic($configKey, [$container, 'router', null])->shouldBeAnInstanceOf(PH\Router\FastRoute::class); + } + + public function it_throws_on_too_few_arguments_during_self_construct(ContainerInterface $container) + { + $configKey = 'test_config_key'; + $error = new \InvalidArgumentException(\sprintf( + 'To invoke %s with custom configuration key statically 3 arguments are required: container, service name and options.', + PH\RouteInjectionFactory::class + )); + + $this::shouldThrow($error)->during('__callStatic', [$configKey, []]); + $this::shouldThrow($error)->during('__callStatic', [$configKey, [$container]]); + $this::shouldThrow($error)->during('__callStatic', [$configKey, [$container, 'router']]); + } + +} diff --git a/spec/ExampleForPhp8/Handler/NoMethodsHandlingHttpMethods.php b/spec/ExampleForPhp8/Handler/NoMethodsHandlingHttpMethods.php new file mode 100644 index 0000000..686da72 --- /dev/null +++ b/spec/ExampleForPhp8/Handler/NoMethodsHandlingHttpMethods.php @@ -0,0 +1,19 @@ + 123])] + #[PHA\Attribute(name: "test_7", options: ["test_7" => 123])] + #[PHA\Attribute(name: "test_4", priority: 2, options: ["test_4" => 123])] + public function severalAttributes(Request $request) + { + } +} \ No newline at end of file diff --git a/spec/ExampleForPhp8/Handler/ValidCommonAttributes.php b/spec/ExampleForPhp8/Handler/ValidCommonAttributes.php new file mode 100644 index 0000000..f4792c7 --- /dev/null +++ b/spec/ExampleForPhp8/Handler/ValidCommonAttributes.php @@ -0,0 +1,35 @@ + 123])] +#[PHA\Attribute(name: "test_c7", options: ["test_c7" => 123])] +#[PHA\Attribute(name: "test_c4", priority: 2, options: ["test_c4" => 123])] +class ValidCommonAttributes +{ + #[PHA\HttpMethod("NO_ATTRIBUTES")] + public function noAttributes(Request $request) + { + } + + #[PHA\HttpMethod("SEVERAL_ATTRIBUTES")] + #[PHA\Attribute(name: "test_5")] + #[PHA\Attribute(name: "test_1", priority: 3)] + #[PHA\Attribute(name: "test_3", priority: 2)] + #[PHA\Attribute(name: "test_6")] + #[PHA\Attribute(name: "test_2", priority: 3, options: ["test_2" => 123])] + #[PHA\Attribute(name: "test_7", options: ["test_7" => 123])] + #[PHA\Attribute(name: "test_4", priority: 2, options: ["test_4" => 123])] + public function severalAttributes(Request $request) + { + } +} \ No newline at end of file diff --git a/spec/ExampleForPhp8/Handler/ValidCommonConsumers.php b/spec/ExampleForPhp8/Handler/ValidCommonConsumers.php new file mode 100644 index 0000000..a9f99aa --- /dev/null +++ b/spec/ExampleForPhp8/Handler/ValidCommonConsumers.php @@ -0,0 +1,37 @@ + 123])] +#[PHA\Consumer(mediaRange: "*/*", name: "test_c7", options: ["test_c7" => 123])] +#[PHA\Consumer(mediaRange: "test/c4", name: "test_c4", priority: 2, options: ["test_c4" => 123])] +#[PHA\Consumer(mediaRange: "test/c8", name: "test_c8", options: ["test_c8" => 123])] +class ValidCommonConsumers +{ + #[PHA\HttpMethod("NO_CONSUMERS")] + public function noConsumers(Request $request) + { + } + + #[PHA\HttpMethod("SEVERAL_CONSUMERS")] + #[PHA\Consumer(mediaRange: "*/*", name: "test_5")] + #[PHA\Consumer(mediaRange: "*/*", name: "test_1", priority: 3)] + #[PHA\Consumer(mediaRange: "test/3", name: "test_3", priority: 2)] + #[PHA\Consumer(mediaRange: "test/6", name: "test_6")] + #[PHA\Consumer(mediaRange: "*/*", name: "test_2", options: ["test_2" => 123], priority: 3)] + #[PHA\Consumer(mediaRange: "*/*", name: "test_7", options: ["test_7" => 123])] + #[PHA\Consumer(mediaRange: "test/4", name: "test_4", options: ["test_4" => 123], priority: 2)] + #[PHA\Consumer(mediaRange: "test/8", name: "test_8", options: ["test_8" => 123])] + public function severalConsumers(Request $request) + { + } +} \ No newline at end of file diff --git a/spec/ExampleForPhp8/Handler/ValidCommonProducers.php b/spec/ExampleForPhp8/Handler/ValidCommonProducers.php new file mode 100644 index 0000000..93736c7 --- /dev/null +++ b/spec/ExampleForPhp8/Handler/ValidCommonProducers.php @@ -0,0 +1,35 @@ + 123])] +#[PHA\Producer(mediaType: "test/c7", name: "test_c7", options: ["test_c7" => 123])] +#[PHA\Producer(mediaType: "test/c4", name: "test_c4", priority: 2, options: ["test_c4" => 123])] +class ValidCommonProducers +{ + #[PHA\HttpMethod("NO_PRODUCERS")] + public function noProducers(Request $request) + { + } + + #[PHA\HttpMethod("SEVERAL_PRODUCERS")] + #[PHA\Producer(mediaType: "test/5", name: "test_5")] + #[PHA\Producer(mediaType: "test/1", name: "test_1", priority: 3)] + #[PHA\Producer(mediaType: "test/3", name: "test_3", priority: 2)] + #[PHA\Producer(mediaType: "test/6", name: "test_6")] + #[PHA\Producer(mediaType: "test/2", name: "test_2", priority: 3, options: ["test_2" => 123])] + #[PHA\Producer(mediaType: "test/7", name: "test_7", options: ["test_7" => 123])] + #[PHA\Producer(mediaType: "test/4", name: "test_4", priority: 2, options: ["test_4" => 123])] + public function severalProducers(Request $request) + { + } +} \ No newline at end of file diff --git a/spec/ExampleForPhp8/Handler/ValidConsumers.php b/spec/ExampleForPhp8/Handler/ValidConsumers.php new file mode 100644 index 0000000..61bf897 --- /dev/null +++ b/spec/ExampleForPhp8/Handler/ValidConsumers.php @@ -0,0 +1,29 @@ + 123], priority: 3)] + #[PHA\Consumer(mediaRange: "*/*", name: "test_7", options: ["test_7" => 123])] + #[PHA\Consumer(mediaRange: "test/4", name: "test_4", options: ["test_4" => 123], priority: 2)] + #[PHA\Consumer(mediaRange: "test/8", name: "test_8", options: ["test_8" => 123])] + public function severalConsumers(Request $request) + { + } +} \ No newline at end of file diff --git a/spec/ExampleForPhp8/Handler/ValidHttpMethods.php b/spec/ExampleForPhp8/Handler/ValidHttpMethods.php new file mode 100644 index 0000000..657d87b --- /dev/null +++ b/spec/ExampleForPhp8/Handler/ValidHttpMethods.php @@ -0,0 +1,45 @@ + 123])] + #[PHA\Producer(mediaType: "test/7", name: "test_7", options: ["test_7" => 123])] + #[PHA\Producer(mediaType: "test/4", name: "test_4", priority: 2, options: ["test_4" => 123])] + public function severalProducers(Request $request) + { + } +} \ No newline at end of file diff --git a/spec/ExampleForPhp8/Handler/ValidRoutes.php b/spec/ExampleForPhp8/Handler/ValidRoutes.php new file mode 100644 index 0000000..7be9988 --- /dev/null +++ b/spec/ExampleForPhp8/Handler/ValidRoutes.php @@ -0,0 +1,23 @@ + 123])] +#[PHA\Route(pattern: "/7", defaults: ["test_7" => 123])] +#[PHA\Route(pattern: "/4", priority: 2, name: "test_4", defaults: ["test_4" => 123])] +#[PHA\Route(pattern: "/8", name: "test_8", defaults: ["test_8" => 123])] +class ValidRoutes +{ + #[PHA\Get()] + public function read(Request $request) + { + } +} \ No newline at end of file diff --git a/spec/Matcher/PropertyValue.php b/spec/Matcher/PropertyValue.php new file mode 100644 index 0000000..de184c8 --- /dev/null +++ b/spec/Matcher/PropertyValue.php @@ -0,0 +1,78 @@ +processProperty($subject, $arguments); + if ($actualPropertyValue !== $expectedPropertyValue) + { + throw new FailureException(\sprintf( + 'Expected "%s" property value of %s instance to be %s, but got %s.', + $propertyName, + \get_class($subject), + \var_export($expectedPropertyValue, true), + \var_export($actualPropertyValue, true) + )); + } + return null; + } + + public function negativeMatch(string $name, $subject, array $arguments): ?DelayedCall + { + [$propertyName, $expectedPropertyValue, $actualPropertyValue] = $this->processProperty($subject, $arguments); + if ($actualPropertyValue === $expectedPropertyValue) + { + throw new FailureException(\sprintf( + 'Did not expect "%s" property value of %s instance to be %s, but got one.', + $propertyName, + \get_class($subject), + \var_export($expectedPropertyValue, true) + )); + } + return null; + } + + /** + * @param object $subject + * @param array $arguments + * @return array + * @throws \ReflectionException + */ + protected function processProperty($subject, array $arguments): array + { + [$propertyName, $expectedPropertyValue] = $arguments; + $classReflection = new \ReflectionClass($subject); + $propertyReflection = $classReflection->getProperty($propertyName); + $propertyReflection->setAccessible(true); + $actualPropertyValue = $propertyReflection->getValue($subject); + return [$propertyName, $expectedPropertyValue, $actualPropertyValue]; + } +} diff --git a/spec/Matcher/PropertyValueType.php b/spec/Matcher/PropertyValueType.php new file mode 100644 index 0000000..aa0dc1e --- /dev/null +++ b/spec/Matcher/PropertyValueType.php @@ -0,0 +1,81 @@ +processProperty($subject, $arguments); + if ($actualPropertyValueType !== $expectedPropertyValueType) + { + throw new FailureException(\sprintf( + 'Expected "%s" property value of %s instance to have type %s, but got %s.', + $propertyName, + \get_class($subject), + $expectedPropertyValueType, + $actualPropertyValueType + )); + } + return null; + } + + public function negativeMatch(string $name, $subject, array $arguments): ?DelayedCall + { + [$propertyName, $expectedPropertyValueType, $actualPropertyValueType] = $this->processProperty($subject, $arguments); + if ($actualPropertyValueType === $expectedPropertyValueType) + { + throw new FailureException(\sprintf( + 'Did not expect "%s" property value of %s instance to be %s, but got one.', + $propertyName, + \get_class($subject), + $expectedPropertyValueType + )); + } + return null; + } + + /** + * @param object $subject + * @param array $arguments + * @return array + * @throws \ReflectionException + */ + protected function processProperty($subject, array $arguments): array + { + [$propertyName, $expectedPropertyValueType] = $arguments; + $classReflection = new \ReflectionClass($subject); + $propertyReflection = $classReflection->getProperty($propertyName); + $propertyReflection->setAccessible(true); + $actualPropertyValue = $propertyReflection->getValue($subject); + $actualPropertyValueType = \is_object($actualPropertyValue) ? \get_class($actualPropertyValue) : \gettype($actualPropertyValue); + return [$propertyName, $expectedPropertyValueType, $actualPropertyValueType]; + } +} diff --git a/src/Articus/PathHandler/Attribute/Factory/PluginManager.php b/src/Articus/PathHandler/Attribute/Factory/PluginManager.php new file mode 100644 index 0000000..76977f3 --- /dev/null +++ b/src/Articus/PathHandler/Attribute/Factory/PluginManager.php @@ -0,0 +1,20 @@ +getServiceConfig($container)); + } +} diff --git a/src/Articus/PathHandler/CacheKeyAwareTrait.php b/src/Articus/PathHandler/CacheKeyAwareTrait.php new file mode 100644 index 0000000..414faa5 --- /dev/null +++ b/src/Articus/PathHandler/CacheKeyAwareTrait.php @@ -0,0 +1,33 @@ +has($options)): + $result = $container->get($options); + if (!($result instanceof CacheInterface)) + { + throw new \LogicException(\sprintf('Invalid cache service for key "%s".', $cacheKey)); + } + break; + default: + throw new \LogicException(\sprintf('Invalid cache configuration for key "%s".', $cacheKey)); + } + return $result; + } + +} diff --git a/src/Articus/PathHandler/Consumer/Factory/Json.php b/src/Articus/PathHandler/Consumer/Factory/Json.php new file mode 100644 index 0000000..cd903ee --- /dev/null +++ b/src/Articus/PathHandler/Consumer/Factory/Json.php @@ -0,0 +1,17 @@ +getServiceConfig($container)); + } +} diff --git a/src/Articus/PathHandler/Consumer/Json.php b/src/Articus/PathHandler/Consumer/Json.php index 5b8f0b1..b5995d4 100644 --- a/src/Articus/PathHandler/Consumer/Json.php +++ b/src/Articus/PathHandler/Consumer/Json.php @@ -11,6 +11,20 @@ */ class Json implements ConsumerInterface { + /** + * Flag if objects in JSON should be decoded as stdClass instances + * @var bool + */ + protected $parseAsStdClass; + + /** + * @param bool $parseAsStdClass + */ + public function __construct(bool $parseAsStdClass) + { + $this->parseAsStdClass = $parseAsStdClass; + } + /** * @inheritdoc * @throws Exception\BadRequest @@ -18,7 +32,7 @@ class Json implements ConsumerInterface public function parse(StreamInterface $body, $preParsedBody, string $mediaType, array $parameters) { //TODO allow to pass decoding options via parameters - $result = \json_decode($body->getContents(), true); + $result = \json_decode($body->getContents(), !$this->parseAsStdClass); if (($result === null) && (\json_last_error() !== \JSON_ERROR_NONE)) { throw new Exception\BadRequest('Malformed JSON: failed to decode'); diff --git a/src/Articus/PathHandler/Consumer/PluginManager.php b/src/Articus/PathHandler/Consumer/PluginManager.php index f8d6e9d..f85cb98 100644 --- a/src/Articus/PathHandler/Consumer/PluginManager.php +++ b/src/Articus/PathHandler/Consumer/PluginManager.php @@ -14,7 +14,7 @@ class PluginManager extends AbstractPluginManager protected $instanceOf = ConsumerInterface::class; protected $factories = [ - Json::class => InvokableFactory::class, + Json::class => Factory\Json::class, Internal::class => InvokableFactory::class, ]; diff --git a/src/Articus/PathHandler/Handler/Factory/PluginManager.php b/src/Articus/PathHandler/Handler/Factory/PluginManager.php new file mode 100644 index 0000000..a43135a --- /dev/null +++ b/src/Articus/PathHandler/Handler/Factory/PluginManager.php @@ -0,0 +1,20 @@ +getServiceConfig($container)); + } +} diff --git a/src/Articus/PathHandler/PluginManager.php b/src/Articus/PathHandler/Handler/PluginManager.php similarity index 81% rename from src/Articus/PathHandler/PluginManager.php rename to src/Articus/PathHandler/Handler/PluginManager.php index 78d1726..f5eb4c8 100644 --- a/src/Articus/PathHandler/PluginManager.php +++ b/src/Articus/PathHandler/Handler/PluginManager.php @@ -1,7 +1,7 @@ getServiceConfig($container), $options ?? []); + $handlerPluginManager = self::getHandlerPluginManager($container); + $cache = self::getCache($container, PH\MetadataProvider\Annotation::CACHE_KEY, $config['cache'] ?? null); + return new PH\MetadataProvider\Annotation($handlerPluginManager, $cache); + } + + protected static function getHandlerPluginManager(ContainerInterface $container): PH\Handler\PluginManager + { + return $container->get(PH\Handler\PluginManager::class); + } +} diff --git a/src/Articus/PathHandler/MetadataProvider/Factory/PhpAttribute.php b/src/Articus/PathHandler/MetadataProvider/Factory/PhpAttribute.php new file mode 100644 index 0000000..e63648d --- /dev/null +++ b/src/Articus/PathHandler/MetadataProvider/Factory/PhpAttribute.php @@ -0,0 +1,30 @@ +getServiceConfig($container), $options ?? []); + $handlerPluginManager = self::getHandlerPluginManager($container); + $cache = self::getCache($container, PH\MetadataProvider\PhpAttribute::CACHE_KEY, $config['cache'] ?? null); + return new PH\MetadataProvider\PhpAttribute($handlerPluginManager, $cache); + } + + protected static function getHandlerPluginManager(ContainerInterface $container): PH\Handler\PluginManager + { + return $container->get(PH\Handler\PluginManager::class); + } +} diff --git a/src/Articus/PathHandler/MetadataProvider/PhpAttribute.php b/src/Articus/PathHandler/MetadataProvider/PhpAttribute.php new file mode 100644 index 0000000..1c67b89 --- /dev/null +++ b/src/Articus/PathHandler/MetadataProvider/PhpAttribute.php @@ -0,0 +1,360 @@ + -> + * @psalm-var array + */ + protected array $handlerClassNames; + + /** + * Map -> + * @psalm-var array + */ + protected array $routes; + + /** + * Map -> -> + * @psalm-var array> + */ + protected array $handlerMethodNames; + + /** + * Map -> -> + * @psalm-var array> + */ + protected array $consumers; + + /** + * Map -> -> + * @psalm-var array> + */ + protected array $attributes; + + /** + * Map -> -> + * @psalm-var array> + */ + protected array $producers; + + /** + * MetadataProvider constructor. + * @param PluginManagerInterface $handlerPluginManager + * @param CacheInterface $cache + */ + public function __construct( + protected PluginManagerInterface $handlerPluginManager, + protected CacheInterface $cache + ) + { + //Restore internal data from cache + [ + $this->handlerClassNames, + $this->routes, + $this->handlerMethodNames, + $this->consumers, + $this->attributes, + $this->producers + ] = $this->cache->get(self::CACHE_KEY) ?? [[], [], [], [], [], []]; + } + + public function __destruct() + { + //Dump updated internal data to cache + if ($this->needCacheUpdate) + { + $this->cache->set( + self::CACHE_KEY, + [ + $this->handlerClassNames, + $this->routes, + $this->handlerMethodNames, + $this->consumers, + $this->attributes, + $this->producers + ] + ); + } + } + + /** + * @inheritdoc + */ + public function getHttpMethods(string $handlerName): array + { + $this->ascertainMetadata($handlerName); + $handlerClassName = $this->handlerClassNames[$handlerName]; + return \array_keys($this->handlerMethodNames[$handlerClassName]); + } + + /** + * @inheritdoc + */ + public function getRoutes(string $handlerName): \Generator + { + $this->ascertainMetadata($handlerName); + $handlerClassName = $this->handlerClassNames[$handlerName]; + yield from ($this->routes[$handlerClassName]); + } + + /** + * @inheritdoc + */ + public function hasConsumers(string $handlerName, string $httpMethod): bool + { + $this->ascertainMetadata($handlerName, $httpMethod); + $handlerClassName = $this->handlerClassNames[$handlerName]; + $handlerMethodName = $this->handlerMethodNames[$handlerClassName][$httpMethod]; + return (!empty($this->consumers[$handlerClassName][$handlerMethodName])); + } + + /** + * @inheritdoc + */ + public function getConsumers(string $handlerName, string $httpMethod): \Generator + { + $this->ascertainMetadata($handlerName, $httpMethod); + $handlerClassName = $this->handlerClassNames[$handlerName]; + $handlerMethodName = $this->handlerMethodNames[$handlerClassName][$httpMethod]; + yield from ($this->consumers[$handlerClassName][$handlerMethodName]); + } + + /** + * @inheritdoc + */ + public function getAttributes(string $handlerName, string $httpMethod): \Generator + { + $this->ascertainMetadata($handlerName, $httpMethod); + $handlerClassName = $this->handlerClassNames[$handlerName]; + $handlerMethodName = $this->handlerMethodNames[$handlerClassName][$httpMethod]; + yield from ($this->attributes[$handlerClassName][$handlerMethodName]); + } + + /** + * @inheritdoc + */ + public function hasProducers(string $handlerName, string $httpMethod): bool + { + $this->ascertainMetadata($handlerName, $httpMethod); + $handlerClassName = $this->handlerClassNames[$handlerName]; + $handlerMethodName = $this->handlerMethodNames[$handlerClassName][$httpMethod]; + return (!empty($this->producers[$handlerClassName][$handlerMethodName])); + } + + /** + * @inheritdoc + */ + public function getProducers(string $handlerName, string $httpMethod): \Generator + { + $this->ascertainMetadata($handlerName, $httpMethod); + $handlerClassName = $this->handlerClassNames[$handlerName]; + $handlerMethodName = $this->handlerMethodNames[$handlerClassName][$httpMethod]; + yield from ($this->producers[$handlerClassName][$handlerMethodName]); + } + + /** + * @inheritdoc + */ + public function executeHandlerMethod(string $handlerName, string $httpMethod, $handler, ServerRequestInterface $request) + { + $this->ascertainMetadata($handlerName, $httpMethod); + $handlerClassName = $this->handlerClassNames[$handlerName]; + $handlerMethodName = $this->handlerMethodNames[$handlerClassName][$httpMethod]; + + //TODO replace with pregenerated code + if (!($handler instanceof $handlerClassName)) + { + throw new \InvalidArgumentException(\sprintf( + 'Invalid handler object: expecting %s, not %s.', + $handlerClassName, + \is_object($handler) ? \get_class($handler) : \gettype($handler) + )); + } + return $handler->{$handlerMethodName}($request); + } + + /** + * Ensures that metadata for specified handler name was loaded + * and optionally checks if this metadata contains information about specified HTTP method + * @param string $handlerName + * @param string|null $httpMethod + */ + protected function ascertainMetadata(string $handlerName, ?string $httpMethod = null): void + { + if (empty($this->handlerClassNames[$handlerName])) + { + try + { + $this->loadMetadata($handlerName); + $this->needCacheUpdate = true; + } + catch (\Throwable $e) + { + //Reset all metadata + [ + $this->handlerClassNames, + $this->routes, + $this->handlerMethodNames, + $this->consumers, + $this->attributes, + $this->producers + ] = [[], [], [], [], [], []]; + throw $e; + } + } + + if (($httpMethod !== null) && empty($this->handlerMethodNames[$this->handlerClassNames[$handlerName]][$httpMethod])) + { + throw new \InvalidArgumentException(\sprintf( + 'Handler %s is not configured to handle %s-method.', $handlerName, $httpMethod + )); + } + } + + /** + * Loads metadata for specified handler name + * @param string $handlerName + */ + protected function loadMetadata(string $handlerName): void + { + $handler = $this->handlerPluginManager->get($handlerName); + if (!\is_object($handler)) + { + throw new \InvalidArgumentException(\sprintf('Handler %s is %s, not object.', $handlerName, \gettype($handler))); + } + $handlerClassName = \get_class($handler); + $this->handlerClassNames[$handlerName] = $handlerClassName; + + $routes = new FastPriorityQueue(); + $handlerMethodNames = []; + $commonConsumers = new FastPriorityQueue(); + $commonAttributes = new FastPriorityQueue(); + $commonProducers = new FastPriorityQueue(); + + $classReflection = new \ReflectionClass($handlerClassName); + + //Process class annotations + foreach ($classReflection->getAttributes() as $phpAttributeReflection) + { + $phpAttribute = match ($phpAttributeReflection->getName()) + { + PHA\Route::class, PHA\Consumer::class, PHA\Attribute::class, PHA\Producer::class => $phpAttributeReflection->newInstance(), + default => null, + }; + switch (true) + { + case ($phpAttribute instanceof PHA\Route): + $routes->insert([$phpAttribute->name, $phpAttribute->pattern, $phpAttribute->defaults], $phpAttribute->priority); + break; + case ($phpAttribute instanceof PHA\Consumer): + $commonConsumers->insert([$phpAttribute->mediaRange, $phpAttribute->name, $phpAttribute->options], $phpAttribute->priority); + break; + case ($phpAttribute instanceof PHA\Attribute): + $commonAttributes->insert([$phpAttribute->name, $phpAttribute->options], $phpAttribute->priority); + break; + case ($phpAttribute instanceof PHA\Producer): + $commonProducers->insert([$phpAttribute->mediaType, $phpAttribute->name, $phpAttribute->options], $phpAttribute->priority); + break; + } + } + if ($routes->isEmpty()) + { + throw new \LogicException(\sprintf('Invalid metadata for %s: no route.', $handlerClassName)); + } + $this->routes[$handlerClassName] = $routes->toArray(); + + //Process public method annotations + foreach ($classReflection->getMethods(\ReflectionMethod::IS_PUBLIC) as $methodReflection) + { + $handlerMethodName = $methodReflection->getName(); + $hasMetadata = false; + $consumers = clone $commonConsumers; + $attributes = clone $commonAttributes; + $producers = clone $commonProducers; + foreach ($methodReflection->getAttributes() as $phpAttributeReflection) + { + $phpAttribute = match ($phpAttributeReflection->getName()) + { + PHA\HttpMethod::class, + PHA\Get::class, + PHA\Post::class, + PHA\Patch::class, + PHA\Put::class, + PHA\Delete::class, + PHA\Consumer::class, + PHA\Attribute::class, + PHA\Producer::class + => $phpAttributeReflection->newInstance(), + default => null, + }; + switch (true) + { + case ($phpAttribute instanceof PHA\HttpMethod): + $httpMethod = $phpAttribute->name; + if (!empty($handlerMethodNames[$httpMethod])) + { + throw new \LogicException(\sprintf( + 'Invalid metadata for %s: both %s and %s are declared to handle %s-method.', + $handlerClassName, + $handlerMethodNames[$httpMethod], + $handlerMethodName, + $httpMethod + )); + } + $handlerMethodNames[$httpMethod] = $handlerMethodName; + $hasMetadata = true; + break; + case ($phpAttribute instanceof PHA\Consumer): + $consumers->insert([$phpAttribute->mediaRange, $phpAttribute->name, $phpAttribute->options], $phpAttribute->priority); + $hasMetadata = true; + break; + case ($phpAttribute instanceof PHA\Attribute): + $attributes->insert([$phpAttribute->name, $phpAttribute->options], $phpAttribute->priority); + $hasMetadata = true; + break; + case ($phpAttribute instanceof PHA\Producer): + $producers->insert([$phpAttribute->mediaType, $phpAttribute->name, $phpAttribute->options], $phpAttribute->priority); + $hasMetadata = true; + break; + } + } + if ($hasMetadata) + { + if ($methodReflection->getNumberOfRequiredParameters() > 1) + { + throw new \LogicException(\sprintf( + 'Invalid method %s with metadata for %s: more than one required parameter.', + $handlerMethodName, + $handlerClassName + )); + } + $this->consumers[$handlerClassName][$handlerMethodName] = $consumers->toArray(); + $this->attributes[$handlerClassName][$handlerMethodName] = $attributes->toArray(); + $this->producers[$handlerClassName][$handlerMethodName] = $producers->toArray(); + } + } + if (empty($handlerMethodNames)) + { + throw new \LogicException(\sprintf('Invalid metadata for %s: no HTTP methods.', $handlerClassName)); + } + $this->handlerMethodNames[$handlerClassName] = $handlerMethodNames; + } +} diff --git a/src/Articus/PathHandler/PhpAttribute/Attribute.php b/src/Articus/PathHandler/PhpAttribute/Attribute.php new file mode 100644 index 0000000..e0a69d5 --- /dev/null +++ b/src/Articus/PathHandler/PhpAttribute/Attribute.php @@ -0,0 +1,29 @@ +getServiceConfig($container)); + } +} diff --git a/src/Articus/PathHandler/RouteInjection/Factory.php b/src/Articus/PathHandler/RouteInjection/Factory.php deleted file mode 100644 index b8fb886..0000000 --- a/src/Articus/PathHandler/RouteInjection/Factory.php +++ /dev/null @@ -1,282 +0,0 @@ - PH\MetadataProvider\Annotation::CACHE_KEY, - PH\Router\FastRoute::class => PH\Router\FastRoute::CACHE_KEY, - ]; - - public function __construct(string $configKey = self::class) - { - parent::__construct($configKey); - } - - /** - * @inheritdoc - * @return RouterInterface - * @throws \Doctrine\Common\Annotations\AnnotationException - * @throws \ReflectionException - */ - public function __invoke(ContainerInterface $container, $requestedName, array $options = null): RouterInterface - { - $options = new Options(\array_merge($this->getServiceConfig($container), $options ?? [])); - - $result = self::getInjectableRouter($container, $options->router); - - $handlerPluginManager = self::getHandlerPluginManager($container, $options->handlers); - $consumerPluginManager = self::getConsumerPluginManager($container, $options->consumers); - $attributePluginManager = self::getAttributePluginManager($container, $options->attributes); - $producerPluginManager = self::getProducerPluginManager($container, $options->producers); - $metadataProvider = self::getMetadataProvider($container, $handlerPluginManager, $options->metadata); - $responseGenerator = self::getResponseGenerator($container); - - //Inject routes - foreach ($options->paths as $pathPrefix => $handlerNames) - { - foreach ($handlerNames as $handlerName) - { - $httpMethods = $metadataProvider->getHttpMethods($handlerName); - foreach ($metadataProvider->getRoutes($handlerName) as [$routeName, $pattern, $defaults]) - { - $middleware = new PH\Middleware( - $handlerName, - $metadataProvider, - $handlerPluginManager, - $consumerPluginManager, - $attributePluginManager, - $producerPluginManager, - $responseGenerator - ); - $route = new Route($pathPrefix . $pattern, $middleware, $httpMethods, $routeName); - if (!empty($defaults)) - { - $route->setOptions(['defaults' => $defaults]); - } - $result->addRoute($route); - } - } - } - - return $result; - } - - /** - * @param ContainerInterface $container - * @param string $cacheAwareClass - * @param $options - * @return CacheInterface - */ - protected static function getCache(ContainerInterface $container, string $cacheAwareClass, $options): CacheInterface - { - $result = null; - switch (true) - { - case ($options === null): - case \is_array($options): - $result = new PH\Cache\DataFilePerKey(self::CACHE_KEYS[$cacheAwareClass], $options['directory'] ?? null); - break; - case (\is_string($options) && $container->has($options)): - $result = $container->get($options); - if (!($result instanceof CacheInterface)) - { - throw new \LogicException(\sprintf('Invalid cache service for "%s".', $cacheAwareClass)); - } - break; - default: - throw new \LogicException(\sprintf('Invalid configuration for "%s" cache.', $cacheAwareClass)); - } - return $result; - } - - /** - * @param ContainerInterface $container - * @param array|string $options - * @return RouterInterface - */ - protected static function getInjectableRouter(ContainerInterface $container, $options): RouterInterface - { - $result = null; - switch (true) - { - case empty($options): - throw new \LogicException('PathHandler router is not configured.'); - case \is_array($options): - $cache = self::getCache($container, PH\Router\FastRoute::class, $options['cache'] ?? []); - $result = new PH\Router\FastRoute($cache); - break; - case (\is_string($options) && $container->has($options)): - $result = $container->get($options); - if (!($result instanceof RouterInterface)) - { - throw new \LogicException('Invalid router for PathHandler.'); - } - break; - default: - throw new \LogicException('Invalid configuration for PathHandler router.'); - } - return $result; - } - - /** - * @param ContainerInterface $container - * @param array|string $options - * @return PluginManagerInterface - */ - protected static function getHandlerPluginManager(ContainerInterface $container, $options): PluginManagerInterface - { - $result = null; - switch (true) - { - case \is_array($options): - $result = new PH\PluginManager($container, $options); - break; - case (\is_string($options) && $container->has($options)): - $result = $container->get($options); - if (!($result instanceof PluginManagerInterface)) - { - throw new \LogicException('Invalid handler plugin manager for PathHandler.'); - } - break; - default: - throw new \LogicException('Invalid configuration for PathHandler handler plugin manager.'); - } - return $result; - } - - /** - * @param ContainerInterface $container - * @param array|string $options - * @return PluginManagerInterface - */ - protected static function getConsumerPluginManager(ContainerInterface $container, $options): PluginManagerInterface - { - $result = null; - switch (true) - { - case \is_array($options): - $result = new PH\Consumer\PluginManager($container, $options); - break; - case (\is_string($options) && $container->has($options)): - $result = $container->get($options); - if (!($result instanceof PluginManagerInterface)) - { - throw new \LogicException('Invalid consumer plugin manager for PathHandler.'); - } - break; - default: - throw new \LogicException('Invalid configuration for PathHandler consumer plugin manager.'); - } - return $result; - } - - /** - * @param ContainerInterface $container - * @param array|string $options - * @return PluginManagerInterface - */ - protected static function getAttributePluginManager(ContainerInterface $container, $options): PluginManagerInterface - { - $result = null; - switch (true) - { - case \is_array($options): - $result = new PH\Attribute\PluginManager($container, $options); - break; - case (\is_string($options) && $container->has($options)): - $result = $container->get($options); - if (!($result instanceof PluginManagerInterface)) - { - throw new \LogicException('Invalid attribute plugin manager for PathHandler.'); - } - break; - default: - throw new \LogicException('Invalid configuration for PathHandler attribute plugin manager.'); - } - return $result; - } - - /** - * @param ContainerInterface $container - * @param array|string $options - * @return PluginManagerInterface - */ - protected static function getProducerPluginManager(ContainerInterface $container, $options): PluginManagerInterface - { - $result = null; - switch (true) - { - case \is_array($options): - $result = new PH\Producer\PluginManager($container, $options); - break; - case (\is_string($options) && $container->has($options)): - $result = $container->get($options); - if (!($result instanceof PluginManagerInterface)) - { - throw new \LogicException('Invalid producer plugin manager for PathHandler.'); - } - break; - default: - throw new \LogicException('Invalid configuration for PathHandler producer plugin manager.'); - } - return $result; - } - - /** - * @param ContainerInterface $container - * @return callable - */ - protected static function getResponseGenerator(ContainerInterface $container): callable - { - $result = $container->get(ResponseInterface::class); - if (!\is_callable($result)) - { - throw new \LogicException('Invalid response generator for PathHandler.'); - } - return $result; - } - - /** - * @param ContainerInterface $container - * @param PluginManagerInterface $handlerPluginManager - * @param $options - * @return PH\MetadataProviderInterface - */ - protected static function getMetadataProvider(ContainerInterface $container, PluginManagerInterface $handlerPluginManager, $options): PH\MetadataProviderInterface - { - $result = null; - switch (true) - { - case empty($options): - throw new \LogicException('PathHandler metadata provider is not configured.'); - case \is_array($options): - $cache = self::getCache($container, PH\MetadataProvider\Annotation::class, $options['cache'] ?? []); - $result = new PH\MetadataProvider\Annotation($handlerPluginManager, $cache); - break; - case (\is_string($options) && $container->has($options)): - $result = $container->get($options); - if (!($result instanceof PH\MetadataProviderInterface)) - { - throw new \LogicException('Invalid metadata provider for PathHandler.'); - } - break; - default: - throw new \LogicException('Invalid configuration for PathHandler metadata provider.'); - } - return $result; - } -} \ No newline at end of file diff --git a/src/Articus/PathHandler/RouteInjection/Options.php b/src/Articus/PathHandler/RouteInjection/Options.php deleted file mode 100644 index d49c224..0000000 --- a/src/Articus/PathHandler/RouteInjection/Options.php +++ /dev/null @@ -1,86 +0,0 @@ - [], - ]; - - /** - * Map -> . - * Each handler name should be available via handler plugin manager. - * @var array Map> - */ - public $paths = []; - - /** - * Configuration for default metadata provider or custom metadata provider service name - * @var array|string - */ - public $metadata = [ - 'cache' => [], - ]; - - /** - * Configuration for default handler plugin manager or custom handler plugin manager service name - * @var array|string - */ - public $handlers = []; - - /** - * Configuration for default consumer plugin manager or custom consumer plugin manager service name - * @var array|string - */ - public $consumers = []; - - /** - * Configuration for default attribute plugin manager or custom attribute plugin manager service name - * @var array|string - */ - public $attributes = []; - - /** - * Configuration for default producer plugin manager or custom producer plugin manager service name - * @var array|string - */ - public $producers = []; - - public function __construct(iterable $options) - { - foreach ($options as $key => $value) - { - switch ($key) - { - case 'router': - $this->router = $value; - break; - case 'paths': - $this->paths = $value; - break; - case 'metadata': - $this->metadata = $value; - break; - case 'handlers': - $this->handlers = $value; - break; - case 'consumers': - $this->consumers = $value; - break; - case 'attributes': - $this->attributes = $value; - break; - case 'producers': - $this->producers = $value; - break; - } - } - } -} \ No newline at end of file diff --git a/src/Articus/PathHandler/RouteInjectionFactory.php b/src/Articus/PathHandler/RouteInjectionFactory.php new file mode 100644 index 0000000..425130b --- /dev/null +++ b/src/Articus/PathHandler/RouteInjectionFactory.php @@ -0,0 +1,130 @@ +getServiceConfig($container), $options ?? []); + + $result = self::getInjectableRouter($container, $config['router'] ?? ['cache' => null]); + + $handlerPluginManager = self::getHandlerManager($container); + $consumerPluginManager = self::getConsumerManager($container); + $attributePluginManager = self::getAttributeManager($container); + $producerPluginManager = self::getProducerManager($container); + $metadataProvider = self::getMetadataProvider($container); + $responseGenerator = self::getResponseGenerator($container); + + //Inject routes + foreach (($config['paths'] ?? []) as $pathPrefix => $handlerNames) + { + foreach ($handlerNames as $handlerName) + { + $httpMethods = $metadataProvider->getHttpMethods($handlerName); + foreach ($metadataProvider->getRoutes($handlerName) as [$routeName, $pattern, $defaults]) + { + $middleware = new Middleware( + $handlerName, + $metadataProvider, + $handlerPluginManager, + $consumerPluginManager, + $attributePluginManager, + $producerPluginManager, + $responseGenerator + ); + $route = new Route($pathPrefix . $pattern, $middleware, $httpMethods, $routeName); + if (!empty($defaults)) + { + $route->setOptions(['defaults' => $defaults]); + } + $result->addRoute($route); + } + } + } + + return $result; + } + + /** + * @param ContainerInterface $container + * @param null|array|string $options + * @return RouterInterface + */ + protected static function getInjectableRouter(ContainerInterface $container, $options): RouterInterface + { + $result = null; + switch (true) + { + case empty($options): + throw new \LogicException('Router is not configured for PathHandler.'); + case \is_array($options): + $cache = self::getCache($container, Router\FastRoute::CACHE_KEY, $options['cache'] ?? []); + $result = new Router\FastRoute($cache); + break; + case (\is_string($options) && $container->has($options)): + $result = $container->get($options); + if (!($result instanceof RouterInterface)) + { + throw new \LogicException('Invalid router service for PathHandler.'); + } + break; + default: + throw new \LogicException('Invalid router configuration for PathHandler.'); + } + return $result; + } + + protected static function getMetadataProvider(ContainerInterface $container): MetadataProviderInterface + { + return $container->get(MetadataProviderInterface::class); + } + + protected static function getHandlerManager(ContainerInterface $container): Handler\PluginManager + { + return $container->get(Handler\PluginManager::class); + } + + protected static function getConsumerManager(ContainerInterface $container): Consumer\PluginManager + { + return $container->get(Consumer\PluginManager::class); + } + + protected static function getAttributeManager(ContainerInterface $container): Attribute\PluginManager + { + return $container->get(Attribute\PluginManager::class); + } + + protected static function getProducerManager(ContainerInterface $container): Producer\PluginManager + { + return $container->get(Producer\PluginManager::class); + } + + protected static function getResponseGenerator(ContainerInterface $container): callable + { + return $container->get(ResponseInterface::class); + } +} \ No newline at end of file