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 Bundle Laravel Package

mkietlinski/doctrine-extensions-bundle

A Symfony bundle that integrates Gedmo's Doctrine extensions, making it easy to register and configure behaviors like timestampable, sluggable, softdelete, tree, translatable, and blameable across your entities with minimal setup.

View on GitHub
Deep Wiki
Context7

Getting Started

First, establish what stack you're actually on. This package is a Symfony bundle (Bundle class + DependencyInjection extension + stof_doctrine_extensions config tree). Laravel's container never discovers or boots it, so on a Laravel/Eloquent codebase there is no day-to-day usage to speak of — composer require gets you a namespace nothing calls. If you are on laravel-doctrine/orm, skip this package and depend on gedmo/doctrine-extensions directly; the registration is ~20 lines in a service provider.

If you are on Symfony + Doctrine ORM, the minimal path is:

  1. composer require mkietlinski/doctrine-extensions-bundle and register Stof\DoctrineExtensionsBundle\StofDoctrineExtensionsBundle in config/bundles.php (Flex auto-registers it if the recipe exists; this fork probably has none, so register by hand).
  2. Create config/packages/stof_doctrine_extensions.yaml and enable one extension on one entity manager:
    stof_doctrine_extensions:
        orm:
            default:
                timestampable: true
    
    The default key must match an entity manager name from doctrine.yaml (doctrine.orm.entity_manager → default). A mismatch is the single most common "why isn't this firing" cause.
  3. Annotate a throwaway entity with #[Gedmo\Timestampable(on: 'create')] on createdAt.
  4. bin/console cache:clear && bin/console doctrine:schema:update --dump-sql (or make a migration).
  5. Persist and flush one row. Verify createdAt is populated — this proves the listener is subscribed, which is the only thing the bundle actually does for you.

Where to look first, in order: the fork's DependencyInjection/Configuration.php (the real, authoritative list of what is togglable — the README is a copy of upstream and may drift), then diff that file against upstream stof/doctrine-extensions-bundle so you know what you're actually running, then upstream's docs for entity-side attribute syntax (those are Gedmo's, not the bundle's).

First real use case: Timestampable + SoftDeleteable on one low-risk entity. Those two are the highest value-to-risk ratio and exercise both the write path (timestamps) and the read path (the softdeleteable query filter), which are the two places this class of package breaks.

Implementation Patterns

Config is per-entity-manager, and that's the main lever. Everything under orm.<em_name> is registered only against that EM's EventManager. Multi-EM apps (read/write split, tenant connections) should enable extensions explicitly per EM rather than globally — enabling loggable or translatable on a EM that doesn't need it adds a listener on every flush for no return.

Enable behavior-by-behavior, not all at once. Start with timestampable, ship, then add sluggable, then softdeleteable. Each one changes query shape or write behavior, and a combined rollout makes it impossible to attribute a regression to a specific listener.

Entity side is plain Gedmo attributes — the bundle contributes nothing here:

#[Gedmo\Timestampable(on: 'create')]
private ?\DateTimeImmutable $createdAt = null;

#[Gedmo\Slug(fields: ['title'], unique: true)]
private ?string $slug = null;

#[Gedmo\SoftDeleteable(fieldName: 'deletedAt', timeAware: false, hardDelete: false)]
class Post { private ?\DateTimeImmutable $deletedAt = null; }

Soft delete is a read-filter, not a column. Day-to-day this means:

  • Normal queries automatically exclude trashed rows.
  • Admin/recycle-bin screens must explicitly disable the filter for that query only:
    $em->getFilters()->disable('softdeleteable');
    try { /* query including deleted */ } finally { $em->getFilters()->enable('softdeleteable'); }
    
    Wrap in try/finally — a filter left disabled leaks deleted records into unrelated responses in the same request, including API serialization.
  • Restoring is $entity->setDeletedAt(null); $em->flush(); — no unfiltered query needed if you already hold the reference.

Blameable needs a user source. The listener has no opinion about who is logged in. Wire it once in a kernel/request listener or a Doctrine event subscriber that resolves the current user from the security token and calls setUserValue() on the gedmo.listener.blameable (aliased by the bundle) service before flush. Do this before you enable blameable on any entity, otherwise every row is attributed to null.

Loggable is the audit trail; pair it with a retention decision. It writes a LogEntry-style row per change per entity. On write-heavy tables budget for the secondary write and set a cleanup job from day one — retrofitting a purge onto a bloated audit table is a migration project.

Translatable: set the locale explicitly per operation. The listener reads the locale from its own state, not from the request. Set it ($listener->setTranslatableLocale($locale) or via the TranslatableListener service) and reset it after the flush, or you'll get the previous request's locale leaking into the next one under long-running workers.

Tree (nested set) day-to-day: use the tree repository from $em->getRepository(Category::class) (it's a NestedTreeRepository when the mapping is present), and treat every insert/move as a multi-row write. Don't call moveUp()/moveDown() in a loop over a large set — batch the operations and wrap them in a transaction.

Testing pattern that actually catches failures: assert the effect, not the absence of exceptions. assertNotNull($post->getCreatedAt()) after flush, assertNotContains($deleted, $repo->findAll()) before disabling the filter and assertContains(...) after. A listener that failed to register produces no error — only a null column — so error-free test runs prove nothing here.

Gotchas and Tips

  • Silent non-registration is the failure mode. timestampable: false or a missing key produces a perfectly bootable app where timestamps are simply never written. After any config change, run bin/console cache:clear (and doctrine:cache:clear-metadata outside dev) and re-verify with a real flush. YAML config is compiled into the container; stale cache means your edit did nothing.
  • Entity manager name mismatch. stof_doctrine_extensions.orm.<name> must exactly match a configured EM. Typos are accepted silently at config level and produce no listeners at runtime.
  • Metadata cache in dev. Attribute changes on entities are also cached. If a new #[Gedmo\Slug] isn't taking effect, it's the metadata cache, not the bundle.
  • Soft-delete filter leakage is a data-exposure bug, not a correctness nit. Any getFilters()->disable() without a finally re-enable is a latent incident, especially in queues and long-lived workers where the EM survives between jobs.
  • Fork provenance. The Composer name (mkietlinski/doctrine-extensions-bundle) differs from the repository name (StofDoctrineExtensionsBundle) — the fork retains the Stof\ namespace and the stof_doctrine_extensions config key. That's annoying for auditing but genuinely useful operationally: swapping to upstream stof/doctrine-extensions-bundle is a composer.json line plus a bundle-class check, with zero config migration. Pin the exact version ("mkietlinski/doctrine-extensions-bundle": "x.y.z"), not a caret range, so a fork-side change can't arrive silently.
  • Pin gedmo/doctrine-extensions alongside it. Gedmo tracks Doctrine ORM internals closely and breaks across minor ORM upgrades. A transitive bump that satisfies the fork's constraint but not your ORM's mapping expectations shows up as a bootstrap mapping exception — loud, but it blocks unrelated dependency upgrades until resolved.
  • Nested-set corruption. An interrupted tree operation (killed worker, uncaught exception mid-flush) leaves left/right bounds inconsistent, and no error is raised. If tree queries start returning odd subsets, plan for verify()/tree rebuild rather than debugging queries.
  • Extension points are limited and upstream-shaped. Where the bundle's toggles aren't enough (e.g. Blameable user resolution, Translatable locale plumbing), you'll write a small subscriber or a compiler pass against the gedmo.listener.* services — the bundle exposes them and steps aside. Don't patch the vendor tree; the fork has no maintainer throughput to absorb or upstream your patches.
  • Escape hatch. If anything about registration becomes unclear, register the Gedmo listener directly on $em->getEventManager()->addEventSubscriber(...) in a small service. It's ~5 lines per listener and removes the bundle from your debugging surface entirely — a reasonable trade for a package with no dependents and no community issue history to search.
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.
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console