Skip to content

Commit

Permalink
Fixtures as Services (#12)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>

* 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 <[email protected]>
Co-authored-by: Jacob Dreesen <[email protected]>
  • Loading branch information
3 people authored May 2, 2024
1 parent c284205 commit 82f0500
Show file tree
Hide file tree
Showing 58 changed files with 1,413 additions and 779 deletions.
1 change: 0 additions & 1 deletion .github/workflows/qa.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 0 additions & 1 deletion .github/workflows/tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 7 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
210 changes: 98 additions & 112 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand All @@ -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<class-string<Fixture>> $fixtures */
/**
* @param list<class-string<Fixture>> $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();

Expand Down Expand Up @@ -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

Expand Down
38 changes: 30 additions & 8 deletions config/services.yaml
Original file line number Diff line number Diff line change
@@ -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:
Expand All @@ -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/*'
5 changes: 0 additions & 5 deletions phpstan-baseline.neon
Original file line number Diff line number Diff line change
@@ -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
Expand Down
Loading

0 comments on commit 82f0500

Please sign in to comment.