Schema Validation and Fixtures
doctrine:schema:validate catches the migration nobody generated; fixtures give every developer the same seed data.
php bin/console doctrine:schema:validate / php bin/console doctrine:fixtures:loadTwo commands sit either side of migrations. One checks that the schema matches the mapping. The other fills an empty database with usable rows.
schema:validate
php bin/console doctrine:schema:validate runs two separate checks and reports each one. The first is that the mapping is internally consistent: both sides of an association agree, the referenced columns exist. The second is that the database currently matches that mapping. It exits with a non-zero status when either fails, which makes it a build step rather than a thing you remember to run.
In practice the second check is what earns its place. It fails when someone changed an entity and did not generate a migration, and it fails in CI, not during a deploy.
Fixtures
A fixture is a class that creates objects and saves them, so a fresh database can be filled with one command. Install DoctrineFixturesBundle, extend Doctrine\Bundle\FixturesBundle\Fixture, and implement load(ObjectManager $manager): void. Inside, build entities, call persist() on each and flush() once at the end: persist schedules, flush writes.
Fixture classes that need objects from another class use references. addReference() names an object; getReference() retrieves it, and DependentFixtureInterface declares the ordering so the reference exists by the time it is asked for.
Example
<?php
// src/DataFixtures/AuthorFixtures.php
declare(strict_types=1);
namespace App\DataFixtures;
use App\Entity\Author;
use Doctrine\Bundle\FixturesBundle\Fixture;
use Doctrine\Bundle\FixturesBundle\FixtureGroupInterface;
use Doctrine\Persistence\ObjectManager;
final class AuthorFixtures extends Fixture implements FixtureGroupInterface
{
// Typed class constants need PHP 8.3 or later; 'const EDITOR = ...'
// without the type is the same constant and works on any version.
public const string EDITOR = 'author-editor';
public static function getGroups(): array
{
// Loadable on its own: doctrine:fixtures:load --group=demo
return ['demo'];
}
public function load(ObjectManager $manager): void
{
$author = new Author();
$author->setName('Ada Byron');
$author->setEmail('[email protected]');
$manager->persist($author); // schedules the insert
$manager->flush(); // writes it; without this, nothing is saved
// Name the object so other fixture classes can ask for it.
$this->addReference(self::EDITOR, $author);
}
}
When to use it
- Fail a CI build when someone changes an entity and forgets the migration
- Give a new developer a working database with one command after cloning
- Reset a local database to known rows before reproducing a bug
- Load only the demo data for one feature with --group instead of the whole set
More examples
A fixture that depends on another
Pass the class name as the second argument to getReference; it is what lets the method return a typed object instead of something your static analyser has to guess at. Splitting fixtures per entity and wiring them with getDependencies keeps each file small, and the reference lookup means the loop never issues a query per row.
<?php
// src/DataFixtures/ArticleFixtures.php
declare(strict_types=1);
namespace App\DataFixtures;
use App\Entity\Article;
use App\Entity\Author;
use Doctrine\Bundle\FixturesBundle\Fixture;
use Doctrine\Common\DataFixtures\DependentFixtureInterface;
use Doctrine\Persistence\ObjectManager;
final class ArticleFixtures extends Fixture implements DependentFixtureInterface
{
public function getDependencies(): array
{
// AuthorFixtures::load() has finished before this one starts.
return [AuthorFixtures::class];
}
public function load(ObjectManager $manager): void
{
// Resolved from the reference, so the loop runs no queries.
$author = $this->getReference(AuthorFixtures::EDITOR, Author::class);
foreach (['Routing', 'Services', 'Migrations'] as $index => $title) {
$article = new Article();
$article->setTitle($title);
$article->setSlug(strtolower($title));
$article->setAuthor($author);
$manager->persist($article);
}
// One flush for all three rows, outside the loop.
$manager->flush();
}
}The validate output, and what each half means
A correct mapping with an out-of-sync database is the everyday case: an entity changed and no migration was generated. Fix it with make:migration, never with doctrine:schema:update --force, which rewrites the live schema with no file to review and no record that it happened.
$ php bin/console doctrine:schema:validate
Mapping
-------
[OK] The mapping files are correct.
Database
--------
[ERROR] The database schema is not in sync with the current mapping file.
$ echo $?
2Seeding a development database
The first run prints "purging database" and the second does not, which is the whole difference --append makes. Use the plain form to reset a development database to a known state, and --append when adding a new fixture class to data you want to keep.
$ php bin/console doctrine:fixtures:load --group=demo
> purging database
> loading App\DataFixtures\AuthorFixtures
> loading App\DataFixtures\ArticleFixtures
$ php bin/console doctrine:fixtures:load --append --group=demo
> loading App\DataFixtures\AuthorFixtures
> loading App\DataFixtures\ArticleFixtures
Discussion