laravel-doctrine/extensions
Adds common Doctrine behavioral extensions to Laravel Doctrine, including soft deletes, timestamps, blameable, sortable, sluggable, tree, loggable and more, with config and service provider wiring so these behaviors work smoothly in your Laravel models.
laravel-doctrine/extensions is the glue package that wires the Gedmo doctrine-extensions behaviour library (SoftDeleteable, Timestampable, Blameable, Sluggable, Loggable, Tree, Sortable, Translatable, Uploadable, IpTraceable) into a laravel-doctrine/orm entity manager. You don't call it directly — you register extension classes in config, annotate entities, and the package attaches the right event subscribers.
2.0.3 is a compatibility-only release (Laravel v13 support). There are no API changes, no new behaviours, no deprecations. Treat the upgrade as routine, but confirm your laravel-doctrine/orm version resolves alongside it.
Minimal setup:
composer require laravel-doctrine/extensions
extensions key in config/doctrine.php. That array is the single source of truth for which behaviours are active. The package's README maps each config class to the Gedmo behaviour it enables — skim that table before writing any code.php artisan doctrine:info and php artisan doctrine:mapping:info to confirm your manager is booting and your entities are discovered.// config/doctrine.php
'extensions' => [
LaravelDoctrine\Extensions\SoftDeleteable\SoftDeleteableExtension::class,
LaravelDoctrine\Extensions\Timestampable\TimestampableExtension::class,
],
#[ORM\Entity]
#[Gedmo\SoftDeleteable(fieldName: 'deletedAt', timeAware: false)]
class Post
{
#[ORM\Column(type: 'datetime_immutable', nullable: true)]
private ?\DateTimeImmutable $deletedAt = null;
#[Gedmo\Timestampable(on: 'create')]
#[ORM\Column(type: 'datetime_immutable')]
private \DateTimeImmutable $createdAt;
}
Then push the schema (php artisan doctrine:schema:update --force in dev, a real migration anywhere else) and run php artisan doctrine:clear-metadata-cache — the listener set is only rebuilt when metadata is re-read.
Config-gated behaviours, entity-level opt-in. The extension classes are global listeners; the annotations decide which entities they touch. Keep the extensions array explicit rather than copying the whole README list — every enabled behaviour adds subscribers to the event manager and filters to the EM, which is measurable overhead and a debugging hazard when something mutates an entity unexpectedly.
Keep the config in version control, not in a published vendor file. Publish config/doctrine.php once and treat extensions as code. A common pattern:
'extensions' => array_filter([
SoftDeleteableExtension::class,
TimestampableExtension::class,
BlameableExtension::class,
app()->environment('production') ? LoggableExtension::class : null,
]),
Blameable needs an explicit user-resolution seam. Gedmo's listener has no idea about Laravel's auth guards. Set the user once per request in a middleware and once per job in a job middleware:
public function handle($request, Closure $next)
{
$listener = $this->em->getEventManager()->getListeners('prePersist');
// or resolve the Gedmo listener from the container and ->setUser(Auth::user())
return $next($request);
}
Wrap Blameable in a small service class so console commands, queued jobs, and impersonation all go through the same code path.
Soft delete + "admin sees everything" pattern. Don't sprinkle getFilters()->disable() through controllers. Put it in a dedicated repository method or a query scope so the filter is re-enabled in a finally block — a leaked disabled filter silently resurrects deleted records for the rest of the request.
public function withTrashed(): static
{
$this->em->getFilters()->disable('soft-deleteable');
return $this;
}
Pair Timestampable with immutable datetime columns. Gedmo works with both DateTime and DateTimeImmutable, but only if the entity property type matches the column type. Pick one convention (datetime_immutable + \DateTimeImmutable) and enforce it across the codebase.
Sluggable for URLs, Loggable for audit. Sluggable is a write-time concern; Loggable produces an ext_log_entries table that grows without bound. If you enable Loggable, decide up front on a retention/pruning job, and exclude high-churn entities with #[Gedmo\Loggable(logEntryClass: ...)]-adjacent config or by simply not annotating them.
Upgrade workflow for a release like 2.0.3. Bump, then:
composer update laravel-doctrine/extensions --with-dependencies
php artisan doctrine:clear-metadata-cache
php artisan doctrine:schema:validate
Smoke-test the four things that actually break on framework bumps: soft-delete filter still hides rows, timestamps still populate on create and update, Blameable resolves a user in HTTP context, and Translatable returns the fallback locale.
Testing. Use the Laravel Doctrine testing traits rather than Eloquent's RefreshDatabase. In tests that assert a row is gone, remember the soft-delete filter is on by default — assertNull($em->find(Post::class, $id)) passes for a soft-deleted row. Assert against raw DBAL or explicitly disable the filter.
$em->getConnection()->executeQuery(...) will return soft-deleted rows. So will ->createNativeQuery(). There is no fix; it's how SQL filters work.null written to the createdBy column — or a TypeError if the property isn't nullable. Either make the columns nullable or set the user before dispatching work.config:cache and metadata cache both need clearing after adding or removing an extension class. A stale metadata cache is the #1 cause of "my annotation is ignored." Symptom: the column exists, the behaviour just doesn't fire.Doctrine\DBAL\Exception\UniqueConstraintViolationException on persist/flush and retry with a suffix, or add a DB unique index and accept the retry loop.Storage::disk('s3') is not pluggable without a custom adapter. For most Laravel apps, handle uploads yourself and let the entity store the path.ext_translations by default) and its listener needs locale + fallback configured. If translated fields come back empty in a non-default locale, check setTranslationFallback and whether the translation row was persisted at all.$em->getFilters()->isEnabled('soft-deleteable') after boot if behaviour looks inconsistent.laravel-doctrine/orm and gedmo/doctrine-extensions. On a framework bump, resolve them together: composer why-not laravel-doctrine/orm and composer show gedmo/doctrine-extensions will tell you quickly whether you're blocked by a constraint rather than a real incompatibility.illuminate/* to ^13.0 only, staying on Laravel 11/12 means pinning 2.0.2. Confirm the constraint before bumping in a monorepo or a package that others depend on.SlugRepository, custom LogEntry class, custom TranslatableListener config) is reachable — but usually via the service container binding the listener, not via this package's config. Read the wrapper's __construct before assuming there's a config key.How can I help you explore Laravel packages today?