From 8b3d927abbe29c81c992f0d700f5f21d0280a35d Mon Sep 17 00:00:00 2001 From: Chris Smith Date: Tue, 16 Jan 2024 16:15:36 +0000 Subject: [PATCH] Automatically extract fixture dependencies MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit It is now no longer required to declare groups on all dependent fixtures, the fixture loader will now automatically find the required dependencies and load them without them being explicitly added to the groups being loaded. Before you were forced to declare groups all the way down the dependency chain, for example: ``` TopLevelFixture (groups: default) ├─ SecondLevelFixture1 (groups: default, alternative) │ ├─ ThirdLevelFixture1 (groups: default, alternative) ├─ SecondLevelFixture2 (groups: default) │ ├─ ThridLevelFixture2 (groups: default) │ ├─ ThirdLevelFixture3 (groups: default, alternative) ``` Now you only need to specify the groups at the highest level: ``` TopLevelFixture (groups: default) ├─ SecondLevelFixture1 (groups: alternative) │ ├─ ThirdLevelFixture1 (groups: none) ├─ SecondLevelFixture2 (groups: none) │ ├─ ThridLevelFixture2 (groups: none) │ ├─ ThirdLevelFixture3 (groups: alternative) ``` In both examples the groups `default` and `alternative` will load the same fixtures. Adds an additional PHPStan error to the baseline which appears to be caused by the backwards compatibility layer. --- phpstan-baseline.neon | 5 ++ src/Loader/SymfonyFixturesLoader.php | 49 ++++++++++--------- .../WithDeepDependenciesFixtures.php | 34 +++++++++++++ .../DataFixtures/WithDependenciesFixtures.php | 2 +- tests/IntegrationTest.php | 23 ++++++--- 5 files changed, 82 insertions(+), 31 deletions(-) create mode 100644 tests/Fixtures/FooBundle/DataFixtures/WithDeepDependenciesFixtures.php diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 6d42dee..a948ea6 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -4,6 +4,11 @@ parameters: - tests/IntegrationTest.php ignoreErrors: + - + message: "#^Call to an undefined method Doctrine\\\\Bundle\\\\FixturesBundle\\\\Loader\\\\SymfonyFixturesLoader\\:\\:getFixture\\(\\)\\.$#" + count: 1 + path: src/Loader/SymfonyFixturesLoader.php + - message: "#^Call to an undefined static method Doctrine\\\\Bundle\\\\FixturesBundle\\\\Loader\\\\SymfonyBridgeLoader\\:\\:addFixture\\(\\)\\.$#" count: 1 diff --git a/src/Loader/SymfonyFixturesLoader.php b/src/Loader/SymfonyFixturesLoader.php index 82b9d81..17bb73a 100644 --- a/src/Loader/SymfonyFixturesLoader.php +++ b/src/Loader/SymfonyFixturesLoader.php @@ -10,9 +10,8 @@ use Doctrine\Common\DataFixtures\FixtureInterface; use LogicException; use ReflectionClass; -use RuntimeException; -use function array_key_exists; +use function array_keys; use function array_values; use function get_class; use function sprintf; @@ -99,19 +98,22 @@ public function getFixtures(array $groups = []): array return $fixtures; } - $filteredFixtures = []; - foreach ($fixtures as $fixture) { - foreach ($groups as $group) { - $fixtureClass = get_class($fixture); - if (isset($this->groupsFixtureMapping[$group][$fixtureClass])) { - $filteredFixtures[$fixtureClass] = $fixture; - continue 2; - } + $requiredFixtures = []; + foreach ($groups as $group) { + if (! isset($this->groupsFixtureMapping[$group])) { + continue; } + + $requiredFixtures += $this->collectDependencies(...array_keys($this->groupsFixtureMapping[$group])); } - foreach ($filteredFixtures as $fixture) { - $this->validateDependencies($filteredFixtures, $fixture); + $filteredFixtures = []; + foreach ($fixtures as $order => $fixture) { + $fixtureClass = get_class($fixture); + if (isset($requiredFixtures[$fixtureClass])) { + $filteredFixtures[$order] = $fixture; + continue; + } } return array_values($filteredFixtures); @@ -130,22 +132,25 @@ private function addGroupsFixtureMapping(string $className, array $groups): void } /** - * @param string[] $fixtures An array of fixtures with class names as keys + * Collect any dependent fixtures from the given classes. * - * @throws RuntimeException + * @psalm-return array */ - private function validateDependencies(array $fixtures, FixtureInterface $fixture): void + private function collectDependencies(string ...$fixtureClass): array { - if (! $fixture instanceof DependentFixtureInterface) { - return; - } + $dependencies = []; - $dependenciesClasses = $fixture->getDependencies(); + foreach ($fixtureClass as $class) { + $dependencies[$class] = true; + $fixture = $this->getFixture($class); - foreach ($dependenciesClasses as $class) { - if (! array_key_exists($class, $fixtures)) { - throw new RuntimeException(sprintf('Fixture "%s" was declared as a dependency for fixture "%s", but it was not included in any of the loaded fixture groups.', $class, get_class($fixture))); + if (! $fixture instanceof DependentFixtureInterface) { + continue; } + + $dependencies += $this->collectDependencies(...$fixture->getDependencies()); } + + return $dependencies; } } diff --git a/tests/Fixtures/FooBundle/DataFixtures/WithDeepDependenciesFixtures.php b/tests/Fixtures/FooBundle/DataFixtures/WithDeepDependenciesFixtures.php new file mode 100644 index 0000000..06ffbfe --- /dev/null +++ b/tests/Fixtures/FooBundle/DataFixtures/WithDeepDependenciesFixtures.php @@ -0,0 +1,34 @@ +assertCount(0, $loader->getFixtures(['group3'])); } - public function testLoadFixturesViaGroupWithMissingDependency(): void + public function testLoadFixturesViaGroupWithDependenciesNotInGroup(): void { $kernel = new IntegrationTestKernel('dev', true); $kernel->addServices(static function (ContainerBuilder $c): void { - // has a "staging" group via the getGroups() method $c->autowire(OtherFixtures::class) ->addTag(FixturesCompilerPass::FIXTURE_TAG); - // no getGroups() method $c->autowire(WithDependenciesFixtures::class) ->addTag(FixturesCompilerPass::FIXTURE_TAG); + $c->autowire(WithDeepDependenciesFixtures::class) + ->addTag(FixturesCompilerPass::FIXTURE_TAG); + + $c->autowire(RequiredConstructorArgsFixtures::class) + ->setArgument('$fooRequiredArg', 'test') + ->addTag(FixturesCompilerPass::FIXTURE_TAG); + + $c->autowire(DependentOnRequiredConstructorArgsFixtures::class) + ->addTag(FixturesCompilerPass::FIXTURE_TAG); + $c->setAlias('test.doctrine.fixtures.loader', new Alias('doctrine.fixtures.loader', true)); }); $kernel->boot(); @@ -193,10 +202,8 @@ public function testLoadFixturesViaGroupWithMissingDependency(): void $loader = $container->get('test.doctrine.fixtures.loader'); $this->assertInstanceOf(SymfonyFixturesLoader::class, $loader); - $this->expectException(RuntimeException::class); - $this->expectExceptionMessage('Fixture "Doctrine\Bundle\FixturesBundle\Tests\Fixtures\FooBundle\DataFixtures\OtherFixtures" was declared as a dependency for fixture "Doctrine\Bundle\FixturesBundle\Tests\Fixtures\FooBundle\DataFixtures\WithDependenciesFixtures", but it was not included in any of the loaded fixture groups.'); - - $loader->getFixtures(['missingDependencyGroup']); + self::assertCount(2, $loader->getFixtures(['groupWithDependencies'])); + self::assertCount(5, $loader->getFixtures(['groupWithDeepDependencies'])); } public function testLoadFixturesViaGroupWithFulfilledDependency(): void