nolanos/laravel-doctrine-factory
Use Eloquent Factories with your Doctrine Entities.
laravel-doctrine/orm ships
with its own factory system,
but its API is frozen at Laravel's pre-8.x style:
$factory->define(...) — no class-based factoriesdefineAs(..., 'admin', ...) instead of first-class state methodsfor(), has())recycle(), no afterMaking / afterCreating callbackshasPosts(), forUser())By contrast, this package keeps you on Laravel's modern factory API. If you already know Eloquent factories you already know this package — read the Laravel docs and use this package as a drop-in replacement. See Feature Compatibility for the full status grid.
Install via Composer:
composer require stemble/laravel-doctrine-factory
Add Laravel's HasFactory trait to your entity and annotate it with @implements HasFactory<YourFactory>.
The annotation is optional but gives IDEs full type inference — User::factory() returns UserFactory,
so state methods like ->admin() are recognised without a cast.
DoctrineFactory extends Laravel's Illuminate\Database\Eloquent\Factories\Factory rather than replacing it,
so the standard HasFactory trait works without modification — there is no Doctrine-specific version to import.
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
/**
* @implements HasFactory<UserFactory>
*/
class User
{
use HasFactory;
private string $name;
private bool $admin = false;
private Collection $posts;
public function __construct(string $name)
{
$this->name = $name;
$this->posts = new ArrayCollection;
}
}
Then create a factory that extends Stemble\LaravelDoctrineFactory\DoctrineFactory:
use Stemble\LaravelDoctrineFactory\DoctrineFactory;
class UserFactory extends DoctrineFactory
{
protected $model = User::class;
public function definition(): array
{
return [
'name' => fake()->name(),
'admin' => false,
];
}
public function admin(): static
{
return $this->state(['admin' => true]);
}
}
Factory discovery follows the same convention as Eloquent: given an entity App\Entities\User, Laravel looks
for Database\Factories\Entities\UserFactory. The factory file lives in database/factories/Entities/UserFactory.php.
Subdirectories under database/factories/ are mirrored from your entity namespace below App\.
Use it the same way you'd use any Laravel factory:
$user = User::factory()->create();
$users = User::factory()->count(3)->create();
$admin = User::factory()->admin()->create();
Status of each Laravel factory feature when used through DoctrineFactory:
| Feature | Status | Notes |
|---|---|---|
Defining factories (definition()) |
✅ works | |
make() |
✅ works* | Also calls EntityManager::persist() — see make() persists to the identity map |
create() |
✅ works | |
count() |
✅ works | |
States (state(), state methods) |
✅ works | |
Sequences (Sequence, sequence()) |
✅ works | |
Has-many (has()) |
✅ works | |
Belongs-to (for()) |
✅ works | |
Magic relationship methods (hasPosts(), forUser()) |
✅ works | |
Many-to-many (hasAttached()) |
N/A | Set a Collection in factory state for plain M2M, or ->has() the pivot entity for M2M with pivot data |
recycle() |
✅ works | |
afterMaking() callback |
✅ works | |
afterCreating() callback |
✅ works | |
| Polymorphic relationships | N/A | No direct Doctrine equivalent |
trashed() / soft deletes |
N/A | Doctrine has no built-in soft deletes |
This package mirrors Laravel's Illuminate\Database\Eloquent\Factories\Factory API as closely as possible,
so the Laravel docs work as your primary reference.
The deviations below fall out of how Doctrine handles persistence, instantiation, and relationships — none of
them are bugs, but they are not obvious from reading the Laravel docs.
make() persists to the identity mapDoctrine splits "register an entity with the EntityManager" (persist) from "write to the database"
(flush). This package mirrors that split across make() and create():
make() — instantiates the entity and calls EntityManager::persist() on it. Nothing is written to
the database yet.create() — does everything make() does, then calls EntityManager::flush() to write the changes.This is the intended deviation from Laravel's make(), which leaves models entirely in-memory. If you call
make() and later trigger an EntityManager::flush() yourself — even unintentionally — the made entities
will be inserted at that point.
Two consequences worth knowing:
cascade: ['persist'] required. Related entities pulled in through the factory chain are
persisted too, so a single flush() saves the whole graph without needing cascade rules on every
association.make() related entities, set them up however you need,
then create() the root — everything flushes together.Eloquent models share a uniform constructor; Doctrine entities don't. This package routes definition attributes through the entity constructor first, then sets remaining attributes via reflection.
class User
{
private string $name;
private bool $admin = false;
private Collection $posts;
public function __construct(string $name)
{
$this->name = $name;
$this->posts = new ArrayCollection;
}
}
class UserFactory extends DoctrineFactory
{
protected $model = User::class;
public function definition(): array
{
return [
'name' => fake()->name(), // matched to constructor parameter
'admin' => false, // set via reflection after construction
];
}
}
A few rules to keep in mind:
MissingConstructorAttributesException is thrown.Non-constructor attributes are written directly to private properties via reflection — setters are not called. If a setter has side effects (validation, eventing, syncing a related collection), it won't fire. Either:
afterMaking() to call the setter explicitly — see Bidirectional Relationships.for() and has() resolve to full entity instances:
$post = Post::factory()->for(User::factory())->make();
$post->getUser(); // returns a User entity
$post->getUser_id(); // ← this doesn't exist
This is fundamental to how Doctrine works — you set $post->user = $userEntity, never
$post->user_id = 123.
The second argument to ->for() (and ->has()) is the entity property name, not a database column,
relationship table, or class name:
// `Post` has a property `User $author`
$user = User::factory()->create();
$post = Post::factory()->for($user, 'author')->create();
When the property name matches the related entity's basename (e.g. property $user of type User), magic
methods like forUser() infer the property automatically and the second argument can be omitted.
Doctrine relationships are often bidirectional — both entities reference each other. The factory only sets one side. The owning side persists correctly to the database, but reading the inverse side in-memory will return an empty collection until you flush and refresh:
$post = Post::factory()->for($user)->make();
$post->getAuthor(); // returns $user ← owning side
$user->getPosts(); // empty Collection ← inverse side, NOT synced
This is the single biggest gotcha for developers coming from Eloquent, where models are dumb data containers and the inverse-side invariant doesn't exist.
afterMaking()Sync the inverse side from afterMaking(). Put it in configure() so it runs every time the factory is
used:
class PostFactory extends DoctrineFactory
{
protected $model = Post::class;
public function definition(): array
{
return [
'title' => fake()->sentence(),
'author' => User::factory(),
];
}
public function configure(): static
{
return $this->afterMaking(function (Post $post) {
$post->getAuthor()->addPost($post);
});
}
}
Now $user->getPosts() returns the new post immediately — no flush, no refresh.
afterMaking() vs afterCreating()afterMaking() runs before flush. Use it for setting up references between in-memory entities —
bidirectional sync, building related graphs that must exist before persistence.afterCreating() runs after flush. Use it when the entity needs a database-assigned ID before the
next operation, or when you're modifying entities that are already persisted.class PostFactory extends DoctrineFactory
{
protected $model = Post::class;
// afterMaking: create child entities in-memory before flush — they'll be persisted together
public function withComments(int $count = 3): static
{
return $this->afterMaking(function (Post $post) use ($count) {
Comment::factory()->count($count)->for($post)->make();
});
}
// afterCreating: needs the database-assigned ID, so it runs after flush
public function withSlug(): static
{
return $this->afterCreating(function (Post $post) {
$post->setSlug($post->getId() . '-' . str($post->getTitle())->slug());
});
}
}
A handful of conventions that come up repeatedly when working with this package.
configure() for default callbacksOverride configure() to register afterMaking / afterCreating callbacks that should always run for a
factory — most commonly, the bidirectional relationship sync shown above. It's called once per factory
instance and is the right place for setup that every caller depends on.
You can use ArrayCollection (or any Doctrine Collection) directly in a definition. The factory
resolves any factories nested inside it:
use Doctrine\Common\Collections\ArrayCollection;
Post::factory()->create([
'tags' => new ArrayCollection([
Tag::factory(), // resolved to a Tag instance
Tag::factory()->make(), // already an instance, used as-is
]),
]);
This is also how you set up plain many-to-many relationships — see the Many-to-many row in the feature table.
Wrap for() / state() / afterMaking() calls in domain-specific methods that accept multiple input
types — entities, factories, or attribute arrays. Callers get a flexible API and you get a single place to
keep the relationship logic:
class PostFactory extends DoctrineFactory
{
public function forAuthor(User|UserFactory|array|null $author = null): static
{
$author ??= [];
$author = is_array($author) ? User::factory()->make($author) : $author;
return $this->for($author, 'author');
}
}
// All four usages work
Post::factory()->forAuthor();
Post::factory()->forAuthor($user);
Post::factory()->forAuthor(User::factory());
Post::factory()->forAuthor(['name' => 'Alice']);
This is more flexible than the built-in magic forUser(), which only accepts an attribute array.
When entities share an inheritance hierarchy, mirror it on the factories. An abstract base factory holds the shared definition; subclasses extend and add their own pieces:
abstract class ContentFactory extends DoctrineFactory
{
public function definition(): array
{
return [
'title' => fake()->sentence(),
'body' => fake()->paragraphs(3, true),
];
}
}
class PostFactory extends ContentFactory
{
protected $model = Post::class;
public function definition(): array
{
return [
...parent::definition(),
'author' => User::factory(),
];
}
}
class PageFactory extends ContentFactory
{
protected $model = Page::class;
public function definition(): array
{
return [
...parent::definition(),
'slug' => fake()->slug(),
];
}
}
EntityManager::refresh() escape hatchWhen the in-memory state diverges from the database — most commonly because a related entity's collection
was mutated after persistence — EntityManager::refresh($entity) re-fetches it from the database:
use LaravelDoctrine\ORM\Facades\EntityManager;
$user = User::factory()->create();
Post::factory()->for($user)->count(3)->create();
EntityManager::refresh($user);
$user->getPosts(); // now reflects the 3 new posts
Manual sync via afterMaking() (above) avoids the round-trip but requires you to know which collections
to update. refresh() is the safer fallback when you don't.
git clone git@github.com:stemble/laravel-doctrine-factory.git
cd laravel-doctrine-factory
composer install
composer test
To publish a new version of the package, create a new tag and push it to the repository:
git tag vx.x.x
git push origin vx.x.x
Go to Packagist and click on "Update" to update the package.
Most methods that this package overrides are documented next to an @override tag in the source, including
the rationale for the change. If you're working on the package itself, the doc blocks are the canonical
reference for why a given override exists.
How can I help you explore Laravel packages today?