diff --git a/PhpUnit/AbstractContainerBuilderTestCase.php b/PhpUnit/AbstractContainerBuilderTestCase.php index a27d8bd..83dbf75 100644 --- a/PhpUnit/AbstractContainerBuilderTestCase.php +++ b/PhpUnit/AbstractContainerBuilderTestCase.php @@ -129,6 +129,21 @@ protected function assertContainerBuilderHasAlias($aliasId, $expectedServiceId = ); } + /** + * Assert that the ContainerBuilder for this test has a service which is created by other service + * + * @param $serivceId + * @param $expectedFactoryClass + * @param $expectedFactoryMethod + */ + protected function assertContainerBuilderHasCreatedByFactoryService($serviceId, $expectedFactoryClass = null, $expectedFactoryMethod) + { + self::assertThat( + $this->container, + new ContainerBuilderHasFactoryConstraint($serviceId, $expectedFactoryClass, $expectedFactoryMethod) + ); + } + /** * Assert that the ContainerBuilder for this test has a parameter and that its value is the given value. * diff --git a/PhpUnit/ContainerBuilderHasFactoryConstraint.php b/PhpUnit/ContainerBuilderHasFactoryConstraint.php new file mode 100644 index 0000000..3432da3 --- /dev/null +++ b/PhpUnit/ContainerBuilderHasFactoryConstraint.php @@ -0,0 +1,215 @@ +serviceId = $serviceId; + $this->expectedFactoryClass = $expectedFactoryClass; + $this->expectedFactoryMethod = $expectedFactoryMethod; + } + + public function toString() + { + if (null === $this->expectedFactoryClass) { + return sprintf('"%s" has factory', $this->serviceId); + } + + return sprintf('"%s" has factory "@%s:%s"', $this->serviceId, $this->expectedFactoryClass, $this->expectedFactoryMethod); + } + + public function evaluate($other, $description = '', $returnResult = false) + { + if (!($other instanceof ContainerBuilder)) { + throw new \InvalidArgumentException( + 'Expected an instance of Symfony\Component\DependencyInjection\ContainerBuilder' + ); + } + + if (!$this->evaluateServiceId($other, $returnResult)) { + return false; + } + + if (!$this->evaluateFactory($other, $returnResult)) { + return false; + } + + if ($this->expectedFactoryClass !== null && !$this->evaluateFactoryClass($other, $returnResult)) { + return false; + } + + return true; + } + + private function evaluateServiceId(ContainerBuilder $containerBuilder, $returnResult) + { + if (!$containerBuilder->hasDefinition($this->serviceId)) { + if ($returnResult) { + return false; + } + + $this->fail( + $containerBuilder, + sprintf( + 'The container builder has no service "%s"', + $this->serviceId + ) + ); + } + + return true; + } + + private function evaluateFactory(ContainerBuilder $containerBuilder, $returnResult) + { + /** @var Definition */ + $definition = $containerBuilder->getDefinition($this->serviceId); + + $factory = $this->getFactoryData($definition); + + if (!is_array($factory)) { + if ($returnResult) { + return false; + } + + $this->fail( + $containerBuilder, + sprintf( + 'The container builder has service "%s" with not "%s" factory', + $this->serviceId, + $this->expectedFactoryClass + ) + ); + } + + return true; + } + + private function evaluateFactoryClass(ContainerBuilder $containerBuilder, $returnResult) + { + /** @var Definition */ + $definition = $containerBuilder->getDefinition($this->serviceId); + + $factory = $this->getFactoryData($definition); + + list($factoryDefinition, $factoryMethod) = $factory; + + if ($factoryDefinition instanceof Reference) { + $factoryClass = (string)$factoryDefinition; + } elseif (is_string($factoryDefinition)) { + $factoryClass = $factoryDefinition; + } else { + if ($returnResult) { + return false; + } + + $this->fail( + $containerBuilder, + sprintf( + 'The container builder has service "%s" with not service "%s" factory', + $this->serviceId, + $this->expectedFactoryClass + ) + ); + } + + $constraint = new IsEqual($this->expectedFactoryClass); + if (!$constraint->evaluate($factoryClass, '', true)) { + if ($returnResult) { + return false; + } + + $this->fail( + $containerBuilder, + sprintf( + 'The container builder has service "%s" with not service class "%s" factory', + $this->serviceId, + $this->expectedFactoryClass + ) + ); + } + + if ($this->expectedFactoryMethod) { + $constraint = new IsEqual($this->expectedFactoryMethod); + if (!$constraint->evaluate($factoryMethod, '', true)) { + if ($returnResult) { + return false; + } + + $this->fail( + $containerBuilder, + sprintf( + 'The container builder has service "%s" with not service class method "%s::%s" factory', + $this->serviceId, + $this->expectedFactoryClass, + $this->expectedFactoryMethod + ) + ); + } + } + + return true; + } + + private function getFactoryData(Definition $definition) + { + if (self::isLegacySymfonyDI()) { + $factoryService = $definition->getFactoryService(); + $factoryMethod = $definition->getFactoryMethod(); + $factoryClass = $definition->getFactoryClass(); + if (!$factoryService && !$factoryClass) { + return null; + } + + return array( $factoryClass ? $factoryClass : $factoryService, $factoryMethod ); + } else { + $factory = $definition->getFactory(); + if (is_array($factory)) { + return $factory; + } + + if (is_string($factory) && false !== strpos($factory, ':')) { + return preg_split('/:/', $factory, 2); + } + + return $factory; + } + } + + + public static function isLegacySymfonyDI() + { + return !method_exists(Definition::class, 'getFactory') && + method_exists(Definition::class, 'getFactoryService'); + } +} diff --git a/README.md b/README.md index db8f41d..0ce36d9 100644 --- a/README.md +++ b/README.md @@ -301,6 +301,10 @@ These are the available semantic assertions for each of the test cases shown abo
Assert that the ContainerBuilder for this test has an alias.
assertContainerBuilderHasAlias($aliasId, $expectedServiceId)
Assert that the ContainerBuilder for this test has an alias and that it is an alias for the given service id.
+
assertContainerBuilderHasCreatedByFactoryService($serviceId)
+
Assert that the ContainerBuilder for this test has an service with factory.
+
assertContainerBuilderHasCreatedByFactoryService($serviceId,$factoryService, $factoryMethod)
+
Assert that the ContainerBuilder for this test has an service with factory for given factory service and method.
assertContainerBuilderHasParameter($parameterName)
Assert that the ContainerBuilder for this test has a parameter.
assertContainerBuilderHasParameter($parameterName, $expectedParameterValue)
diff --git a/Tests/Fixtures/MatthiasDependencyInjectionTestExtension.php b/Tests/Fixtures/MatthiasDependencyInjectionTestExtension.php index ea13f15..6a315be 100644 --- a/Tests/Fixtures/MatthiasDependencyInjectionTestExtension.php +++ b/Tests/Fixtures/MatthiasDependencyInjectionTestExtension.php @@ -5,8 +5,11 @@ use Symfony\Component\Config\FileLocator; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Definition; +use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\DependencyInjection\Extension\ExtensionInterface; +use Symfony\Component\DependencyInjection\Loader\YamlFileLoader; use Symfony\Component\DependencyInjection\Loader\XmlFileLoader; +use Matthias\SymfonyDependencyInjectionTest\PhpUnit\ContainerBuilderHasFactoryConstraint; class MatthiasDependencyInjectionTestExtension implements ExtensionInterface { @@ -16,6 +19,17 @@ public function load(array $config, ContainerBuilder $container) $loader = new XmlFileLoader($container, new FileLocator(__DIR__)); $loader->load('services.xml'); + // load factory services definitions + if (ContainerBuilderHasFactoryConstraint::isLegacySymfonyDI()) { + $loader->load('services-factory-legacy.xml'); + } else { + $loader->load('services-factory.xml'); + + // Load old syntax for services in YML files + $ymlLoader = new YamlFileLoader($container, new FileLocator(__DIR__)); + $ymlLoader->load('services-factory-old-syntax.yml'); + } + // set a parameter manually $container->setParameter('manual_parameter', 'parameter value'); @@ -31,6 +45,22 @@ public function load(array $config, ContainerBuilder $container) // add an alias to an existing service $container->setAlias('manual_alias', 'service_id'); + + // add an factory service + $container->register('manual_factory_service', 'stdClass'); + + if (ContainerBuilderHasFactoryConstraint::isLegacySymfonyDI()) { + $container + ->register('manual_created_by_factory_service', 'stdClass') + ->setFactoryService(new Reference('manual_factory_service')) + ->setFactoryMethod('factoryMethod'); + ; + } else { + $container + ->register('manual_created_by_factory_service', 'stdClass') + ->setFactory('manual_factory_service:factoryMethod') + ; + } } public function getAlias() diff --git a/Tests/Fixtures/services-factory-legacy.xml b/Tests/Fixtures/services-factory-legacy.xml new file mode 100644 index 0000000..736cb9e --- /dev/null +++ b/Tests/Fixtures/services-factory-legacy.xml @@ -0,0 +1,12 @@ + + + + + + + + + + diff --git a/Tests/Fixtures/services-factory-old-syntax.yml b/Tests/Fixtures/services-factory-old-syntax.yml new file mode 100644 index 0000000..7544846 --- /dev/null +++ b/Tests/Fixtures/services-factory-old-syntax.yml @@ -0,0 +1,3 @@ +services: + created_with_factory_with_old_syntax: + factory: ['@factory_service', 'factoryMethod'] diff --git a/Tests/Fixtures/services-factory.xml b/Tests/Fixtures/services-factory.xml new file mode 100644 index 0000000..5037246 --- /dev/null +++ b/Tests/Fixtures/services-factory.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + diff --git a/Tests/Fixtures/services.xml b/Tests/Fixtures/services.xml index 9abc74b..84a5ce4 100644 --- a/Tests/Fixtures/services.xml +++ b/Tests/Fixtures/services.xml @@ -26,5 +26,6 @@ + diff --git a/Tests/PhpUnit/AbstractExtensionTestCaseTest.php b/Tests/PhpUnit/AbstractExtensionTestCaseTest.php index f24215d..9475b1d 100644 --- a/Tests/PhpUnit/AbstractExtensionTestCaseTest.php +++ b/Tests/PhpUnit/AbstractExtensionTestCaseTest.php @@ -3,6 +3,7 @@ namespace Matthias\DependencyInjectionTests\Test\DependencyInjection; use Matthias\SymfonyDependencyInjectionTest\PhpUnit\AbstractExtensionTestCase; +use Matthias\SymfonyDependencyInjectionTest\PhpUnit\ContainerBuilderHasFactoryConstraint; use Matthias\SymfonyDependencyInjectionTest\Tests\Fixtures\MatthiasDependencyInjectionTestExtension; use PHPUnit\Framework\ExpectationFailedException; @@ -28,6 +29,9 @@ public function if_load_is_successful_it_does_not_fail() // defined in services.xml $this->assertContainerBuilderHasSyntheticService('synthetic_service'); + // defined in services-factory.xml + $this->assertContainerBuilderHasCreatedByFactoryService('created_by_factory_service', 'factory_service', 'factoryMethod'); + // manually defined parameter $this->assertContainerBuilderHasParameter('manual_parameter', 'parameter value'); // Just check parameter exists, value will not be checked. @@ -49,8 +53,24 @@ public function if_load_is_successful_it_does_not_fail() // check for existence of manually created arguments, not checking values. $this->assertContainerBuilderHasServiceDefinitionWithArgument('manual_service_id', 0); $this->assertContainerBuilderHasServiceDefinitionWithArgument('manual_service_id', 1); + + // manually created factory service + $this->assertContainerBuilderHasCreatedByFactoryService( + 'manual_created_by_factory_service', + 'manual_factory_service', + 'factoryMethod' + ); + + if (!ContainerBuilderHasFactoryConstraint::isLegacySymfonyDI()) { + $this->assertContainerBuilderHasCreatedByFactoryService( + 'created_with_factory_with_old_syntax', + 'factory_service', + 'factoryMethod' + ); + } } + /** * @test */ diff --git a/Tests/PhpUnit/ContainerBuilderHasFactoryConstraintTest.php b/Tests/PhpUnit/ContainerBuilderHasFactoryConstraintTest.php new file mode 100644 index 0000000..6f7b0b9 --- /dev/null +++ b/Tests/PhpUnit/ContainerBuilderHasFactoryConstraintTest.php @@ -0,0 +1,131 @@ +assertSame($shouldMatch, $constraint->evaluate($containerBuilder, null, true)); + } + + public function containerBuilderProvider() + { + $rightServiceId = 'some_service_id'; + $wrongServiceId = 'other_service_id'; + $factoryClass = 'factory_class_service'; + $invalidFactoryClass = 'invalid_class_service'; + $factoryMethod = 'someMethod'; + $invalidFactoryMethod = 'invalidMethod'; + + $emptyContainerBuilder = new ContainerBuilder(); + + $builderWithFactory = new ContainerBuilder(); + $factoryReference = new Reference($factoryClass); + + if (ContainerBuilderHasFactoryConstraint::isLegacySymfonyDI()) { + $builderWithFactory->register($rightServiceId) + ->setFactoryService($factoryReference) + ->setFactoryMethod($factoryMethod); + } else { + $builderWithFactory->register($rightServiceId) + ->setFactory([$factoryReference,$factoryMethod]); + } + + $builderWithFactory->register($wrongServiceId); + + return array( + // the container does not have the service + array($emptyContainerBuilder, $rightServiceId, null, null, false), + + // the container has service created by factory + array($builderWithFactory, $rightServiceId, null, null, true ), + + // the container has service, but they has not factory + array($builderWithFactory, $wrongServiceId, null, null, false ), + + // the container has service created by factory, but factory is invalid + array($builderWithFactory, $rightServiceId, $invalidFactoryClass, $factoryMethod, false ), + + // the container has service created by factory, but factory method is invalid + array($builderWithFactory, $rightServiceId, $factoryClass, $invalidFactoryMethod, false ), + + // the container has service created by factory, and whole arguments are valid + array($builderWithFactory, $rightServiceId, $factoryClass, $factoryMethod, true ), + + // the container has service created by factory, and whole arguments are valid + array($builderWithFactory, $wrongServiceId, $factoryClass, $factoryMethod, false ), + + ); + } + + /** + * @test + */ + public function it_has_a_string_representation() + { + $serviceId = 'service_id'; + $factoryClass = 'SomeFactoryClass'; + $factoryMethod = 'someMethod'; + $constraint = new ContainerBuilderHasFactoryConstraint($serviceId, $factoryClass, $factoryMethod); + $this->assertSame( + '"service_id" has factory "@SomeFactoryClass:someMethod"', + $constraint->toString() + ); + } + + /** + * @test + */ + public function it_expects_a_string_for_service_id() + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('string'); + + new ContainerBuilderHasFactoryConstraint(new \stdClass(), 'service_id'); + } + + /** + * @test + */ + public function it_expects_a_string_for_factory_class() + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('string'); + + new ContainerBuilderHasFactoryConstraint('service_id', new \stdClass(), ''); + } + + /** + * @test + */ + public function it_expects_a_string_for_factory_method() + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('string'); + + new ContainerBuilderHasFactoryConstraint('service_id', 'FactoryClass', new \stdClass()); + } + + /** + * @test + */ + public function it_expects_factory_class_be_informed() + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('inform'); + + new ContainerBuilderHasFactoryConstraint('service_id', null, 'factoryMethod'); + } +}