From 82f0500a2adc30b45377eaee88ce783022686a3a Mon Sep 17 00:00:00 2001 From: Nils Weigel Date: Thu, 2 May 2024 13:51:13 +0200 Subject: [PATCH] Fixtures as Services (#12) * moved fixture loading out of the command and split into various services to provide various extension points * renamed fixture::load to create to ease the upgrade path * renamed fixture::load to create to ease the upgrade path * updated readme, added notes on migration and using events * updated readme, updated notes on migration * applied review suggestions, added AfterExecuteFixture event * Apply suggestions from code review Co-authored-by: Luka Dschaak * updated README.md for new event * Some improvements for #12 (#14) * Run GitHub workflows for all PRs * Finalize everything * Prefer lists to arrays * Separate interfaces * Inline ::class calls * Use public properties in events * Fix templating * Add missing "@return $this" annotations * Move attribute below annotation * Use instanceof instead of is_a() * Make exception name parameters required * Make $fixtureLocator parameter of FixtureLoader private * Improve output of LoadFixturesCommand * Remove unused config node * Remove unused parameters * Strip "Interface" suffix * Rename DependentFixture interface to HasDependencies and FixtureGroup interface to HasGroups * renamed exception CircularFixtureDependency * do not enable profile if we are uncertain it was enabled before * throw exception if requested fixture was not found * Feature/load single fixture cli command (#16) * fixed load all fixtures command description and help * added load single fixture command * added load singular fixture command * updated changelog --------- Co-authored-by: Luka Dschaak Co-authored-by: Jacob Dreesen --- .github/workflows/qa.yaml | 1 - .github/workflows/tests.yaml | 1 - CHANGELOG.md | 8 +- README.md | 210 ++++++++---------- config/services.yaml | 38 +++- phpstan-baseline.neon | 5 - src/Command/LoadFixtureCommand.php | 68 ++++++ src/Command/LoadFixturesCommand.php | 95 ++------ src/DependencyInjection/Configuration.php | 3 +- .../NeustaPimcoreFixtureExtension.php | 10 +- ...WasCreated.php => AfterExecuteFixture.php} | 7 +- src/Event/AfterLoadFixtures.php | 16 ++ src/Event/BeforeExecuteFixture.php | 14 ++ src/Event/BeforeLoadFixtures.php | 16 ++ src/Event/CreateFixture.php | 16 -- src/EventListener/ExecutionTimesListener.php | 37 --- src/EventListener/ExecutionTreeListener.php | 25 --- src/EventListener/PimcoreLoadOptimization.php | 55 +++++ src/EventListener/ProfilerDisabler.php | 38 ++++ .../SortFixturesByDependencyBeforeLoad.php | 32 +++ src/Executor/Executor.php | 10 + src/Executor/PimcoreFixtureExecutor.php | 13 ++ src/Factory/FixtureFactory.php | 143 ------------ .../FixtureInstantiator.php | 22 -- .../FixtureInstantiatorForAll.php | 33 --- ...nstantiatorForParametrizedConstructors.php | 81 ------- src/Fixture.php | 10 - src/Fixture/AbstractFixture.php | 53 +++++ src/Fixture/Fixture.php | 8 + src/Fixture/HasDependencies.php | 18 ++ src/Fixture/HasGroups.php | 18 ++ src/FixtureLoader/FixtureLoader.php | 49 ++++ src/FixtureLoader/SelectiveFixtureLoader.php | 31 +++ src/Locator/AllFixturesLocator.php | 21 ++ src/Locator/FixtureLocator.php | 13 ++ src/Locator/FixtureNotFound.php | 19 ++ src/Locator/FixturesGroupLocator.php | 77 +++++++ src/Locator/NamedFixtureLocator.php | 67 ++++++ .../ObjectReferenceRepository.php | 68 ++++++ src/Sorter/CircularFixtureDependency.php | 14 ++ src/Sorter/FixtureDependencySorter.php | 76 +++++++ src/Sorter/UnresolvedFixtureDependency.php | 14 ++ tests/Fixtures/DependantFixture.php | 23 -- tests/Fixtures/FixtureWithDependency.php | 15 -- tests/Fixtures/SomeFixture.php | 15 -- .../Functional/Factory/FixtureFactoryTest.php | 140 ------------ tests/Locator/FixturesGroupLocatorTest.php | 69 ++++++ tests/Locator/NamedFixtureLocatorTest.php | 69 ++++++ ...ockDependentFixtureWithoutDependencies.php | 19 ++ tests/Mock/MockFixtureAlphaDependsOnBeta.php | 21 ++ tests/Mock/MockFixtureBetaDependsOnGamma.php | 21 ++ tests/Mock/MockFixtureDependsOnItself.php | 21 ++ tests/Mock/MockFixtureGammaDependsOnAlpha.php | 21 ++ tests/Mock/MockFixtureOfGroupRed.php | 21 ++ tests/Mock/MockFixtureOneDependsOnTwo.php | 21 ++ tests/Mock/MockFixtureTwoDependsOnOne.php | 21 ++ tests/Mock/MockFixtureWithoutDependencies.php | 13 ++ tests/Sorter/FixtureDependencySorterTest.php | 129 +++++++++++ 58 files changed, 1413 insertions(+), 779 deletions(-) create mode 100644 src/Command/LoadFixtureCommand.php rename src/Event/{FixtureWasCreated.php => AfterExecuteFixture.php} (55%) create mode 100644 src/Event/AfterLoadFixtures.php create mode 100644 src/Event/BeforeExecuteFixture.php create mode 100644 src/Event/BeforeLoadFixtures.php delete mode 100644 src/Event/CreateFixture.php delete mode 100644 src/EventListener/ExecutionTimesListener.php delete mode 100644 src/EventListener/ExecutionTreeListener.php create mode 100644 src/EventListener/PimcoreLoadOptimization.php create mode 100644 src/EventListener/ProfilerDisabler.php create mode 100644 src/EventListener/SortFixturesByDependencyBeforeLoad.php create mode 100644 src/Executor/Executor.php create mode 100644 src/Executor/PimcoreFixtureExecutor.php delete mode 100644 src/Factory/FixtureFactory.php delete mode 100644 src/Factory/FixtureInstantiator/FixtureInstantiator.php delete mode 100644 src/Factory/FixtureInstantiator/FixtureInstantiatorForAll.php delete mode 100644 src/Factory/FixtureInstantiator/FixtureInstantiatorForParametrizedConstructors.php delete mode 100644 src/Fixture.php create mode 100644 src/Fixture/AbstractFixture.php create mode 100644 src/Fixture/Fixture.php create mode 100644 src/Fixture/HasDependencies.php create mode 100644 src/Fixture/HasGroups.php create mode 100644 src/FixtureLoader/FixtureLoader.php create mode 100644 src/FixtureLoader/SelectiveFixtureLoader.php create mode 100644 src/Locator/AllFixturesLocator.php create mode 100644 src/Locator/FixtureLocator.php create mode 100644 src/Locator/FixtureNotFound.php create mode 100644 src/Locator/FixturesGroupLocator.php create mode 100644 src/Locator/NamedFixtureLocator.php create mode 100644 src/ReferenceRepository/ObjectReferenceRepository.php create mode 100644 src/Sorter/CircularFixtureDependency.php create mode 100644 src/Sorter/FixtureDependencySorter.php create mode 100644 src/Sorter/UnresolvedFixtureDependency.php delete mode 100644 tests/Fixtures/DependantFixture.php delete mode 100644 tests/Fixtures/FixtureWithDependency.php delete mode 100644 tests/Fixtures/SomeFixture.php delete mode 100644 tests/Functional/Factory/FixtureFactoryTest.php create mode 100644 tests/Locator/FixturesGroupLocatorTest.php create mode 100644 tests/Locator/NamedFixtureLocatorTest.php create mode 100644 tests/Mock/MockDependentFixtureWithoutDependencies.php create mode 100644 tests/Mock/MockFixtureAlphaDependsOnBeta.php create mode 100644 tests/Mock/MockFixtureBetaDependsOnGamma.php create mode 100644 tests/Mock/MockFixtureDependsOnItself.php create mode 100644 tests/Mock/MockFixtureGammaDependsOnAlpha.php create mode 100644 tests/Mock/MockFixtureOfGroupRed.php create mode 100644 tests/Mock/MockFixtureOneDependsOnTwo.php create mode 100644 tests/Mock/MockFixtureTwoDependsOnOne.php create mode 100644 tests/Mock/MockFixtureWithoutDependencies.php create mode 100644 tests/Sorter/FixtureDependencySorterTest.php diff --git a/.github/workflows/qa.yaml b/.github/workflows/qa.yaml index 1d4c5b1..aba50cd 100644 --- a/.github/workflows/qa.yaml +++ b/.github/workflows/qa.yaml @@ -4,7 +4,6 @@ on: push: branches: [ "main" ] pull_request: - branches: [ "main" ] workflow_dispatch: schedule: - cron: "10 4 * * 2" # Every Tuesday at 4:10 AM UTC diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index d9caadd..657adb5 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -4,7 +4,6 @@ on: push: branches: [ "main" ] pull_request: - branches: [ "main" ] workflow_dispatch: schedule: - cron: "10 4 * * 2" # Every Tuesday at 4:10 AM UTC diff --git a/CHANGELOG.md b/CHANGELOG.md index 86fc4ad..6bf9808 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog -## next +## 2.0.0 + +- Feature: Fixtures as Services (#12) + +See [Upgrading from earlier Version](README.md#upgrading-from-earlier-version) for upgrade instructions + +## 1.0.0 - feature: update bundle structure diff --git a/README.md b/README.md index 87c22a2..cadae5c 100644 --- a/README.md +++ b/README.md @@ -20,20 +20,43 @@ It can be useful for testing purposes, or for seeding a database with initial da Neusta\Pimcore\FixtureBundle\NeustaPimcoreFixtureBundle::class => ['test' => true], ``` +### Upgrading from earlier Version + +Fixtures are now considered actual services and are loaded through Dependency Injection (DI). +To align with this approach, +you'll need to update your Fixture classes by moving service dependencies from the `create` method to the constructor. +If your Fixture relies on other Fixtures, implement the `HasDependencies` interface. + +Here are the key changes: + +1. **Fixture Interface Update** + The old fixture interface `Neusta\Pimcore\FixtureBundle\Fixture` has been replaced with `Neusta\Pimcore\FixtureBundle\Fixture\Fixture`. You can also extend from `Neusta\Pimcore\FixtureBundle\Fixture\AbstractFixture` to implement your Fixtures. + +2. **Change in `create` Method** + The signature of the `create` method has been modified. It no longer takes any arguments, meaning all service dependencies must be specified via Dependency Injection. This is typically done through the constructor. + +3. **Fixtures as Services** + Fixtures must be made available in the Dependency Injection container to be discovered. To do this, tag them with `neusta_pimcore_fixture.fixture`, or use autoconfiguration for automatic tagging. + +4. **Specifying Inter-Fixture Dependencies** + If your Fixture depends on others, use the `HasDependencies` interface to specify these dependencies. Additional guidance is available in the section "[Referencing Fixtures and Depending on Other Fixtures](#referencing-fixtures-and-depending-on-other-fixtures)". + +Make sure to update your Fixture classes according to these changes to ensure proper functionality and compatibility with this Bundle. + ## Usage ### Writing Fixtures -Data fixtures are PHP classes where you create objects and persist them to the database. +Data fixtures are PHP service classes where you create objects and persist them to the database. Imagine that you want to add some `Product` objects to your database. To do this, create a fixture class and start adding products: ```php -use Neusta\Pimcore\FixtureBundle\Fixture; +use Neusta\Pimcore\FixtureBundle\Fixture\AbstractFixture; use Pimcore\Model\DataObject\Product; -final class ProductFixture implements Fixture +final class ProductFixture extends AbstractFixture { public function create(): void { @@ -50,38 +73,82 @@ final class ProductFixture implements Fixture } ``` -### Loading Fixtures +### Referencing Fixtures and Depending on Other Fixtures + +Suppose you want to link a `Product` fixture to a `Group` fixture. To do this, you need to create a `Group` fixture first and keep a reference to it. Later, you can use this reference when creating the `Product` fixture. + +This process requires the `Group` fixture to exist before the `Product` fixture. You can achieve this ordering by implementing the `HasDependencies` interface. + +```php +use Neusta\Pimcore\FixtureBundle\Fixture\AbstractFixture; +use Pimcore\Model\DataObject\ProductGroup; + +final class ProductGroupFixture extends AbstractFixture +{ + public function create(): void + { + $productGroup = new ProductGroup(); + $productGroup->setParentId(0); + $productGroup->setPublished(true); + $productGroup->setKey('My Product Group'); + $productGroup->save(); + + $this->addReference('my-product-group', $productGroup); + } +} +``` + +```php +use Neusta\Pimcore\FixtureBundle\Fixture\AbstractFixture; +use Neusta\Pimcore\FixtureBundle\Fixture\HasDependencies; +use Pimcore\Model\DataObject\Product; +use Pimcore\Model\DataObject\ProductGroup; + +final class ProductFixture extends AbstractFixture implements HasDependencies +{ + public function create(): void + { + $productGroup = $this->getReference('my-product-group', ProductGroup::class); + + $product = new Product(); + $product->setParentId(0); + $product->setPublished(true); + $product->setKey('My grouped Product'); + $product->setProductGroup($productGroup); + $product->save(); + } -To use fixtures in tests, a few preparations must be made. + public function getDependencies(): array + { + return [ + ProductGroupFixture::class, + ]; + } +} +``` -Currently, the `FixtureFactory` still has to be instantiated manually. -The easiest way to do this is with a project-specific kernel base class. +### Loading Fixtures + +To load fixtures in Tests, we offer the `SelectiveFixtureLoader`. To streamline your test setup, we recommend creating a base class with a method to load fixtures via the `SelectiveFixtureLoader`. Here's an example demonstrating how to implement this. ```php -use Neusta\Pimcore\FixtureBundle\Factory\FixtureFactory; -use Neusta\Pimcore\FixtureBundle\Factory\FixtureInstantiator\FixtureInstantiatorForAll; -use Neusta\Pimcore\FixtureBundle\Factory\FixtureInstantiator\FixtureInstantiatorForParametrizedConstructors; use Neusta\Pimcore\FixtureBundle\Fixture; use Pimcore\Test\KernelTestCase; abstract class BaseKernelTestCase extends KernelTestCase { - protected FixtureFactory $fixtureFactory; - - /** @param list> $fixtures */ + /** + * @param list> $fixtures + */ protected function importFixtures(array $fixtures): void { - $this->fixtureFactory ??= (new FixtureFactory([ - new FixtureInstantiatorForParametrizedConstructors(static::getContainer()), - new FixtureInstantiatorForAll(), - ])); - - $this->fixtureFactory->createFixtures($fixtures); + /** @var SelectiveFixtureLoader $fixtureLoader */ + $fixtureLoader = static::getContainer()->get(SelectiveFixtureLoader::class); + $fixtureLoader->setFixturesToLoad($fixtures)->loadFixtures(); } protected function tearDown(): void { - unset($this->fixtureFactory); \Pimcore\Cache::clearAll(); \Pimcore::collectGarbage(); @@ -113,104 +180,23 @@ final class MyCustomTest extends BaseKernelTestCase ### Accessing Services from the Fixtures -Sometimes you may need to access your application's services inside a fixture class. -You can use normal dependency injection for this: - -> [!IMPORTANT] -> You need to create your `FixtureFactory` with the `FixtureInstantiatorForParametrizedConstructors` for this to work! - -```php -final class SomeFixture implements Fixture -{ - public function __construct( - private Something $something, - ) { - } - - public function create(): void - { - // ... use $this->something - } -} -``` - -### Depending on Other Fixtures - -In a fixture, you can depend on other fixtures. -Therefore, you have to reference them in your `create()` method as parameters. - -> [!IMPORTANT] -> All parameters of the `create()` method in your fixtures may *only* reference other fixtures. -> Everything else is not allowed! - -Referencing other fixtures ensures they are created before this one. - -This also allows accessing some state of the other fixtures. - -```php -final class SomeFixture implements Fixture -{ - public function create(OtherFixture $otherFixture): void - { - // do something with $otherFixture->someInformation - } -} - -final class OtherFixture implements Fixture -{ - public string $someInformation; - - public function create(): void - { - $this->someInformation = 'some information created in this fixture'; - } -} -``` - -The state can also be accessed from the tests: - -```php -use Neusta\Pimcore\FixtureBundle\Fixture; -use Pimcore\Model\DataObject\Product; - -final class ProductFixture implements Fixture -{ - public int $productId; +As the Fixtures are just normal PHP Services you can use all DI features like constructor, setter or property injection as usual. - public function create(): void - { - $product = new Product(); - $product->setParentId(0); - $product->setPublished(true); - $product->setKey("Product Fixture"); - // ... +### Extension and customization through Events - $product->save(); +The Bundle provides the following events to facilitate extensions and customization: - $this->productId = $product->getId(); - } -} -``` +1. **`BeforeLoadFixtures`** + This event is triggered before any fixture is executed. It contains all the fixtures that are scheduled for execution, accessible via `$event->getFixtures()`. You can alter the list of fixtures to be loaded by using `$event->setFixtures(...)`. -```php -use Pimcore\Model\DataObject; - -final class MyCustomTest extends BaseKernelTestCase -{ - /** @test */ - public function some_product_test(): void - { - $this->importFixtures([ - ProductFixture::class, - ]); +2. **`AfterLoadFixtures`** + This event occurs after all relevant fixtures have been executed. It carries the fixtures that have been successfully loaded, which can be accessed through `$event->loadedFixtures`. - $productFixture = $this->fixtureFactory->getFixture(ProductFixture::class); - $product = DataObject::getById($productFixture->productId); +3. **`BeforeExecuteFixture`** + This event is triggered just before a fixture is executed. Using this event, you can prevent the execution of a specific fixture by setting `$event->setPreventExecution(true)`. - self::assertNotNull($product); - } -} -``` +3. **`AfterExecuteFixture`** + This event occurs after a fixture has been executed. ## Contribution diff --git a/config/services.yaml b/config/services.yaml index 712f67c..7711ac5 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -1,14 +1,21 @@ services: + _defaults: + autowire: true + autoconfigure: true + bind: + $allFixtures: !tagged_iterator neusta_pimcore_fixture.fixture - Symfony\Component\DependencyInjection\ContainerInterface: '@service_container' + Neusta\Pimcore\FixtureBundle\Command\: + resource: '../src/Command/*' - Neusta\Pimcore\FixtureBundle\Command\LoadFixturesCommand: - arguments: - $container: '@service_container' - $eventDispatcher: '@event_dispatcher' - $fixtureClass: !abstract defined in extension - tags: - - { name: 'console.command' } + Neusta\Pimcore\FixtureBundle\EventListener\: + resource: '../src/EventListener/*' + + Neusta\Pimcore\FixtureBundle\Executor\: + resource: '../src/Executor/*' + + Neusta\Pimcore\FixtureBundle\FixtureLoader\: + resource: '../src/FixtureLoader/*' Neusta\Pimcore\FixtureBundle\Helper\AssetHelper: arguments: @@ -21,3 +28,18 @@ services: Neusta\Pimcore\FixtureBundle\Helper\DocumentHelper: arguments: $prefix: !abstract defined in extension + + Neusta\Pimcore\FixtureBundle\Locator\: + resource: '../src/Locator/*' + + Neusta\Pimcore\FixtureBundle\FixtureLoader\FixtureLoader: + arguments: + $fixtureLocator: '@Neusta\Pimcore\FixtureBundle\Locator\AllFixturesLocator' + + Neusta\Pimcore\FixtureBundle\FixtureLoader\SelectiveFixtureLoader: + arguments: + $fixtureLocator: '@Neusta\Pimcore\FixtureBundle\Locator\NamedFixtureLocator' + public: true + + Neusta\Pimcore\FixtureBundle\ReferenceRepository\: + resource: '../src/ReferenceRepository/*' diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 7975d90..bc1ba4e 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -1,10 +1,5 @@ parameters: ignoreErrors: - - - message: "#^Call to an undefined method Symfony\\\\Component\\\\Console\\\\Output\\\\OutputInterface\\:\\:section\\(\\)\\.$#" - count: 1 - path: src/Command/LoadFixturesCommand.php - - message: "#^Cannot call method getId\\(\\) on Pimcore\\\\Model\\\\DataObject\\\\Classificationstore\\\\GroupConfig\\|null\\.$#" count: 1 diff --git a/src/Command/LoadFixtureCommand.php b/src/Command/LoadFixtureCommand.php new file mode 100644 index 0000000..0a03ad0 --- /dev/null +++ b/src/Command/LoadFixtureCommand.php @@ -0,0 +1,68 @@ +setHelp( + <<<'EOF' + The %command.name% command takes a single fixture and loads it. + That fixture itself may of course depend on further fixtures, thus allowing to build up + an entire fixture hierarchy to load. + The result is sample data in your Pimcore instance. + + To define the fixture to load, it's fully qualified class name has to be provided. + e.g. %command.name% 'MyProduct\SomeBundle\TheAwesomeFixture' + + Important: This command must not be executed twice. On a second execution, + this command would fail, because the keys of the Pimcore data objects are already present. + To re-run the command, please reset the database first. + EOF + ); + + $this->addArgument('fixture', InputArgument::REQUIRED, 'The Fixture to load'); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $fixture = $input->getArgument('fixture'); + + $output->writeln('Loading fixture'); + $output->writeln(''); + + $this->eventDispatcher->addListener(BeforeExecuteFixture::class, function (BeforeExecuteFixture $event) use ($output) { + $output->writeln(sprintf( + ' - Loading %s', + $event->fixture::class + )); + }); + + $this->fixtureLoader->setFixturesToLoad([$fixture])->loadFixtures(); + + $output->writeln('Loading fixtures completed.'); + + return Command::SUCCESS; + } +} diff --git a/src/Command/LoadFixturesCommand.php b/src/Command/LoadFixturesCommand.php index e8875d3..e97ea43 100644 --- a/src/Command/LoadFixturesCommand.php +++ b/src/Command/LoadFixturesCommand.php @@ -2,37 +2,23 @@ namespace Neusta\Pimcore\FixtureBundle\Command; -use Neusta\Pimcore\FixtureBundle\Event\CreateFixture; -use Neusta\Pimcore\FixtureBundle\Event\FixtureWasCreated; -use Neusta\Pimcore\FixtureBundle\EventListener\ExecutionTimesListener; -use Neusta\Pimcore\FixtureBundle\EventListener\ExecutionTreeListener; -use Neusta\Pimcore\FixtureBundle\Factory\FixtureFactory; -use Neusta\Pimcore\FixtureBundle\Factory\FixtureInstantiator\FixtureInstantiatorForAll; -use Neusta\Pimcore\FixtureBundle\Factory\FixtureInstantiator\FixtureInstantiatorForParametrizedConstructors; -use Neusta\Pimcore\FixtureBundle\Fixture; +use Neusta\Pimcore\FixtureBundle\Event\BeforeExecuteFixture; +use Neusta\Pimcore\FixtureBundle\FixtureLoader\FixtureLoader; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; -use Symfony\Component\Console\Helper\Table; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; -use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\EventDispatcher\EventDispatcherInterface; #[AsCommand( name: 'neusta:pimcore-fixtures:load', - description: 'Loads a defined fixture class.', + description: 'Loads all defined fixture classes.', )] -class LoadFixturesCommand extends Command +final class LoadFixturesCommand extends Command { - private OutputInterface $output; - - /** - * @param class-string $fixtureClass - */ public function __construct( - private ContainerInterface $container, - private EventDispatcherInterface $eventDispatcher, - private string $fixtureClass, + private readonly FixtureLoader $fixtureLoader, + private readonly EventDispatcherInterface $eventDispatcher, ) { parent::__construct(); } @@ -41,19 +27,8 @@ protected function configure(): void { $this->setHelp( <<<'EOF' - The %command.name% command takes a single fixture and loads it. - That fixture itself may of course depend on further fixtures, thus allowing to build up - an entire fixture hierarchy to load. - The result is sample data in your Pimcore instance. - - Use -v, --verbose to output the time the fixtures took to create and to show an ordered list - of executed fixtures. - - php %command.name% -v + The %command.name% command loads all available fixtures. - Important: This command is currently very limited. The fixture that is - loaded is static at the moment. Its dependency hierarchy containing further fixtures must be extended - when new fixtures are created and need to be loaded as well. Important: This command must not be executed twice. On a second execution, this command would fail, because the keys of the Pimcore data objects are already present. To re-run the command, please reset the database first. @@ -63,57 +38,17 @@ protected function configure(): void protected function execute(InputInterface $input, OutputInterface $output): int { - $this->output = $output; - - $output->writeln('Loading fixture'); + $output->writeln('Loading fixtures'); $output->writeln(''); - $instantiators = [ - new FixtureInstantiatorForParametrizedConstructors($this->container), - new FixtureInstantiatorForAll(), - ]; - - if (OutputInterface::VERBOSITY_VERBOSE === $this->output->getVerbosity()) { - $executionTimesListener = new ExecutionTimesListener(); - $executionTreeListener = new ExecutionTreeListener(); - $this->eventDispatcher->addListener(CreateFixture::class, $executionTimesListener->onCreateFixture(...)); - $this->eventDispatcher->addListener(FixtureWasCreated::class, $executionTimesListener->onFixtureWasCreated(...)); - $this->eventDispatcher->addListener(CreateFixture::class, $executionTreeListener->onCreateFixture(...)); - } - - $fixtureFactory = new FixtureFactory($instantiators, $this->eventDispatcher); - $fixtureFactory->createFixtures([$this->fixtureClass]); - - if (isset($executionTimesListener, $executionTreeListener)) { - $executionTimes = $executionTimesListener->getExecutionTimes(); - $executionTimes['All together'] = $executionTimesListener->getTotalTime(); - $convertedExecutionTimes = array_map( - // some magic formatting '0.02' => ' 0.020' - static fn ($value, $key) => [$key, str_pad(sprintf('%.3f', $value), 9, ' ', \STR_PAD_LEFT)], - $executionTimes, - array_keys($executionTimes), - ); - - $output->writeln('Execution times of the fixtures:'); - - $table = new Table($output->section()); - $table->setHeaders(['Fixture', 'Time in s']); - $table->setRows($convertedExecutionTimes); - $table->render(); - - $output->writeln('When the duration is 0.000,' - . ' it will likely mean that the fixture has already been executed as a dependency of another fixture.'); - $output->writeln(''); - - $executionTree = $executionTreeListener->getExecutionTree(); - $output->writeln('Execution tree of the fixtures:'); + $this->eventDispatcher->addListener(BeforeExecuteFixture::class, function (BeforeExecuteFixture $event) use ($output) { + $output->writeln(sprintf( + ' - Loading %s', + $event->fixture::class + )); + }); - foreach ($executionTree as ['fixtureClass' => $fixtureClass, 'level' => $level]) { - $prefix = str_pad(' ', $level * 2); - $output->writeln($prefix . $fixtureClass); - } - $output->writeln(''); - } + $this->fixtureLoader->loadFixtures(); $output->writeln('Loading fixtures completed.'); diff --git a/src/DependencyInjection/Configuration.php b/src/DependencyInjection/Configuration.php index 6cd90f1..ebbdcd5 100644 --- a/src/DependencyInjection/Configuration.php +++ b/src/DependencyInjection/Configuration.php @@ -6,7 +6,7 @@ use Symfony\Component\Config\Definition\Builder\TreeBuilder; use Symfony\Component\Config\Definition\ConfigurationInterface; -class Configuration implements ConfigurationInterface +final class Configuration implements ConfigurationInterface { public function getConfigTreeBuilder(): TreeBuilder { @@ -15,7 +15,6 @@ public function getConfigTreeBuilder(): TreeBuilder $rootNode ->children() - ->scalarNode('fixture_class')->defaultValue(null)->end() ->scalarNode('asset_base_path')->defaultValue(null)->end() ->scalarNode('data_object_base_path')->defaultValue(null)->end() ->scalarNode('document_base_path')->defaultValue(null)->end() diff --git a/src/DependencyInjection/NeustaPimcoreFixtureExtension.php b/src/DependencyInjection/NeustaPimcoreFixtureExtension.php index 6c0ca72..986f3d1 100644 --- a/src/DependencyInjection/NeustaPimcoreFixtureExtension.php +++ b/src/DependencyInjection/NeustaPimcoreFixtureExtension.php @@ -3,7 +3,7 @@ namespace Neusta\Pimcore\FixtureBundle\DependencyInjection; -use Neusta\Pimcore\FixtureBundle\Command\LoadFixturesCommand; +use Neusta\Pimcore\FixtureBundle\Fixture\Fixture; use Neusta\Pimcore\FixtureBundle\Helper\AssetHelper; use Neusta\Pimcore\FixtureBundle\Helper\DataObjectHelper; use Neusta\Pimcore\FixtureBundle\Helper\DocumentHelper; @@ -12,7 +12,7 @@ use Symfony\Component\DependencyInjection\Loader\YamlFileLoader; use Symfony\Component\HttpKernel\DependencyInjection\ConfigurableExtension; -class NeustaPimcoreFixtureExtension extends ConfigurableExtension +final class NeustaPimcoreFixtureExtension extends ConfigurableExtension { /** * @param array $mergedConfig @@ -22,9 +22,6 @@ protected function loadInternal(array $mergedConfig, ContainerBuilder $container $loader = new YamlFileLoader($container, new FileLocator(\dirname(__DIR__, 2) . '/config')); $loader->load('services.yaml'); - $definition = $container->getDefinition(LoadFixturesCommand::class); - $definition->setArgument('$fixtureClass', $mergedConfig['fixture_class']); - $definition = $container->getDefinition(AssetHelper::class); $definition->setArgument('$prefix', $mergedConfig['asset_base_path']); @@ -33,5 +30,8 @@ protected function loadInternal(array $mergedConfig, ContainerBuilder $container $definition = $container->getDefinition(DocumentHelper::class); $definition->setArgument('$prefix', $mergedConfig['document_base_path']); + + $container->registerForAutoconfiguration(Fixture::class) + ->addTag('neusta_pimcore_fixture.fixture'); } } diff --git a/src/Event/FixtureWasCreated.php b/src/Event/AfterExecuteFixture.php similarity index 55% rename from src/Event/FixtureWasCreated.php rename to src/Event/AfterExecuteFixture.php index a16f199..3711620 100644 --- a/src/Event/FixtureWasCreated.php +++ b/src/Event/AfterExecuteFixture.php @@ -1,11 +1,10 @@ - $loadedFixtures + */ + public function __construct( + public readonly array $loadedFixtures, + ) { + } +} diff --git a/src/Event/BeforeExecuteFixture.php b/src/Event/BeforeExecuteFixture.php new file mode 100644 index 0000000..135885e --- /dev/null +++ b/src/Event/BeforeExecuteFixture.php @@ -0,0 +1,14 @@ + $fixtures + */ + public function __construct( + public array $fixtures, + ) { + } +} diff --git a/src/Event/CreateFixture.php b/src/Event/CreateFixture.php deleted file mode 100644 index 71a0027..0000000 --- a/src/Event/CreateFixture.php +++ /dev/null @@ -1,16 +0,0 @@ - */ - private array $executionTimes = []; - private float $currentTime; - - public function onCreateFixture(CreateFixture $event): void - { - $this->currentTime = microtime(true); - } - - public function onFixtureWasCreated(FixtureWasCreated $event): void - { - $this->executionTimes[$event->fixture::class] = microtime(true) - $this->currentTime; - } - - /** - * @return array - */ - public function getExecutionTimes(): array - { - return $this->executionTimes; - } - - public function getTotalTime(): float - { - return array_sum($this->executionTimes); - } -} diff --git a/src/EventListener/ExecutionTreeListener.php b/src/EventListener/ExecutionTreeListener.php deleted file mode 100644 index 916534c..0000000 --- a/src/EventListener/ExecutionTreeListener.php +++ /dev/null @@ -1,25 +0,0 @@ - */ - private array $executionTree = []; - - public function onCreateFixture(CreateFixture $event): void - { - $this->executionTree[] = ['fixtureClass' => $event->class, 'level' => $event->level]; - } - - /** - * @return list - */ - public function getExecutionTree(): array - { - return $this->executionTree; - } -} diff --git a/src/EventListener/PimcoreLoadOptimization.php b/src/EventListener/PimcoreLoadOptimization.php new file mode 100644 index 0000000..609bc72 --- /dev/null +++ b/src/EventListener/PimcoreLoadOptimization.php @@ -0,0 +1,55 @@ + 'beforeCommand', + AfterLoadFixtures::class => 'afterCommand', + ]; + } + + public function beforeCommand(): void + { + $this->versionEnabled = Version::isEnabled(); + $this->cacheEnabled = Cache::isEnabled(); + Version::disable(); + Cache::disable(); + + $this->originalSqlLogger = Db::getConnection()->getConfiguration()->getSQLLogger(); + Db::getConnection()->getConfiguration()->setSQLLogger(null); + + $this->profilerDisabler->disable(); + } + + public function afterCommand(): void + { + Db::getConnection()->getConfiguration()->setSQLLogger($this->originalSqlLogger); + + $this->versionEnabled && Version::enable(); + $this->cacheEnabled && Cache::enable(); + } +} diff --git a/src/EventListener/ProfilerDisabler.php b/src/EventListener/ProfilerDisabler.php new file mode 100644 index 0000000..c3492ad --- /dev/null +++ b/src/EventListener/ProfilerDisabler.php @@ -0,0 +1,38 @@ +profiler?->disable(); + } + + public function enable(): void + { + $this->profiler?->enable(); + } +} diff --git a/src/EventListener/SortFixturesByDependencyBeforeLoad.php b/src/EventListener/SortFixturesByDependencyBeforeLoad.php new file mode 100644 index 0000000..8053b22 --- /dev/null +++ b/src/EventListener/SortFixturesByDependencyBeforeLoad.php @@ -0,0 +1,32 @@ + $allFixtures + */ + public function __construct( + private readonly \Traversable $allFixtures, + ) { + } + + public static function getSubscribedEvents(): array + { + return [ + BeforeLoadFixtures::class => 'sortFixturesByDependency', + ]; + } + + public function sortFixturesByDependency(BeforeLoadFixtures $event): void + { + $event->fixtures = (new FixtureDependencySorter(iterator_to_array($this->allFixtures, false))) + ->sort($event->fixtures); + } +} diff --git a/src/Executor/Executor.php b/src/Executor/Executor.php new file mode 100644 index 0000000..d4d39f5 --- /dev/null +++ b/src/Executor/Executor.php @@ -0,0 +1,10 @@ +create(); + } +} diff --git a/src/Factory/FixtureFactory.php b/src/Factory/FixtureFactory.php deleted file mode 100644 index ff2cef9..0000000 --- a/src/Factory/FixtureFactory.php +++ /dev/null @@ -1,143 +0,0 @@ -, Fixture> */ - private array $instances = []; - - /** - * @param list $instantiators - */ - public function __construct( - private array $instantiators, - private ?EventDispatcherInterface $eventDispatcher = null, - ) { - } - - /** - * @param list> $fixtures - */ - public function createFixtures(array $fixtures): void - { - $versionEnabled = Version::isEnabled(); - $cacheEnabled = Cache::isEnabled(); - Version::disable(); - Cache::disable(); - - foreach ($fixtures as $fixtureClass) { - $this->ensureIsFixture($fixtureClass); - $this->createFixture($fixtureClass, 0); - } - - $versionEnabled && Version::enable(); - $cacheEnabled && Cache::enable(); - } - - /** - * @template T of Fixture - * - * @param class-string $fixture - * - * @return T - * - * @throws \RuntimeException when the fixture hasn't been created - */ - public function getFixture(string $fixture): Fixture - { - if (!isset($this->instances[$fixture])) { - throw new \RuntimeException(sprintf('Fixture "%s" has not been created.', $fixture)); - } - - $instance = $this->instances[$fixture]; - \assert($instance instanceof $fixture); - - return $instance; - } - - /** - * @param class-string $fixtureClass - */ - private function createFixture(string $fixtureClass, int $level): void - { - if (isset($this->instances[$fixtureClass])) { - return; - } - - $this->eventDispatcher?->dispatch(new CreateFixture($fixtureClass, $level)); - - $this->instances[$fixtureClass] = $this->instantiateFixture($fixtureClass); - - $args = []; - foreach ($this->getDependencies($fixtureClass, 'create') as $dependentFixtureClass) { - $this->createFixture($dependentFixtureClass, $level + 1); - $args[] = $this->instances[$dependentFixtureClass]; - } - - $this->instances[$fixtureClass]->create(...$args); - - $this->eventDispatcher?->dispatch(new FixtureWasCreated($this->instances[$fixtureClass])); - } - - /** - * @param class-string $fixtureClass - */ - private function instantiateFixture(string $fixtureClass): Fixture - { - foreach ($this->instantiators as $instantiator) { - if ($instantiator->supports($fixtureClass)) { - return $instantiator->instantiate($fixtureClass); - } - } - - throw new \RuntimeException(sprintf( - 'No instantiator found that is able to instantiate fixtures of type "%s".', - $fixtureClass, - )); - } - - /** - * @return list> - */ - private function getDependencies(string $fixtureClass, string $methodName): iterable - { - foreach ((new \ReflectionMethod($fixtureClass, $methodName))->getParameters() as $parameter) { - $type = $parameter->getType(); - - if (!$type instanceof \ReflectionNamedType) { - throw new \LogicException(sprintf( - 'Parameter "$%s" of %s::%s() has an invalid type.', - $parameter->getName(), - // @phpstan-ignore-next-line this is a method parameter, so it always has a class - $parameter->getDeclaringClass()->getName(), - $parameter->getDeclaringFunction()->getName(), - )); - } - - $this->ensureIsFixture($type->getName()); - - yield $type->getName(); - } - } - - /** @phpstan-assert class-string $fixtureClass */ - private function ensureIsFixture(string $fixtureClass): void - { - if (!is_a($fixtureClass, Fixture::class, true)) { - throw new \InvalidArgumentException(sprintf( - 'Expected "%s" to implement "%s", but it does not.', - $fixtureClass, - Fixture::class, - )); - } - } -} diff --git a/src/Factory/FixtureInstantiator/FixtureInstantiator.php b/src/Factory/FixtureInstantiator/FixtureInstantiator.php deleted file mode 100644 index 96e2e32..0000000 --- a/src/Factory/FixtureInstantiator/FixtureInstantiator.php +++ /dev/null @@ -1,22 +0,0 @@ - $fixtureClass - */ - public function supports(string $fixtureClass): bool; - - /** - * @template T of Fixture - * - * @param class-string $fixtureClass - * - * @return T - */ - public function instantiate(string $fixtureClass): Fixture; -} diff --git a/src/Factory/FixtureInstantiator/FixtureInstantiatorForAll.php b/src/Factory/FixtureInstantiator/FixtureInstantiatorForAll.php deleted file mode 100644 index b43fc4d..0000000 --- a/src/Factory/FixtureInstantiator/FixtureInstantiatorForAll.php +++ /dev/null @@ -1,33 +0,0 @@ -isAbstract()) { - return false; - } - - $constructor = $class->getConstructor(); - - return !$constructor || ($constructor->isPublic() && 0 === $constructor->getNumberOfRequiredParameters()); - } - - public function instantiate(string $fixtureClass): Fixture - { - $fixture = new $fixtureClass(); - \assert($fixture instanceof Fixture); - - return $fixture; - } -} diff --git a/src/Factory/FixtureInstantiator/FixtureInstantiatorForParametrizedConstructors.php b/src/Factory/FixtureInstantiator/FixtureInstantiatorForParametrizedConstructors.php deleted file mode 100644 index 91b0a9d..0000000 --- a/src/Factory/FixtureInstantiator/FixtureInstantiatorForParametrizedConstructors.php +++ /dev/null @@ -1,81 +0,0 @@ -isAbstract() || !$constructor = $class->getConstructor()) { - return false; - } - - return $constructor->isPublic() && $constructor->getNumberOfRequiredParameters() > 0; - } - - public function instantiate(string $fixtureClass): Fixture - { - $constructor = (new \ReflectionClass($fixtureClass))->getConstructor(); - \assert($constructor instanceof \ReflectionMethod); - - $constructorServices = []; - foreach ($constructor->getParameters() as $parameter) { - $reflectionType = $parameter->getType(); - - if (!$reflectionType instanceof \ReflectionNamedType) { - $this->throwLogicException( - 'Parameter "$%s" of %s::%s() has no type, while it should be an implementation of "%s".', - $parameter, - ); - } - - $type = $reflectionType->getName(); - - if (ContainerInterface::class === $type) { - $constructorServices[] = $this->container; - continue; - } - - if ($this->container->has($type)) { - $constructorServices[] = $this->container->get($type); - continue; - } - - $this->throwLogicException( - 'Parameter "$%s" of %s::%s() is not known to container. Check if it exists and is public. "%s"', - $parameter, - ); - } - - $fixture = new $fixtureClass(...$constructorServices); - \assert($fixture instanceof Fixture); - - return $fixture; - } - - private function throwLogicException(string $message, \ReflectionParameter $parameter): never - { - throw new \LogicException(sprintf( - $message, - $parameter->getName(), - // @phpstan-ignore-next-line this is a constructor parameter, so it always has a class - $parameter->getDeclaringClass()->getName(), - $parameter->getDeclaringFunction()->getName(), - Fixture::class, - )); - } -} diff --git a/src/Fixture.php b/src/Fixture.php deleted file mode 100644 index fb9c682..0000000 --- a/src/Fixture.php +++ /dev/null @@ -1,10 +0,0 @@ -objectReferenceRepository = $objectReferenceRepository; + } + + protected function setReference(string $name, object $reference): void + { + $this->objectReferenceRepository->setReference($name, $reference); + } + + /** + * @throws \BadMethodCallException - if repository already has a reference by $name + */ + protected function addReference(string $name, object $object): void + { + $this->objectReferenceRepository->addReference($name, $object); + } + + /** + * @template T of object + * + * @param class-string $class + * + * @return T + * + * @throws \OutOfBoundsException - if repository does not exist + */ + protected function getReference(string $name, string $class): object + { + return $this->objectReferenceRepository->getReference($name, $class); + } + + /** + * @param class-string $class + */ + protected function hasReference(string $name, string $class): bool + { + return $this->objectReferenceRepository->hasReference($name, $class); + } +} diff --git a/src/Fixture/Fixture.php b/src/Fixture/Fixture.php new file mode 100644 index 0000000..19c9d35 --- /dev/null +++ b/src/Fixture/Fixture.php @@ -0,0 +1,8 @@ +> + */ + public function getDependencies(): array; +} diff --git a/src/Fixture/HasGroups.php b/src/Fixture/HasGroups.php new file mode 100644 index 0000000..169d7ae --- /dev/null +++ b/src/Fixture/HasGroups.php @@ -0,0 +1,18 @@ + + */ + public static function getGroups(): array; +} diff --git a/src/FixtureLoader/FixtureLoader.php b/src/FixtureLoader/FixtureLoader.php new file mode 100644 index 0000000..8ccc9ea --- /dev/null +++ b/src/FixtureLoader/FixtureLoader.php @@ -0,0 +1,49 @@ +eventDispatcher->dispatch(new BeforeLoadFixtures($this->locateFixtures()))->fixtures; + + $loadedFixtures = []; + foreach ($fixtures as $fixture) { + if ($this->eventDispatcher->dispatch(new BeforeExecuteFixture($fixture))->preventExecution) { + continue; + } + + $this->executor->execute($fixture); + $this->eventDispatcher->dispatch(new AfterExecuteFixture($fixture)); + + $loadedFixtures[] = $fixture; + } + + $this->eventDispatcher->dispatch(new AfterLoadFixtures($loadedFixtures)); + } + + /** + * @return list + */ + protected function locateFixtures(): array + { + return $this->fixtureLocator->getFixtures(); + } +} diff --git a/src/FixtureLoader/SelectiveFixtureLoader.php b/src/FixtureLoader/SelectiveFixtureLoader.php new file mode 100644 index 0000000..ef7cd45 --- /dev/null +++ b/src/FixtureLoader/SelectiveFixtureLoader.php @@ -0,0 +1,31 @@ +> $fixtureNames + * + * @return $this + */ + public function setFixturesToLoad(array $fixtureNames): self + { + $this->fixtureLocator->setFixturesToLoad($fixtureNames); + + return $this; + } +} diff --git a/src/Locator/AllFixturesLocator.php b/src/Locator/AllFixturesLocator.php new file mode 100644 index 0000000..7274150 --- /dev/null +++ b/src/Locator/AllFixturesLocator.php @@ -0,0 +1,21 @@ + $allFixtures + */ + public function __construct( + private readonly \Traversable $allFixtures, + ) { + } + + public function getFixtures(): array + { + return iterator_to_array($this->allFixtures, false); + } +} diff --git a/src/Locator/FixtureLocator.php b/src/Locator/FixtureLocator.php new file mode 100644 index 0000000..cdf2110 --- /dev/null +++ b/src/Locator/FixtureLocator.php @@ -0,0 +1,13 @@ + + */ + public function getFixtures(): array; +} diff --git a/src/Locator/FixtureNotFound.php b/src/Locator/FixtureNotFound.php new file mode 100644 index 0000000..ee00b5e --- /dev/null +++ b/src/Locator/FixtureNotFound.php @@ -0,0 +1,19 @@ +> $fixturesToLoad + */ + public static function forFixtures(array $fixturesToLoad): self + { + return new self(sprintf( + 'Fixtures not found: "%s"', + implode('", "', $fixturesToLoad), + )); + } +} diff --git a/src/Locator/FixturesGroupLocator.php b/src/Locator/FixturesGroupLocator.php new file mode 100644 index 0000000..b4b9e95 --- /dev/null +++ b/src/Locator/FixturesGroupLocator.php @@ -0,0 +1,77 @@ + */ + private array $groupNamesToLoad = []; + + /** + * @param \Traversable $allFixtures + */ + public function __construct( + private readonly \Traversable $allFixtures, + ) { + } + + /** + * @return list + */ + public function getGroupsToLoad(): array + { + return $this->groupNamesToLoad; + } + + /** + * @param list $groupNames + * + * @return $this + */ + public function setGroupsToLoad(array $groupNames): self + { + $this->groupNamesToLoad = $groupNames; + + return $this; + } + + public function getFixtures(): array + { + if (empty($this->getGroupsToLoad())) { + return iterator_to_array($this->allFixtures, false); + } + + $fixtures = []; + foreach ($this->allFixtures as $fixture) { + if (!$fixture instanceof HasGroups) { + continue; + } + + if (!$this->hasAtLeastOneGroupMatch($fixture::getGroups(), $this->getGroupsToLoad())) { + continue; + } + + $fixtures[] = $fixture; + } + + return $fixtures; + } + + /** + * @param list $groupsOfTheFixture + * @param list $requestedGroups + */ + private function hasAtLeastOneGroupMatch(array $groupsOfTheFixture, array $requestedGroups): bool + { + foreach ($groupsOfTheFixture as $groupName) { + if (\in_array($groupName, $requestedGroups, true)) { + return true; + } + } + + return false; + } +} diff --git a/src/Locator/NamedFixtureLocator.php b/src/Locator/NamedFixtureLocator.php new file mode 100644 index 0000000..187c238 --- /dev/null +++ b/src/Locator/NamedFixtureLocator.php @@ -0,0 +1,67 @@ +> */ + private array $fixturesToLoad = []; + + /** + * @param \Traversable $allFixtures + */ + public function __construct( + private readonly \Traversable $allFixtures, + ) { + } + + /** + * @return list> + */ + public function getFixturesToLoad(): array + { + return $this->fixturesToLoad; + } + + /** + * @param list> $fixtureNames + * + * @return $this + */ + public function setFixturesToLoad(array $fixtureNames): self + { + $this->fixturesToLoad = $fixtureNames; + + return $this; + } + + public function getFixtures(): array + { + $fixturesToLoad = $this->getFixturesToLoad(); + if (empty($fixturesToLoad)) { + return iterator_to_array($this->allFixtures, false); + } + + $fixtures = []; + foreach ($this->allFixtures as $fixture) { + $keys = array_keys($fixturesToLoad, $fixture::class); + if (empty($keys)) { + continue; + } + + $fixtures[] = $fixture; + + array_walk($keys, static function ($k) use (&$fixturesToLoad) { + unset($fixturesToLoad[$k]); + }); + } + + if (!empty($fixturesToLoad)) { + throw FixtureNotFound::forFixtures($fixturesToLoad); + } + + return $fixtures; + } +} diff --git a/src/ReferenceRepository/ObjectReferenceRepository.php b/src/ReferenceRepository/ObjectReferenceRepository.php new file mode 100644 index 0000000..1a3cd5c --- /dev/null +++ b/src/ReferenceRepository/ObjectReferenceRepository.php @@ -0,0 +1,68 @@ +> + */ + private array $referencesByClass = []; + + public function setReference(string $name, object $reference): void + { + $this->referencesByClass[$reference::class][$name] = $reference; + } + + /** + * @throws \BadMethodCallException - if repository already has a reference by $name + */ + public function addReference(string $name, object $reference): void + { + if (isset($this->referencesByClass[$reference::class][$name])) { + throw new \BadMethodCallException(sprintf( + 'Reference to "%s" for class "%s" already exists, use method setReference() in order to override it', + $name, + $reference::class, + )); + } + + $this->setReference($name, $reference); + } + + /** + * Loads an object using stored reference named by $name. + * + * @template T of object + * + * @param class-string $class + * + * @return T + * + * @throws \OutOfBoundsException - if repository does not exist + */ + public function getReference(string $name, string $class): object + { + if (!$this->hasReference($name, $class)) { + throw new \OutOfBoundsException(sprintf('Reference to "%s" for class "%s" does not exist', $name, $class)); + } + + $reference = $this->referencesByClass[$class][$name]; + \assert($reference instanceof $class); + + return $reference; + } + + /** + * Check if an object is stored using reference named by $name. + * + * @param class-string $class + */ + public function hasReference(string $name, string $class): bool + { + return isset($this->referencesByClass[$class][$name]); + } +} diff --git a/src/Sorter/CircularFixtureDependency.php b/src/Sorter/CircularFixtureDependency.php new file mode 100644 index 0000000..fca8009 --- /dev/null +++ b/src/Sorter/CircularFixtureDependency.php @@ -0,0 +1,14 @@ +> */ + private array $checking = []; + + /** + * @param list $allFixtures + */ + public function __construct( + private readonly array $allFixtures, + ) { + } + + /** + * @param list $fixtures + * + * @return list + */ + public function sort(array $fixtures = []): array + { + $fixtures = empty($fixtures) ? $this->allFixtures : $fixtures; + + $sorted = []; + foreach ($fixtures as $fixture) { + $this->add($fixture, $sorted); + } + + return $sorted; + } + + /** + * @param list $sorted + */ + private function add(Fixture $fixture, array &$sorted): void + { + if (\in_array($fixture, $sorted, true)) { + return; + } + + if (\in_array($fixture::class, $this->checking, true)) { + throw new CircularFixtureDependency($fixture::class); + } + $this->checking[] = $fixture::class; + + if (!$fixture instanceof HasDependencies || [] === $fixture->getDependencies()) { + $sorted[] = $fixture; + + return; + } + + foreach ($fixture->getDependencies() as $dependency) { + $this->add($this->getFixture($dependency), $sorted); + } + $sorted[] = $fixture; + + $this->checking = array_filter($this->checking, fn ($v) => $v !== $fixture::class); + } + + private function getFixture(string $name): Fixture + { + foreach ($this->allFixtures as $fixture) { + if ($fixture::class === $name) { + return $fixture; + } + } + + throw new UnresolvedFixtureDependency($name); + } +} diff --git a/src/Sorter/UnresolvedFixtureDependency.php b/src/Sorter/UnresolvedFixtureDependency.php new file mode 100644 index 0000000..61a3bee --- /dev/null +++ b/src/Sorter/UnresolvedFixtureDependency.php @@ -0,0 +1,14 @@ +createdAt)) { - // Ensures that this method doesn't get called twice on the same object. - throw new \LogicException('Should never happen.'); - } - - $this->createdAt = microtime(true); - - // ensure the next fixture is not created in the same microsecond - usleep(1); - } -} diff --git a/tests/Fixtures/FixtureWithDependency.php b/tests/Fixtures/FixtureWithDependency.php deleted file mode 100644 index c83e992..0000000 --- a/tests/Fixtures/FixtureWithDependency.php +++ /dev/null @@ -1,15 +0,0 @@ -createdAt = microtime(true); - } -} diff --git a/tests/Fixtures/SomeFixture.php b/tests/Fixtures/SomeFixture.php deleted file mode 100644 index e7f2b3f..0000000 --- a/tests/Fixtures/SomeFixture.php +++ /dev/null @@ -1,15 +0,0 @@ -created = true; - } -} diff --git a/tests/Functional/Factory/FixtureFactoryTest.php b/tests/Functional/Factory/FixtureFactoryTest.php deleted file mode 100644 index d6ffaba..0000000 --- a/tests/Functional/Factory/FixtureFactoryTest.php +++ /dev/null @@ -1,140 +0,0 @@ -createFixtures([SomeFixture::class]); - - $fixture = $factory->getFixture(SomeFixture::class); - self::assertInstanceOf(SomeFixture::class, $fixture); - self::assertTrue($fixture->created); - } - - /** - * @test - */ - public function it_throws_when_prompted_to_create_a_fixture_that_does_not_implement_the_Fixture_interface(): void - { - $factory = new FixtureFactory([new FixtureInstantiatorForAll()]); - - $this->expectException(\InvalidArgumentException::class); - $this->expectExceptionMessage('Expected "stdClass" to implement "Neusta\Pimcore\FixtureBundle\Fixture", but it does not.'); - - $factory->createFixtures([\stdClass::class]); - } - - /** - * @test - */ - public function it_creates_depending_fixtures_first(): void - { - $factory = new FixtureFactory([new FixtureInstantiatorForAll()]); - - $factory->createFixtures([FixtureWithDependency::class]); - - $fixtureWithDependency = $factory->getFixture(FixtureWithDependency::class); - $dependantFixture = $factory->getFixture(DependantFixture::class); - self::assertInstanceOf(FixtureWithDependency::class, $fixtureWithDependency); - self::assertInstanceOf(DependantFixture::class, $dependantFixture); - self::assertGreaterThan($dependantFixture->createdAt, $fixtureWithDependency->createdAt); - } - - /** - * @test - */ - public function it_throws_when_dependency_has_invalid_type(): void - { - $factory = new FixtureFactory([new FixtureInstantiatorForAll()]); - $fixture = new class() implements Fixture { - public function create($something): void - { - } - }; - - $this->expectException(\LogicException::class); - $this->expectExceptionMessageMatches('/Parameter "\$something" of .+::create\(\) has an invalid type/'); - - $factory->createFixtures([$fixture::class]); - } - - /** - * @test - */ - public function it_throws_when_depending_on_a_non_fixture_object(): void - { - $factory = new FixtureFactory([new FixtureInstantiatorForAll()]); - $fixture = new class() implements Fixture { - public function create(\stdClass $noFixture): void - { - } - }; - - $this->expectException(\LogicException::class); - $this->expectExceptionMessage('Expected "stdClass" to implement "Neusta\Pimcore\FixtureBundle\Fixture", but it does not.'); - - $factory->createFixtures([$fixture::class]); - } - - /** - * @test - */ - public function it_creates_a_fixture_only_once(): void - { - $factory = new FixtureFactory([new FixtureInstantiatorForAll()]); - - $factory->createFixtures([FixtureWithDependency::class]); - - $dependantFixture = $factory->getFixture(DependantFixture::class); - - $factory->createFixtures([DependantFixture::class]); - - self::assertSame($dependantFixture, $factory->getFixture(DependantFixture::class)); - } - - /** - * @test - */ - public function it_dispatches_fixture_creation_events(): void - { - $eventDispatcher = new EventDispatcher(); - $factory = new FixtureFactory([new FixtureInstantiatorForAll()], $eventDispatcher); - $createFixtureEvent = null; - $fixtureWasCreatedEvent = null; - - $eventDispatcher->addListener(CreateFixture::class, function ($event) use (&$createFixtureEvent) { - $createFixtureEvent = $event; - }); - $eventDispatcher->addListener(FixtureWasCreated::class, function ($event) use (&$fixtureWasCreatedEvent) { - $fixtureWasCreatedEvent = $event; - }); - - $factory->createFixtures([SomeFixture::class]); - - self::assertEquals(new CreateFixture(SomeFixture::class, 0), $createFixtureEvent); - self::assertEquals(new FixtureWasCreated($factory->getFixture(SomeFixture::class)), $fixtureWasCreatedEvent); - } -} diff --git a/tests/Locator/FixturesGroupLocatorTest.php b/tests/Locator/FixturesGroupLocatorTest.php new file mode 100644 index 0000000..8a410d0 --- /dev/null +++ b/tests/Locator/FixturesGroupLocatorTest.php @@ -0,0 +1,69 @@ +setGroupsToLoad([]); + + $locatedFixtures = $namedFixtureLocator->getFixtures(); + self::assertSame([], $locatedFixtures); + } + + /** + * @test + */ + public function it_filters_for_wanted_fixture(): void + { + $fixture = new MockFixtureOfGroupRed(); + + $namedFixtureLocator = new FixturesGroupLocator(new \ArrayObject([$fixture])); + $namedFixtureLocator->setGroupsToLoad(['red']); + + $locatedFixtures = $namedFixtureLocator->getFixtures(); + self::assertSame([$fixture], $locatedFixtures); + } + + /** + * @test + */ + public function it_skips_unwanted_fixture(): void + { + $fixture = new MockFixtureOfGroupRed(); + + $namedFixtureLocator = new FixturesGroupLocator(new \ArrayObject([$fixture])); + $namedFixtureLocator->setGroupsToLoad(['blue']); + + $locatedFixtures = $namedFixtureLocator->getFixtures(); + self::assertSame([], $locatedFixtures); + } + + /** + * @test + */ + public function it_provides_all_fixtures_if_no_filter_is_given(): void + { + $fixture = new MockFixtureWithoutDependencies(); + + $namedFixtureLocator = new FixturesGroupLocator(new \ArrayObject([$fixture])); + $emptyFilter = []; + $namedFixtureLocator->setGroupsToLoad($emptyFilter); + + $locatedFixtures = $namedFixtureLocator->getFixtures(); + self::assertSame([$fixture], $locatedFixtures); + } +} diff --git a/tests/Locator/NamedFixtureLocatorTest.php b/tests/Locator/NamedFixtureLocatorTest.php new file mode 100644 index 0000000..c15cd60 --- /dev/null +++ b/tests/Locator/NamedFixtureLocatorTest.php @@ -0,0 +1,69 @@ +setFixturesToLoad($emptyList); + + $locatedFixtures = $namedFixtureLocator->getFixtures(); + self::assertSame([], $locatedFixtures); + } + + /** + * @test + */ + public function it_filters_for_wanted_fixture(): void + { + $fixture = new MockFixtureWithoutDependencies(); + + $namedFixtureLocator = new NamedFixtureLocator(new \ArrayObject([$fixture])); + $namedFixtureLocator->setFixturesToLoad([MockFixtureWithoutDependencies::class]); + + $locatedFixtures = $namedFixtureLocator->getFixtures(); + self::assertSame([$fixture], $locatedFixtures); + } + + /** + * @test + */ + public function it_skips_unwanted_fixture(): void + { + $unwantedFixture = new MockFixtureWithoutDependencies(); + $wantedFixture = new MockDependentFixtureWithoutDependencies(); + + $namedFixtureLocator = new NamedFixtureLocator(new \ArrayObject([$unwantedFixture, $wantedFixture])); + $namedFixtureLocator->setFixturesToLoad([MockDependentFixtureWithoutDependencies::class]); + + $locatedFixtures = $namedFixtureLocator->getFixtures(); + self::assertSame([$wantedFixture], $locatedFixtures); + } + + /** + * @test + */ + public function it_provides_all_fixtures_if_no_filter_is_given(): void + { + $fixture = new MockFixtureWithoutDependencies(); + + $namedFixtureLocator = new NamedFixtureLocator(new \ArrayObject([$fixture])); + + $locatedFixtures = $namedFixtureLocator->getFixtures(); + self::assertSame([$fixture], $locatedFixtures); + } +} diff --git a/tests/Mock/MockDependentFixtureWithoutDependencies.php b/tests/Mock/MockDependentFixtureWithoutDependencies.php new file mode 100644 index 0000000..f49e898 --- /dev/null +++ b/tests/Mock/MockDependentFixtureWithoutDependencies.php @@ -0,0 +1,19 @@ +sort(); + + self::assertEmpty($sorted); + } + + /** + * @test + */ + public function it_sorts_a_list_with_one_entry(): void + { + $fixture = new MockFixtureWithoutDependencies(); + + $fixtureDependencySorter = new FixtureDependencySorter([$fixture]); + $sorted = $fixtureDependencySorter->sort(); + + self::assertCount(1, $sorted); + self::assertEquals([$fixture], $sorted); + } + + /** + * @test + */ + public function it_sorts_two_fixtures_having_no_dependencies(): void + { + $fixture1 = new MockFixtureWithoutDependencies(); + $fixture2 = new MockDependentFixtureWithoutDependencies(); + + $fixtureDependencySorter = new FixtureDependencySorter([$fixture1, $fixture2]); + $sorted = $fixtureDependencySorter->sort(); + + self::assertCount(2, $sorted); + self::assertEquals([$fixture1, $fixture2], $sorted); + } + + /** + * @test + */ + public function it_throws_exception_for_one_fixture_depending_on_itself(): void + { + $fixture = new MockFixtureDependsOnItself(); + + $fixtureDependencySorter = new FixtureDependencySorter([$fixture]); + + $this->expectException(CircularFixtureDependency::class); + $this->expectExceptionMessage( + 'CircularFixtureDependency: Circular Reference detected in Fixture "Neusta\Pimcore\FixtureBundle\Tests\Mock\MockFixtureDependsOnItself"' + ); + $fixtureDependencySorter->sort(); + } + + /** + * @test + */ + public function it_throws_exception_for_one_fixture_with_unresolved_dependency(): void + { + $fixture = new MockFixtureOneDependsOnTwo(); + + $fixtureDependencySorter = new FixtureDependencySorter([$fixture]); + + $this->expectException(UnresolvedFixtureDependency::class); + $this->expectExceptionMessage( + 'UnresolvedFixtureDependency: Fixture "Neusta\Pimcore\FixtureBundle\Tests\Mock\MockFixtureTwoDependsOnOne" not found' + ); + $fixtureDependencySorter->sort(); + } + + /** + * @test + */ + public function it_throws_exception_for_two_fixture_depending_on_each_other(): void + { + $fixture1 = new MockFixtureOneDependsOnTwo(); + $fixture2 = new MockFixtureTwoDependsOnOne(); + + $fixtureDependencySorter = new FixtureDependencySorter([$fixture1, $fixture2]); + + $this->expectException(CircularFixtureDependency::class); + $this->expectExceptionMessage( + 'CircularFixtureDependency: Circular Reference detected in Fixture "Neusta\Pimcore\FixtureBundle\Tests\Mock\MockFixtureOneDependsOnTwo"' + ); + $fixtureDependencySorter->sort(); + } + + /** + * @test + */ + public function it_throws_exception_for_three_fixture_with_circular_dependency(): void + { + $fixture1 = new MockFixtureAlphaDependsOnBeta(); + $fixture2 = new MockFixtureBetaDependsOnGamma(); + $fixture3 = new MockFixtureGammaDependsOnAlpha(); + + $fixtureDependencySorter = new FixtureDependencySorter([$fixture1, $fixture2, $fixture3]); + + $this->expectException(CircularFixtureDependency::class); + $this->expectExceptionMessage( + 'CircularFixtureDependency: Circular Reference detected in Fixture "Neusta\Pimcore\FixtureBundle\Tests\Mock\MockFixtureAlphaDependsOnBeta"' + ); + $fixtureDependencySorter->sort(); + } +}