Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laravel Doctrine Factory Laravel Package

stemble/laravel-doctrine-factory

Use Laravel's modern Eloquent factories with Doctrine entities. Drop-in replacement for the official frozen pre-8.x factory system, adding class-based factories, first-class states, relationship helpers, sequences, recycle, and afterMaking/afterCreating callbacks.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and wire up in three steps.

composer require stemble/laravel-doctrine-factory
  1. Add Laravel's standard trait to the entity — the unmodified Illuminate\Database\Eloquent\Factories\HasFactory, not a Doctrine-specific one:
/**
 * @implements HasFactory<UserFactory>
 */
class User
{
    use HasFactory;
}
  1. Create a factory extending Stemble\LaravelDoctrineFactory\DoctrineFactory, setting protected $model.

  2. Place it at database/factories/Entities/UserFactory.php for an entity at App\Entities\User. Discovery mirrors your entity namespace below App\ — same convention as Eloquent, no registration file, no service provider config.

Where to look first. Read the Laravel 11.x Eloquent factories docs, not this package's. The README is explicit that the modern factory API is the contract; the package README only documents the deviations. Then read the "Differences From Eloquent" section once, carefully — make() semantics and the constructor/reflection split are the two things that will bite you in the first hour.

First use case: migrating an existing suite. The package exists to replace laravel-doctrine/orm's frozen $factory->define(...) closures. Take your most-used entity, convert its closure to a definition() array plus a couple of state methods (->admin(), ->inactive()), and move one test file over. If that test file passes unchanged apart from admin() now being a method rather than a string state, you have validated the whole migration path for that entity and can batch the rest.

Pre-flight check before you write any factory. For the entity in question, answer:

  • Does the constructor have required parameters? (They must appear as keys in definition().)
  • Are any relationships bidirectional? (You will need configure() + afterMaking().)
  • Do any setters have side effects you rely on? (Reflection will skip them.)

An entity with none of these is trivially portable. An entity with all three is where you spend the day.

Implementation Patterns

Split definition() by construction path

Keys are routed: constructor parameter names go to the constructor, everything else is written to private properties via reflection.

public function definition(): array
{
    return [
        'name'  => fake()->name(), // constructor param
        'admin' => false,          // reflection
    ];
}

Design the array with that split in mind. If a value must go through constructor logic (normalisation, derived fields, invariants), it belongs in the constructor branch. If it is plain state, let reflection handle it.

States as first-class methods

public function admin(): static
{
    return $this->state(['admin' => true]);
}

Prefer named methods over state([...]) at call sites — they are greppable, discoverable in the IDE, and composable: User::factory()->admin()->count(3)->create().

Relationships: property names, not columns

The second argument to for() / has() is the entity property name.

$post = Post::factory()->for($user, 'author')->create();
$user = User::factory()->has(Post::factory()->count(3), 'posts')->create();

// magic methods when property name == related class basename
$post = Post::factory()->forUser()->create();
$user = User::factory()->hasPosts(3)->create();

Collections can be passed directly in a definition or override array, with nested factories resolved in place:

Post::factory()->create([
    'tags' => new ArrayCollection([Tag::factory(), Tag::factory()->make()]),
]);

For many-to-many, either set the Collection in state, or ->has() the pivot entity when pivot data matters.

Bidirectional sync in configure()

This is the pattern you will write most often. The factory sets only the owning side; the inverse collection is empty in memory until flush + refresh.

public function configure(): static
{
    return $this->afterMaking(function (Post $post) {
        $post->getAuthor()->addPost($post);
    });
}

Put it in configure() so every caller inherits it, rather than a state method people forget to chain.

afterMaking() vs afterCreating()

Use a single rule: does the callback need a database-assigned ID?

  • No → afterMaking() — bidirectional sync, building child graphs that flush with the parent.
  • Yes → afterCreating() — slug from ID, external references, anything depending on the persisted row.
public function withComments(int $count = 3): static
{
    return $this->afterMaking(function (Post $post) use ($count) {
        Comment::factory()->count($count)->for($post)->make();
    });
}

Note the make() inside — children are persisted via the identity map and flushed with the parent. No cascade: ['persist'] needed anywhere.

Build the graph, flush once

The most valuable workflow this package unlocks: assemble a complex aggregate with make(), wire it up by hand, then create() the root.

$post = Post::factory()->make();          // persisted, not flushed
$post->addTag($tag);                       // arbitrary in-memory setup
$post->getAuthor()->setTeam($team);
$post->getId();                            // still null
$post->getAuthor();                        // fully wired
// a later create() flushes the entire graph

Treat make() as "stage" and create() as "commit".

Domain helper methods

Wrap factory plumbing in intent-named methods that accept several input shapes — this is where the package pays off in test readability.

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');
}

Factory inheritance for entity hierarchies

Mirror single-table / class-table inheritance on the factory side. Abstract base holds shared definition; concrete factories spread parent::definition().

Where it fits in the suite

  • Seeders — replace closure registrations wholesale; count() and create() behave as expected.
  • Pest/PHPUnit tests — the usual Laravel patterns apply (RefreshDatabase equivalent for Doctrine, transactional wrappers). Remember create() issues a real flush(); that interacts with any transaction boundary you have set up.
  • recycle() — genuinely useful for Doctrine because it hands you the same entity instance across a batch, sidestepping the inverse-side sync problem entirely for shared parents.

Gotchas and Tips

make() persists. This is the number-one source of surprise. Made entities sit in the identity map, so any subsequent flush() — including one triggered by an unrelated create() later in the test — inserts them. If you make() something and then abandon it, you may find stray rows. Never assume "make is dirty, create is clean"; assume "make is staged, create is committed."

Missing constructor attributes throw. MissingConstructorAttributesException means a required constructor parameter has no matching key. Fix it in definition() or pass it at call time — there is no implicit default. Note it is required parameters only; optional ones can be omitted.

Reflection bypasses setters. Private properties are written directly. Validation, eventing, and collection syncing in setters will not fire. Two remedies: route the value through the constructor, or call the setter from afterMaking().

Inverse sides are empty. $post->getAuthor() returns the User; $user->getPosts() returns an empty Collection. This is not a bug and not fixable by config — Doctrine's owning/inverse model simply differs from Eloquent's dumb-data-container model. Sync in configure(), or EntityManager::refresh($user) after flush:

use LaravelDoctrine\ORM\Facades\EntityManager;

$user = User::factory()->create();
Post::factory()->for($user)->count(3)->create();

EntityManager::refresh($user);
$user->getPosts(); // now 3

Relationship names are property names. for($user, 'author') — not 'user', not 'user_id', not 'posts'. A mismatch is the most common silent-ish failure. When in doubt, omit the argument and use the magic method (forUser()), which infers from the type.

Verify afterCreating() writes. afterCreating() runs after the flush. A mutation on a managed entity there should be picked up on the next flush, but if your assertion reads the database immediately it may not see it. Assert against in-memory state, or refresh() explicitly. Confirm the actual timing early in your adoption rather than debugging a flaky assertion later.

Batching performance. ->count(50)->create() under the hood is still a flush-heavy path; for large fixtures prefer building with make() and flushing once.

Feature gaps are real, not configurable. No polymorphic relationships (no Doctrine equivalent) and no trashed()/soft deletes. hasAttached() is N/A — use a Collection in state or ->has() the pivot entity.

Extension points are two: configure() for callbacks that must always run, and factory inheritance for shared definitions across a hierarchy. There is no plugin/hook layer beyond that — which is fine, because you can drop to raw entity manipulation in afterMaking() whenever the DSL runs out.

Adoption risk is the main caveat. Two stars, zero dependents, a single maintainer, and an API surface that tracks a fast-moving upstream (Illuminate\...\Factory). Pin the version in composer.json, run it against your Laravel minor version deliberately, and keep the "Differences From Eloquent" section handy. MIT + thin-wrapper architecture means the failure mode is bounded: worst case you vendor the DoctrineFactory class and keep your factories unchanged, because the HasFactory trait and class-based factory shape are Laravel's own.

Debugging ladder. Empty inverse collection → sync in afterMaking(). Entity missing from DB → you called make() and never flushed. Wrong relation → check the property name. Odd property values → a setter you expected to run; it didn't. Stale reads → EntityManager::refresh().

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
robertmarney/lara-hierarchial-collections
simonschaufi/rule-doc-generator
emissaoja/emissaoja
blackbricksoftware/gravityforms-civicrm-form-processor
al-rahabi/fancy-crud
jinas/dhivatar-component
mltstephane/laravel-quality
bernskiold/laravel-talentlms
bernskioldmedia/laravel-form-architect
youbar/easy-crud
c-point-labs/laravel-hostyourai
dbravoan/dbcms-admin-theme-dbcms
dbravoan/dbcms-theme-db-cyber
dbravoan/dbcms-blocks-basic
dbravoan/dbcms-module-content-audit
dbravoan/dbcms-module-ai-assistant
paygate-to/paygate-php
padosoft/askmydocs-connector-base
defstudio/supercharged-carbon
bernskioldmedia/laravel-matomo