orbitale / array-fixture
A Doctrine Data Fixture service based on PHP arrays.
Installs: 30 665
Dependents: 0
Suggesters: 0
Security: 0
Stars: 16
Watchers: 3
Forks: 0
Open Issues: 0
Requires
- php: ^7.3|^8.0
- doctrine/data-fixtures: ^1.4
- doctrine/instantiator: ^1.3|^2.0
- doctrine/persistence: ^1.3|^2.0|^3.0
Requires (Dev)
- doctrine/mongodb-odm: ^2.2
- doctrine/orm: ^2.7|^3.0
- phpstan/phpstan: ^1.11
- phpstan/phpstan-deprecation-rules: ^1.2
- phpstan/phpstan-doctrine: ^1.4
- phpstan/phpstan-phpunit: ^1.4
- phpstan/phpstan-symfony: ^1.4
- phpunit/phpunit: ^9.5
Suggests
- doctrine/doctrine-fixtures-bundle: To use with ORMFixtureInterface and register fixtures automatically in the Dependency Injection container.
README
A Doctrine Data Fixture service that allow you to write your fixtures as PHP arrays.
Installation
Install the library in your project with Composer:
composer require orbitale/array-fixture
Why?
Doctrine Fixtures examples across the web mostly look like this:
<?php namespace AppBundle\DataFixtures\ORM; use App\Entity\Post; use Doctrine\Bundle\FixturesBundle\Fixture; use Doctrine\Persistence\ObjectManager; class PostFixtures extends Fixture { public function load(ObjectManager $manager) { $post = new Post(); $post->setTitle('First post'); $post->setDescription('Lorem ipsum'); $manager->persist($post); $manager->flush(); } }
This example contains two main issues:
- It is very verbose: you do everything by hand.
- Your entities must have setters, and therefore expose an anemic model.
This package provides a Fixture class you can use (with or without the DoctrineBundle) that allows you to write PHP arrays instead of objects and manual instructions.
The biggest advantage is that you can easily get a raw PHP export of your database (such as with PhpMyAdmin) and put it inside the fixture (this requires checking fields names, but search/replace will handily help making this fast), or you can also fetch data from any source (Fixtures are registered as services when using them in Symfony, so you could inject any service in their constructor) and return it as a PHP array (such as fetching from an API, a CSV file, a Yaml file, etc.).
There are some specific features that are however only available in pure PHP mode (like getting references and using a Closure
to populate the field).
Usage
Here is an example of a standard fixtures class:
<?php namespace AppBundle\DataFixtures\ORM; use App\Entity\Post; use Orbitale\Component\ArrayFixture\ArrayFixture; use Doctrine\Bundle\FixturesBundle\ORMFixtureInterface; class PostFixtures extends ArrayFixture implements ORMFixtureInterface { public function getEntityClass(): string { return Post::class; } public function getObjects(): iterable { yield ['title' => 'First post', 'description' => 'Lorem ipsum']; yield ['title' => 'Second post', 'description' => 'muspi meroL']; } }
Let's explain:
- We are extending the
ArrayFixture
class from this package - You must implement
ORMFixtureInterface
yourself to allow Symfony to autoconfigure this fixture as a service. This is mandatory when using Symfony and the DoctrineBundle. - The
getEntityClass()
method needs to know which Entity will be managed by this Fixture class. - The
getObjects()
method must return aniterable
, so it could either be anarray
or aGenerator
, since the fixture will only loop on it.
Every array ingetObjects()
will be hydrated in an instance of the Entity class, without the need for setters. Bye anemic entities!
Then, new entity is persisted.
After all entities are hydrated and persisted, the Entity Manager will be flushed, and entities will be saved in your database.
References
Sometimes, an entity depends on another to be populated.
With this Fixture class, it becomes much simpler since a few methods can be overriden in order to simplify the understanding on how our entities are persisted.
ℹ Note: For this, you must determine in which order these fixtures must be executed, by implementing either the Doctrine\Common\DataFixtures\OrderedFixtureInterface
or Doctrine\Common\DataFixtures\DependentFixtureInterface
interfaces (but not both!).
See the examples:
<?php namespace App\DataFixtures\ORM; use App\Entity\Tag; use Orbitale\Component\ArrayFixture\ArrayFixture; use Doctrine\Bundle\FixturesBundle\ORMFixtureInterface; class TagFixtures extends ArrayFixture implements ORMFixtureInterface { public function getEntityClass(): string { return Tag::class; } public function getReferencePrefix(): ?string { return 'tags-'; } public function getMethodNameForReference(): string { return 'getName'; } public function getObjects(): array { return [ ['name' => 'Some tag'], ]; } }
Thanks to the overrides of getReferencePrefix
and getMethodNameForReference
, after each Tag
is persisted, the Fixture will add a reference of the tag in the memory by calling this:
$this->addReference($this->getReferencePrefix().$tag->getName(), $tag);
Then, we can create the PostFixtures
and fetch the reference just like we would do with any common Fixture:
<?php namespace App\DataFixtures\ORM; use App\Entity\Post; use Orbitale\Component\ArrayFixture\ArrayFixture; use Doctrine\Bundle\FixturesBundle\ORMFixtureInterface; class PostFixtures extends ArrayFixture implements ORMFixtureInterface { public function getEntityClass(): string { return Post::class; } public function getObjects(): array { return [ [ 'title' => 'First post', 'tags' => [ $this->getReference('tags-Some tag'), ], ], ]; } }
Here, we reuse the Some tag
tag name and the tags-
prefix that were specified in the TagFixtures
!
🚀Pro tip: You can also use a class constant in TagFixtures
(or any other class) for the tag name ("Some tag"
here) and reuse it in your tests, this will allow you to change Some Tag
to something else without breaking your tests!
Using a callable to get a self-referenced entity
When you have self-referencing relationships, you may need a reference of an object that may have already been persisted.
For this, first, you should set the flushEveryXIterations
option to 1
(view below) to allow flushing every entity. Be careful, because flushing every time is more time-consuming. Don't do this if you have a lot of entities to process in your fixture (like hundreds).
Next, you can set a callable
element as the value of your object so you can interact manually with the injected object
as 1st argument, and the AbstractFixture
object as 2nd argument.
The EntityManagerInterface
is also injected as 3rd argument in case you need to do some specific requests or query through another
table.
Example here:
<?php namespace App\DataFixtures\ORM; use App\Entity\Post; use Orbitale\Component\ArrayFixture\ArrayFixture; use Doctrine\Bundle\FixturesBundle\ORMFixtureInterface; use Doctrine\ORM\EntityManagerInterface; class PostFixtures extends ArrayFixture implements ORMFixtureInterface { public function getEntityClass(): string { return Post::class; } /** * With this, we can retrieve a Post reference with this method: * $this->getReference('posts-1'); * where '1' is the post id. */ public function getReferencePrefix(): ?string { return 'posts-'; } protected function flushEveryXIterations(): int { return 1; } public function getObjects(): array { return [ ['id' => 1, 'title' => 'First post', 'parent' => null], [ 'title' => 'Second post', 'parent' => function(Post $object, ArrayFixture $fixture, EntityManagerInterface $manager) { return $fixture->getReference('posts-1'); }, ], ]; } }
This allows perfect synchronicity when dealing with self-referencing relations.
The advantage of closures is that you can also use external references by using a use (...)
directive in the closure definition, or you can also call the entity itself, the fixture or the entity manager since they are passed as arguments (as in the example) to the closure at runtime.
Insert primary keys
If you want, you can specify the primary key field of your entity, like this:
public function getObjects(): array { return [ ['id' => 1, 'title' => 'Post 1'], ['id' => 2, 'title' => 'Post 2'], ]; }
If your primary key name is different than id
, it's fine: the fixture class uses Doctrine's Metadata
in order to detect your primary key.
This is really efficient when using uuid
as field type for your entities, as UUIDs can be generated either from the database or from your code itself, and Doctrine will take it in account, contrary to auto-increment-integer fields that need a feedback from the database to generate the ID.
Note that this feature is not yet compatible with composite primary keys (yet, feel free to help if you know some about this!).
ArrayFixture
class reference
The ArrayFixture
class contains several protected
methods you can override for your needs:
getReferencePrefix()
(defaultnull
)
Used to make the Fixture class call$this->addReference()
after each entity is persisted.
References are stored as{referencePrefix}-{id|__toString()|specific method name}
.getMethodNameForReference()
(defaultgetId
)
Used to specify which method on the object is used to specify the reference.
Defaults togetId
, always falls back to__toString()
if exists, throws an exception in case method doesn't exist.flushEveryXIterations()
(default0
)
Used to flush in batches instead of flushing only once at the end of all fixtures persist.disableLogger()
(defaulttrue
)
Used to disable SQL queries logging, useful to save memory at runtime when you have hundreds of entities to save.clearEntityManagerOnFlush()
(defaulttrue
)
Used to make sure the Entity manager is totally cleared after aflush()
, this saves Doctrine from potential memory leaks or high memory consumption, but at the cost of needing to make more queries if you need to fetch objects from the database, even though this is supposed to be a really rare and edgy case.createNewInstance(array $data)
This method is here to automatically instantiate your object without constructor and populate its properties without setters.
You can override it in case you have a very custom way of handling object creation, even though this library should be sufficient.
License and Copyright
This project is licensed under LGPL-2.1. Check the LICENSE file for more details.