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

gedmo/doctrine-extensions

Doctrine Behavioral Extensions adds powerful behaviors to Doctrine ORM and MongoDB ODM, including Sluggable, Timestampable, Blameable, Loggable, and Translatable, easily attached via Doctrine's event system.

View on GitHub
Deep Wiki
Context7

Getting Started

Decision point first. gedmo/doctrine-extensions is a Doctrine ORM/ODM add-on, not an Eloquent one. It buys you nothing unless you already run Doctrine in your Laravel app (typically via laravel-doctrine/orm). If your models are Eloquent, the honest answer is to skip this package — the friction of splitting your persistence layer is larger than anything Timestampable or Sluggable saves you. If you do run Doctrine (CMS modules, legacy Symfony ports, event-sourced admin panels), this is close to mandatory infrastructure.

Minimal setup in a Laravel app that already has Doctrine wired up:

composer require gedmo/doctrine-extensions

Then register only the listeners you actually use, per entity manager. With laravel-doctrine/extensions:

// config/doctrine.php
'managers' => [
    'default' => [
        'meta' => 'attribute',
        'extensions' => [
            Gedmo\Timestampable\TimestampableListener::class,
            Gedmo\Sluggable\SluggableListener::class,
        ],
    ],
],

If you wire Doctrine manually, add them to the event manager yourself (order matters, see below):

$evm = $em->getEventManager();
$evm->addEventSubscriber(new TimestampableListener());
$evm->addEventSubscriber(new SluggableListener());

First use case — a Post with zero-effort timestamps and a URL slug:

#[ORM\Entity]
#[ORM\Table(name: 'posts')]
class Post
{
    #[ORM\Id, ORM\GeneratedValue, ORM\Column(type: 'integer')]
    private ?int $id = null;

    #[ORM\Column(type: 'string', length: 200)]
    private string $title = '';

    #[Gedmo\Sluggable(fields: ['title'])]
    #[ORM\Column(type: 'string', length: 255, unique: true)]
    private string $slug = '';

    #[Gedmo\Timestampable(on: 'create')]
    #[ORM\Column(type: 'datetime_immutable')]
    private \DateTimeImmutable $createdAt;

    #[Gedmo\Timestampable(on: 'update')]
    #[ORM\Column(type: 'datetime_immutable')]
    private \DateTimeImmutable $updatedAt;
}
$post = new Post();
$post->setTitle('Hello World');
$em->persist($post);
$em->flush();          // slug = "hello-world", createdAt/updatedAt populated

Read first: doc/timestampable.md, doc/sluggable.md, docs/attributes.md (attributes are the supported path; annotations are deprecated and removed in 4.0), and doc/frameworks/laravel.md. Use the example/em.php bootstrap if your EntityManager setup throws listener/metadata errors.

Implementation Patterns

Timestampable / Blameable via mapped superclass. Don't copy timestamp fields into 40 entities. Extend a BaseEntity with #[ORM\MappedSuperclass] carrying createdAt, updatedAt, createdBy, updatedBy. This is the single highest-leverage use of the package in a day-to-day codebase.

Blameable needs a per-request user. The listener holds state, so push the authenticated user at boot (or from middleware) into the registered instance:

$evm = app('em')->getEventManager();
foreach ($evm->getListeners(\Doctrine\ORM\Events::onFlush) as $listener) {
    if ($listener instanceof \Gedmo\Blameable\BlameableListener) {
        $listener->setUserValue(auth()->user()); // object for relations, string/id for scalar fields
    }
}

Do the same for TranslatableListener::setTranslatableLocale() — set it in a locale middleware, or per entity via the Gedmo\Translatable\Translatable interface when you translate several records in one request.

Sluggable — treat it as URL infrastructure. Use fields: ['title'] for simple cases, unique: true plus a DB unique index for public routes, and a custom SlugHandlerInterface (e.g. tree path handlers) when the slug must include parent segments. Set updatable: false on anything that has been published; regenerating slugs silently is a broken-links generator.

SoftDeleteable — opt-in via the SQL filter, not the listener alone. The listener only sets the date; you must enable the filter on each manager:

app('em')->getFilters()->enable('soft-deleteable');

For admin views, disable it around the query (->disable(), then re-enable in a finally) or expose a withTrashed() repository method. Keep this in one place — scattered toggles are how "deleted" rows leak into customer-facing listings.

Tree — pick by read pattern. Nested set (#[Gedmo\Tree(type: 'nested')] + TreeLeft/TreeRight/TreeLevel) is ideal for menu/category hierarchies read far more than written: getChildren(), getPath(), childrenHierarchy(), moveDown(). Materialized path is easier to write and cheaper to move; closure is best when you need arbitrary ancestor queries but costs an extra table. Use one strategy per aggregate.

Translatable. Requires a translation entity (extend Gedmo\Translatable\Entity\MappedSuperclass\AbstractTranslation), a locale per request, and the walker for reads:

$query->setHint(
    \Doctrine\ORM\Query::HINT_CUSTOM_OUTPUT_WALKER,
    \Gedmo\Translatable\Query\TreeWalker\TranslationWalker::class
);
$query->setHint(\Gedmo\Translatable\TranslatableListener::HINT_FALLBACK, 1);

Loggable for audit trails: mark the entity #[Gedmo\Loggable], the fields #[Gedmo\Versioned], and register a LogEntry entity extending AbstractLogEntry. Set LoggableListener::setUsername(...) from the same middleware as Blameable.

Sortable for manually ordered collections (banners, dashboard widgets): #[Gedmo\SortablePosition] plus a class-level #[Gedmo\Sortable(group: ['category'])]. Wrap reorders in $em->transactional() so positions can't tear.

General workflow tip: wrap mutations in transactions and keep one flush() per logical operation. Gedmo listeners run inside onFlush/prePersist, so they see the whole UnitOfWork — flushing per-row makes Sluggable and Sortable do N queries instead of 1.

Gotchas and Tips

  • The listener does nothing. 90% of "Gedmo is broken" reports are one of: wrong EntityManager (multi-manager apps), stale metadata cache (php artisan doctrine:clear:metadata:cache after adding attributes), or a missing use Gedmo\Mapping\Annotation as Gedmo; so #[Gedmo\Sluggable] resolved to the wrong class.
  • Attributes only for new code. Annotations require doctrine/annotations and are slated for removal in 4.0. If you're on Laravel + PHP 8, never start with annotations.
  • SoftDeleteable and unique indexes fight. A unique email or slug blocks re-creating a record whose predecessor is soft-deleted. Either drop the DB constraint for those columns, or partial-index/nullable-token it; don't discover this in production.
  • The soft-delete filter leaks into everything. It rewrites SQL for the whole manager, including DQL joins in reports and any raw DBAL query expectations. If a count is inexplicably low, check the enabled filters first.
  • Queue workers and Octane hold listener state. Blameable user, Translatable locale, and Uploadable paths persist between jobs/requests. Set them at the start of each job and call $em->clear() between units of work; otherwise job #2 gets job #1's user as createdBy.
  • Sluggable uniqueness is a SELECT. For bulk imports, rows pending in the same flush aren't visible to it — dedupe source values in application code before flushing batches.
  • Nested set is write-expensive and lock-sensitive. Moving a node rewrites left/right values for large subtrees; concurrent writes can corrupt the tree. Always mutate inside a transaction, and keep the tree:recover-style verification in your ops toolbox.
  • Timestampable timezone is your PHP timezone, not Laravel's cast logic. Align date_default_timezone_set() with config('app.timezone'), or timestamps drift against Eloquent-written tables.
  • Loggable is DBAL-version-sensitive (DBAL ^3.2, not ^4.0) and grows a table forever. Plan retention/archiving from day one.
  • Uploadable leaves orphan files when a transaction rolls back after the file write. Hook a cleanup job on failure, and set a default path/storage config explicitly — the listener throws rather than guessing.
  • Version pinning. 3.x needs PHP 8+, ORM 2.14/3.0; 4.0 removes annotations. Check the v2.4→v3.0 upgrade doc before bumping, and pin the major in composer.json since listener APIs changed.
  • Testing. Register listeners manually in unit tests ($evm->addEventSubscriber(new TimestampableListener())) on an in-memory SQLite EM; don't assume the framework wiring is active.
  • Extension points worth knowing: custom SlugHandlerInterface implementations, custom tree strategies via AbstractTreeListener, a custom LogEntry class, and a custom Gedmo\Mapping\Driver if you have in-house mapping metadata.
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.
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle
glavweb/extra-bundle
glavweb/api-bundle
glavweb/admin-bundle