diff --git a/CHANGELOG.md b/CHANGELOG.md index c9cd625..100f240 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Added support for PHP 8.3 - Added support for Shopware 6.6 +- Added `--dry` option to all fixture load commands + - This option will prevent the fixtures from being executed but still prints all fixtures it would execute ### Removed - Dropped support for PHP 8.1 diff --git a/src/Command/LoadSingleFixtureCommand.php b/src/Command/LoadSingleFixtureCommand.php index 303ef90..b42c354 100644 --- a/src/Command/LoadSingleFixtureCommand.php +++ b/src/Command/LoadSingleFixtureCommand.php @@ -5,6 +5,7 @@ namespace Basecom\FixturePlugin\Command; use Basecom\FixturePlugin\FixtureLoader; +use Basecom\FixturePlugin\FixtureOption; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; @@ -25,6 +26,7 @@ public function __construct( protected function configure(): void { $this->addOption('with-dependencies', 'w', InputOption::VALUE_NONE, 'Run fixture with dependencies') + ->addOption('dry', description: 'Only list fixtures that would run without executing them') ->addArgument('fixtureName', InputArgument::REQUIRED, 'Name of Fixture to load'); } @@ -32,24 +34,32 @@ protected function execute(InputInterface $input, OutputInterface $output): int { $io = new SymfonyStyle($input, $output); - $io->title('Running a single fixture'); + /** @var string $fixtureName */ + $fixtureName = $input->getArgument('fixtureName'); + $dry = (bool) ($input->getOption('dry') ?? false); + $withDependencies = (bool) ($input->getOption('with-dependencies') ?? false); - $groupNameInput = $input->getArgument('fixtureName'); - - if (!\is_string($groupNameInput)) { + if (!\is_string($fixtureName)) { $io->error('Please make sure that your argument is of type string'); return Command::FAILURE; } - $withDependencies = $input->getOption('with-dependencies'); - if (!\is_bool($withDependencies)) { - $io->error('Please make sure that your argument is of type boolean'); + $io->title("Running single fixture: {$fixtureName}"); - return Command::FAILURE; + if ($dry) { + $io->note('[INFO] Dry run mode enabled. No fixtures will be executed.'); } - $this->loader->runSingle($io, $groupNameInput, $withDependencies); + $options = new FixtureOption( + dryMode: $dry, + fixtureNames: [$fixtureName], + withDependencies: $withDependencies + ); + + if (!$this->loader->run($options, $io)) { + return Command::FAILURE; + } $io->success('Done!'); diff --git a/src/FixtureLoader.php b/src/FixtureLoader.php index 4dd61fe..f8d61b5 100644 --- a/src/FixtureLoader.php +++ b/src/FixtureLoader.php @@ -9,13 +9,22 @@ class FixtureLoader { private readonly array $fixtures; - private array $fixtureReference; public function __construct(\Traversable $fixtures) { $this->fixtures = iterator_to_array($fixtures); } + /** + * This method runs the fixtures. What fixtures are executed and with what logic + * can be configured using the FixtureOption object. + * + * Generally speaking the following options are available: + * - $dryMode: If set to true, the fixtures will not be executed (only printed) + * - $groupName: If set, only fixtures with the given group name will be executed + * - $fixtureNames: If set, only fixtures with the given class name will be executed + * - $withDependencies: If set to true, all dependencies of the fixtures will be executed as well + */ public function run(FixtureOption $option, ?SymfonyStyle $io = null): bool { $fixtures = $this->prefilterFixtures($option); @@ -30,11 +39,25 @@ public function run(FixtureOption $option, ?SymfonyStyle $io = null): bool return false; } + if ($option->withDependencies) { + $references = $this->buildFixtureReference($this->recursiveGetAllDependenciesOfFixtures($fixtures)); + } + $this->runFixtures($option, $references, $io); return true; } + /** + * The prefilterFixtures method is responsible for filtering the fixtures based on the provided + * FixtureOption object. It takes into account the group name and fixture names specified in the + * FixtureOption. If a group name is provided, it filters the fixtures to include only those + * belonging to the specified group. If fixture names are provided, it filters the fixtures to + * include only those whose class names match the provided fixture names. The method returns + * the filtered array of fixtures. + * + * @return array + */ private function prefilterFixtures(FixtureOption $option): array { $fixtures = $this->fixtures; @@ -47,40 +70,29 @@ private function prefilterFixtures(FixtureOption $option): array ); } - return $fixtures; - } - - public function runSingle(SymfonyStyle $io, string $fixtureName, bool $withDependencies = false): void - { - foreach ($this->fixtures as $fixture) { - $className = $fixture::class ?: ''; - - if (!str_contains(strtolower($className), strtolower($fixtureName))) { - continue; - } - - $io->note('Fixture '.$className.' found and will be loaded.'); - - if (!$withDependencies) { - $bag = new FixtureBag(); - $fixture->load($bag); - - return; - } - - $this->fixtureReference = $this->buildFixtureReference($this->fixtures); - $this->runFixtures(new FixtureOption(), array_merge(array_map( - fn (string $fixtureClass) => $this->fixtureReference[$fixtureClass], - $this->recursiveGetAllDependenciesOfFixture($fixture) - ), [$fixture]), $io); + if (!empty($option->fixtureNames)) { + $fixtures = array_filter( + $fixtures, + static function (Fixture $fixture) use ($option) { + $fqcn = $fixture::class; + $className = substr(strrchr($fqcn, '\\') ?: '', 1); - return; + return \in_array($className, $option->fixtureNames, true); + } + ); } - $io->comment('No Fixture with name '.$fixtureName.' found'); + return $fixtures; } /** + * Checks that all dependencies of the fixtures are in the same group. + * + * This method iterates over each fixture in the provided fixture references and checks if the fixture has any dependencies. + * If it does, it checks if these dependencies are in the same group as the fixture. + * If not, it returns false, indicating that not all dependencies are in the same group. + * If all dependencies are in the same group, it returns true. + * * @param array $fixtureReferences */ private function checkThatAllDependenciesAreInGroup( @@ -89,12 +101,10 @@ private function checkThatAllDependenciesAreInGroup( ?SymfonyStyle $io = null ): bool { foreach ($fixtureReferences as $fixture) { - // If fixture doesn't have any dependencies, skip the check. if (\count($fixture->dependsOn()) <= 0) { continue; } - // Check if dependencies of fixture are in the same group. if (!$this->checkDependenciesAreInSameGroup($fixture, $fixtureReferences, $groupName, $io)) { return false; } @@ -127,6 +137,11 @@ private function checkDependenciesAreInSameGroup( } /** + * This method actually executed the given fixtures array. It sorts all fixtures by dependencies + * and priority. This method will also run a correction loop to detect circular dependencies. + * + * If the dryMode option is set to true, the fixtures will not be executed, only printed. + * * @param array $fixtures */ private function runFixtures(FixtureOption $option, array $fixtures, ?SymfonyStyle $io = null): void @@ -149,13 +164,60 @@ private function runFixtures(FixtureOption $option, array $fixtures, ?SymfonySty } } - private function recursiveGetAllDependenciesOfFixture(Fixture $fixture): array + /** + * Recursively retrieves all dependencies of the given fixtures. + * + * This method iterates over each fixture in the provided array and recursively fetches all of its dependencies. + * It uses the `recursiveGetAllDependenciesOfFixture` method to get the dependencies of each individual fixture. + * The result is a unique array of all dependencies for the entire set of fixtures. + * + * @param array $fixtures + * + * @return array + */ + private function recursiveGetAllDependenciesOfFixtures(array $fixtures): array + { + $allFixtures = $this->buildFixtureReference($this->fixtures); + + $keys = []; + foreach ($fixtures as $fixture) { + $keys = array_merge($keys, $this->recursiveGetAllDependenciesOfFixture($fixture, $allFixtures)); + } + + $keys = array_unique($keys); + + return array_merge( + $fixtures, + array_map( + static fn (string $key) => $allFixtures[$key], + $keys + ) + ); + } + + /** + * Recursively retrieves all dependencies of the given fixture and returns them as an array. + * The array contains the FQCN of all the dependency fixtures. + * + * @param array $allFixtures + * + * @return array + */ + private function recursiveGetAllDependenciesOfFixture(Fixture $fixture, array $allFixtures): array { - return array_unique(array_merge($fixture->dependsOn(), array_reduce($fixture->dependsOn(), function ($carry, $item) { - return array_merge($carry, $this->recursiveGetAllDependenciesOfFixture($this->fixtureReference[$item])); + return array_unique(array_merge($fixture->dependsOn(), array_reduce($fixture->dependsOn(), function ($carry, $item) use ($allFixtures) { + return array_merge($carry, $this->recursiveGetAllDependenciesOfFixture($allFixtures[$item], $allFixtures)); }, []))); } + /** + * Restructures a normal array with numeric keys to an associative array with the class name as key + * and the fixture object as value. + * + * @param array $fixtures + * + * @return array + */ private function buildFixtureReference(array $fixtures): array { $result = []; @@ -168,6 +230,8 @@ private function buildFixtureReference(array $fixtures): array } /** + * Sort all fixtures by priority. + * * @param array $fixtures * * @return array @@ -183,6 +247,9 @@ private function sortAllByPriority(array $fixtures): array } /** + * Sort all fixtures by dependencies. This makes sure that fixtures with dependencies are executed after their + * dependencies. + * * @param array $fixtures * * @return array @@ -197,6 +264,10 @@ private function buildDependencyTree(array $fixtures): array return $fixtures; } + /** + * A comparison function to sort fixtures by dependencies. This function is used in the uasort function + * to sort fixtures by dependencies. + */ private function compareDependencies(Fixture $a, Fixture $b): int { $aDependsOnB = \in_array($b::class, $a->dependsOn(), true); @@ -210,6 +281,15 @@ private function compareDependencies(Fixture $a, Fixture $b): int } /** + * The runCorrectionLoop method is responsible for detecting circular dependencies in the fixtures. + * It iterates over the fixtures and their dependencies and checks if there are any circular dependencies. + * If a circular dependency is detected, it throws an exception. If no circular dependencies are detected, + * it returns the fixtures array. + * + * The method takes an array of fixtures and the number of tries as arguments. The number of tries is used + * to prevent an infinite loop in case of a circular dependency. If the number of tries reaches zero, the method + * throws an exception. (Indicating that a circular dependency was detected) + * * @param array $fixtures * * @return array @@ -230,6 +310,10 @@ private function runCorrectionLoop(array $fixtures, int $tries): array } foreach ($fixture->dependsOn() as $dependent) { + if (!\array_key_exists($dependent, $fixtures)) { + continue; + } + if (\in_array($fixtures[$dependent], $existing, true)) { continue; } diff --git a/src/FixtureOption.php b/src/FixtureOption.php index c86ccb0..2e03787 100644 --- a/src/FixtureOption.php +++ b/src/FixtureOption.php @@ -8,7 +8,9 @@ { public function __construct( public bool $dryMode = false, - public ?string $groupName = null + public ?string $groupName = null, + public array $fixtureNames = [], + public bool $withDependencies = false, ) { } }