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

Doctrine Extensions Laravel Package

mkietlinski/doctrine-extensions

mkietlinski/doctrine-extensions provides reusable Doctrine ORM extensions for PHP projects, helping reduce boilerplate and add common behavior to your Doctrine setup. Install via Composer and integrate with your existing entity manager configuration.

View on GitHub
Deep Wiki
Context7

Getting Started

This package is a Thin Laravel-flavored wrapper around Doctrine behavioral extensions (Gedmo lineage): Timestampable, Sluggable, SoftDeleteable, Blameable, Tree, Loggable, Translatable, Sortable. It is only meaningful if your app already runs Doctrine ORM in Laravel (typically via laravel-doctrine/orm). On an Eloquent-only app, stop here.

Minimum steps to first value:

  1. Confirm the stack assumption.

    composer show doctrine/orm laravel-doctrine/orm 2>/dev/null | grep -E "name|versions"
    

    If doctrine/orm is absent, this package is not for your app.

  2. Audit composer.json before installing anything else. Check the require block for the Doctrine ORM constraint. ORM 2.x vs 3.x is the single biggest breaking variable — ORM 3 dropped annotations in favor of attributes, changed UnitOfWork internals, and rewired listeners. If the constraint is ^2.0, plan for a version pin and a future migration.

  3. Diff against upstream. Because this is a fork of gedmo/doctrine-extensions, the first question is what does it add? Clone both and:

    git clone https://github.com/MKietlinski/DoctrineExtensions /tmp/fork
    git clone https://github.com/doctrine-extensions/DoctrineExtensions /tmp/upstream
    diff -r /tmp/fork/src /tmp/upstream/src | head -200
    

    If the diff is immaterial (namespace changes, tiny patches), depend on upstream gedmo/doctrine-extensions directly and skip this fork. If the diff carries a needed fix, vendor the code into app/Doctrine/Behaviors/ rather than pulling a 0-star dependency.

  4. Find the registration path. Look at:

    • src/ — grep for EventSubscriber, loadClassMetadata, onFlush, prePersist, postPersist. This tells you which hooks exist and therefore which behaviors are actually wired.
    • *ServiceProvider — if one exists, it shows the intended config-publishing route and default listener registration.
    • tests/ — if thin, you'll write your own integration test as part of adoption.
  5. Register listeners with the Laravel Doctrine bridge. With laravel-doctrine/orm, extensions live in config/doctrine.php under managers.default.extensions:

    // config/doctrine.php
    'managers' => [
        'default' => [
            'meta' => 'attributes',
            'extensions' => [
                \MKietlinski\DoctrineExtensions\Timestampable\TimestampableListener::class,
                \MKietlinski\DoctrineExtensions\SoftDeleteable\SoftDeleteableListener::class,
            ],
        ],
    ],
    
  6. First use case — Timestampable on one entity. It's prePersist/preUpdate only, doesn't touch queries, and proves the plumbing end-to-end:

    #[ORM\Entity]
    #[Gedmo\Timestampable(on: 'create')] // adjust namespace to what the package ships
    class Invoice
    {
        #[ORM\Column(type: 'datetime_immutable')]
        private ?\DateTimeImmutable $createdAt = null;
    
        #[ORM\Column(type: 'datetime_immutable')]
        private ?\DateTimeImmutable $updatedAt = null;
    }
    

    Persist, flush, assert createdAt is non-null. That is the whole smoke test.

Implementation Patterns

Behavior-by-behavior roll-out, in risk order. Enable in this sequence, with a test per behavior before moving on:

  1. Timestampable — query-neutral, safe.
  2. Blameable — needs a user-provider callback; safe once wired (BlameableListener::setUserValue($user)).
  3. Sluggable — pure application-side, but plan slug uniqueness at the DB layer.
  4. SoftDeleteable — changes every query plan on the entity; test all repositories touching it.
  5. Sortable / Tree — ordering and hierarchy mutations; concurrency-relevant.
  6. Translatable / Loggable — schema-heavy; defer unless the product explicitly requires them.

Configuration as attributes on the entity. This is the idiomatic usage — behaviors are declared inline rather than registered centrally:

#[ORM\Entity]
#[Gedmo\Sluggable(fields: ['title'], slugField: 'slug')]
#[Gedmo\SoftDeleteable(fieldName: 'deletedAt', timeAware: true)]
class Post
{
    #[ORM\Column(type: 'string', length: 255, unique: true)]
    private string $slug;

    #[ORM\Column(type: 'datetime_immutable', nullable: true)]
    private ?\DateTimeImmutable $deletedAt = null;
}

Keep the behavior attribute and the underlying column definition together — this pairing is the thing you'll be re-reading six months later.

Wire Blameable via a request lifecycle listener. Blame is the only common behavior that needs runtime state (the authenticated user). Register it in a Laravel listener or a boot hook:

// app/Providers/DoctrineServiceProvider.php
Event::listen(\Illuminate\Auth\Events\Authenticated::class, function ($e) {
    app(\MKietlinski\DoctrineExtensions\Blameable\BlameableListener::class)
        ->setUserValue($e->user);
});

Without this, Blameable silently writes null on every insert — a debugging trap.

Soft-delete is a query-level filter, not a column. SoftDeleteable hides rows at the ORM level; find() returns null, findBy() excludes them. Any nativeQuery, DB::table(), or raw EXPLAIN you write will still see them. Wrap "include deleted" cases explicitly (the package typically exposes an isSoftDeleteable filter switch — grep the listener for the filter-disabling method).

Repository patterns to pair with behaviors.

// Restore a soft-deleted row
$em->getFilters()->disable('soft-deleteable');
$post = $em->getRepository(Post::class)->find($id);
$post->setDeletedAt(null);
$em->flush();

Do the filter toggling in a service, not scattered across controllers, or it will leak into unrelated queries.

Cross-cutting audit via Loggable-style listeners in tests. If the fork ships a Loggable behavior, use it for compliance-flavored tests (who changed what when) rather than hand-asserting timestamps in every feature test.

Doctrine migrations integration. Behaviors add metadata; they do not create columns. Write the columns into your doctrine/migrations diff, then let behaviors populate them — never rely on the behavior to "just work" against a schema that lacks created_at etc.

Test pattern for behavior correctness. Assert at the entity layer, not the HTTP layer:

it('timestamps on create', function () {
    $invoice = new Invoice();
    $em->persist($invoice);
    $em->flush();
    expect($invoice->getCreatedAt())->not->toBeNull();
});

Behavior bugs are query/state bugs, and HTTP tests hide them.

Gotchas and Tips

The fork's reason for existing is the first thing to understand. Without a diff against gedmo/doctrine-extensions, you inherit unknown behavioral deltas — subtly different slug collision handling, changed translatable fallback, altered soft-delete filter semantics. Diff before adopting.

OR M major version pin is the highest-likelihood silent breakage. A doctrine/orm bump can leave listeners registered but inert — the loadClassMetadata hook fires but behavior attributes aren't recognized, columns stay null, and no exception is raised. Add a test asserting behavior on every major upgrade.

Metadata driver mismatch is the most common silent failure. Attributes on entities but meta => 'annotations' in config/doctrine.php (or vice versa) means behaviors quietly no-op. Verify by dumping metadata once after setup:

$meta = $em->getClassMetadata(Invoice::class);
dump($meta->getName(), array_keys($meta->fieldMappings));

If the behavior field isn't present, the driver is mismatched.

Soft-delete filters exclude more than you think. Counts, aggregates, joins, and admin screens all go through the filter. Reports that "don't add up" after enabling SoftDeleteable are almost always this. EXPLAIN any high-traffic repository query post-adoption.

Slug uniqueness is application-layer only. Two concurrent inserts can race to the same slug. Enforce a unique index in the migration; if the package's default collision handler isn't sufficient, override the slug listener or add a retry.

Blameable silently nulls without a user provider. If BlameableListener::setUserValue() isn't called per-request, every createdBy is null and no error surfaces. Wire it via Auth::user() in an event listener, and test that the user is actually set.

Translatable fallback returns null, not the default locale, unless configured. Set the fallback explicitly; otherwise i18n pages render blank in untranslated locales.

Tree mutations on hot tables risk deadlocks under concurrency. Nested-set updates rewrite parent paths; under concurrent writes this serializes and can deadlock. Batch tree moves through a queue for anything beyond low-volume admin editing.

Loggable is write-amplifying. Plan table growth and retention from day one; a loggable entity on a busy table can dominate insert cost.

Behavior attributes are not magic — grep the listener when in doubt. The fastest debugging route is opening the behavior's subscriber and reading loadClassMetadata and onFlush. With no community, there's no Stack Overflow answer — the source is the documentation.

Pin to a commit SHA and plan to vendor. With 0 stars, 0 dependents, and a single maintainer, composer update is not a safe path. Pin:

"require": {
    "mkietlinski/doctrine-extensions": "dev-main#<sha>"
}

And have a fork/vendor plan before the package becomes load-bearing.

Prefer upstream if the diff is thin. gedmo/doctrine-extensions has community, issue history, and CI. The Laravel gluing in this package is usually a service provider and a config array — trivially reproducible in your own app/Providers. Do that if the diff audit shows the fork adds nothing you need.

Extension points are Doctrine-native. Custom behavior is a Doctrine\Common\EventSubscriber implementing loadClassMetadata and onFlush. If the package doesn't ship a behavior you need, write the subscriber yourself — it's ~150 lines and you own the code.

Ramp-up estimate. ~1–2 days for a Doctrine-literate engineer to read the listeners and confirm behavior configuration; ~1–2 weeks to safely roll out a standard behavior set with tests and query-plan validation. If your team is Eloquent-native, the ramp cost exceeds the package's value.

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.
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core
nodisrupt/laravel-agent
sierratecnologia/media-manager