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

fsi/doctrine-extensions-bundle

FSi DoctrineExtensionsBundle integrates Gedmo/DoctrineExtensions into Symfony, providing configuration and service registration for ORM extensions like Timestampable, Sluggable, Tree, Translatable, SoftDeleteable, and Blameable with minimal setup.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and register. The bundle is a thin Symfony integration layer over fsi/doctrine-extensions; it wires the library's Doctrine event subscribers into the container so you don't hand-register them.

composer require fsi/doctrine-extensions-bundle
// config/bundles.php
return [
    // ...
    FSi\Bundle\DoctrineExtensionsBundle\FSiDoctrineExtensionsBundle::class => ['all' => true],
];

First place to look: the bundle's config tree, not the README. With only 3 stars and no dependent packages, docs are thin and may lag the code. Run bin/console config:dump-reference fsi_doctrine_extensions first — that gives you the authoritative, version-matched key list. Then confirm what was actually registered:

bin/console debug:container --tag=doctrine.event_subscriber | grep -i fsi
bin/console debug:event-dispatcher doctrine.event_subscriber

Minimal config. Extensions are enabled per Doctrine connection (so a multi-EM app only pays for listeners where needed):

# config/packages/fsi_doctrine_extensions.yaml
fsi_doctrine_extensions:
    orm:
        default:
            timestampable: true
            blameable: true
            softdeleteable: true
            sortable: true

First use case — an auditable entity. Start with Timestampable + Blameable on one entity. Add the library trait to the entity, persist it, and confirm createdAt/updatedAt and createdBy/updatedBy populate without you touching them:

namespace App\Entity;

use Doctrine\ORM\Mapping as ORM;
use FSi\DoctrineExtensions\Timestampable\TimestampableTrait;
use FSi\DoctrineExtensions\Blameable\BlameableTrait;

/**
 * @ORM\Entity
 */
class Article
{
    use TimestampableTrait;
    use BlameableTrait;

    /** @ORM\Id @ORM\GeneratedValue @ORM\Column(type="integer") */
    private ?int $id = null;

    /** @ORM\Column(type="string", length=255) */
    private string $title = '';
}

If the columns appear after bin/console doctrine:schema:update --dump-sql, the traits are being read correctly by the mapping driver and the subscribers are live. That's your green light for everything else.


Implementation Patterns

Trait-per-entity, opt-in per aggregate

Do not enable the traits on a base entity that everything inherits. Doctrine's annotation driver reads trait properties as if declared on the class, so inheritance works, but it drags audit columns and soft-delete semantics onto join tables, log rows, and value objects where they're noise. Compose per aggregate root:

class Invoice
{
    use TimestampableTrait;   // createdAt / updatedAt
    use BlameableTrait;       // createdBy / updatedBy
    use SoftDeletableTrait;   // deletedAt
    use SortableTrait;        // position
}

Blameable needs a user resolver wired to your security stack

The listener asks a resolver "who is acting right now?". The bundle's default resolver reads the Symfony token storage, so it works out of the box with the standard security bundle. In a custom auth setup (API keys, service-to-service, impersonation), decorate the resolver rather than forking the listener:

# config/services.yaml
services:
    App\Doctrine\ApiKeyUserResolver:
        decorates: fsi_doctrine_extensions.blameable.user_resolver
        arguments: ['@security.token_storage', '@request_stack']

Verify the actual service id with bin/console debug:container | grep -i blameable — the id changed across versions, and the decorator silently does nothing if you typo it.

Soft-delete: filter on by default, off by explicit exception

SoftDeletable installs a DQL filter so find()/findBy()/joins skip tombstoned rows. That's the right default. Flip it off only in the specific workflows that need it — an admin "trash" screen, an undelete action, or a GDPR purge job:

$filters = $em->getFilters();
$filterName = 'soft-deletable'; // confirm: $filters->getEnabledFilters()
$filters->disable($filterName);

$deleted = $em->getRepository(Invoice::class)
    ->findBy(['deletedAt' => /* not null */]);

$filters->enable($filterName);

Always re-enable in a finally. A leaked disabled filter in a long-running worker means deleted records silently reappear in unrelated code paths — one of the nastiest bugs to diagnose.

Sortable: treat position as derived state

The listener maintains an integer position per entity (optionally per group). Practical rules:

  • Exclude position from forms and API payloads — it's owned by the listener.
  • Reorder by moving entities through the library's API, then flush() once; don't setPosition() in a loop and flush per row.
  • Add a composite unique index on (group_column, position) so a concurrent insert fails loudly instead of producing two rows with the same slot.

Fixtures and tests

Listeners fire in fixtures too, which makes assertions brittle. In doctrine:fixtures:load and in KernelTestCase setups, either disable the extensions in the test connection config or set the user resolver to a deterministic fake. For unit tests of the entity itself, the traits are plain properties — you can test them without a kernel.

Pairing with API Platform / forms

Because audit columns are populated by listeners during flush, they survive API Platform's serializer round-trip untouched as long as they're on the read group and not the write group. Same for deletedAt. The common mistake is exposing updatedAt as writable — clients then pin it and you lose the audit trail.


Gotchas and Tips

Do not run this alongside stof/doctrine-extensions-bundle (Gedmo) on the same entity. Both ship timestampable/blameable/softdeleteable listeners. You get duplicate column writes, inconsistent deletedAt semantics, and two soft-delete filters. Pick one library for a given entity, or scope them to different entity managers.

The library predates PHP 8 attributes. Last release 2021-07-26. Doctrine annotations, XML, or YAML mapping are safe; #[ORM\Column] on trait-injected properties may not be picked up depending on the pinned doctrine/orm. If columns don't appear in schema:update --dump-sql, that's the first thing to check — redeclare the mapping in the entity or stay on annotations.

Compatibility risk is real. Zero dependents and a 2021 last release means Symfony 6.x / Doctrine ORM 3.x support is unverified. Before adopting, run composer why-not fsi/doctrine-extensions-bundle against your target stack and check the library's open issues. For greenfield projects, weigh Gedmo or hand-rolled LifecycleEventSubscribers — these listeners are ~50 lines each to reproduce.

Blameable is null in console/CLI context. No token means no user, so createdBy is null for any row created by bin/console commands, message consumers, or cron. If that's unacceptable, wrap those entry points in a synthetic token (Security::setToken()) or your resolver should fall back to a system principal.

Soft delete breaks unique constraints. A UNIQUE(email) on a soft-deletable User means the tombstoned row still blocks re-registration. Either drop the DB-level constraint for a partial index, or null out the unique column in a preSoftDelete hook.

Soft delete and getReference() / DQL joins. The filter applies to DQL, not to proxy initialisation in every case, and raw ->getConnection()->executeQuery() obviously bypasses it entirely. Audit any raw SQL touching soft-deletable tables.

Uploadable leaks files. If you use the upload extension, deleting or replacing a file leaves the old object in storage unless the entity/lifecycle is configured correctly. Treat the storage bucket as eventually-consistent garbage and run a reconciliation job.

Timestampable granularity. These are \DateTime columns on this vintage of the library, not DateTimeImmutable — mutation is possible. Also check the column type: datetime truncates to the second, so ordering by createdAt on burst inserts can tie. Use datetime_immutable/datetimetz in your own override if ordering matters.

Debugging checklist when columns don't populate:

  1. bin/console debug:container --tag=doctrine.event_subscriber — is the listener registered at all?
  2. Is the extension enabled for the right connection name in fsi_doctrine_extensions.orm.<name>? A mismatch with doctrine.dbal.default_connection is the #1 cause.
  3. Is the trait actually used on the concrete class (not just the parent)?
  4. Is deletedAt/createdAt in the flush() diff? If Doctrine sees no change, the listener never ran.
  5. Clear the metadata cache: bin/console cache:clear after any mapping change — stale annotation metadata is a frequent false negative.

Extension points. The cleanest customization surfaces are (a) decorating the user resolver for blameable, (b) decorating the listener service itself to add a post-write hook (e.g. publish a domain event after updatedAt changes), and (c) supplying your own storage abstraction for uploadable. Forking the bundle should be a last resort; with a 3-star single-maintainer codebase, a fork is cheap but so is replacing it.

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.
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
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