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

fsi/doctrine-extensions

Doctrine extensions for Laravel that add common ORM behaviors such as soft deletes, timestamps, sluggable, tree, and sortable mappings, helping you reuse familiar persistence patterns with Doctrine entities.

View on GitHub
Deep Wiki
Context7

Getting Started

1. Confirm the fit before installing. fsi/doctrine-extensions is a Doctrine ORM library, not an Eloquent one. If your Laravel app is Eloquent-first, stop here. It only pays off in an app where you have (or are introducing) a Doctrine EntityManager — typically via laravel-doctrine/orm, which gives you a ManagerRegistry binding and doctrine.* config.

composer require fsi/doctrine-extensions
# you almost certainly also need:
composer require laravel-doctrine/orm doctrine/orm:^2.14

2. Where to look first. Read the package src/ tree, not the README — it's the least maintained artefact. The two things worth your time:

  • DoctrineExtensions/Uploadable/ — the interesting half: File, FileInterface, FileWriterInterface, FileReaderInterface, UploadableListener, and the annotation classes used in entity mapping.
  • DoctrineExtensions/Timestampable/ — the thin half: CreatedAt/UpdatedAt annotations plus a listener that just sets new \DateTime() on prePersist/preUpdate.

There is no Laravel bundle here. The sibling fsi/doctrine-extensions-bundle (Symfony) is still the best documentation for how the listeners must be wired — copy its listener registration and config shape into a service provider.

3. Register the listeners. In a dedicated provider, resolve the EM and attach listeners in boot():

public function boot(ManagerRegistry $registry)
{
    $em = $registry->getManager();
    $evm = $em->getEventManager();

    $evm->addEventSubscriber(new TimestampableListener());
    $evm->addEventSubscriber(new UploadableListener(
        storage_path('app/doctrine-uploads'),   // directory the writer uses
        false                                   // overwrite?
    ));
}

4. First use case — pick the timestamp one. Annotate createdAt/updatedAt on an entity, flush(), and confirm the values populate. That validates your event wiring with zero filesystem risk. Once timestamps work, move to Uploadable, which is where the real value (and real complexity) lives.


Implementation Patterns

Timestamps belong to the model, not the controller. The whole point is deleting $entity->setUpdatedAt(new \DateTime()) from every action and repository method. Let the listener own it:

/** @ORM\Entity */
class Invoice
{
    /** @ORM\Column(type="datetime") @Timestampable\CreatedAt */
    private \DateTime $createdAt;

    /** @ORM\Column(type="datetime", nullable=true) @Timestampable\UpdatedAt */
    private ?\DateTime $updatedAt = null;
}

If you use PHP 8 attributes rather than annotations, check whether the bundled annotation classes are #[Attribute]-decorated — with a 2021 release they likely are not, so you'll stay on annotations or write a tiny mapping shim.

Uploadable — the day-to-day workflow. The pattern is: the entity stores a File value object, the listener writes bytes to disk in postPersist/postUpdate and removes them in postRemove. Typical flow in an upload action:

$file = new \FSi\Component\Filesystem\File\File($request->file('avatar')->getRealPath());
$file->setName($request->file('avatar')->getClientOriginalName());
$user->setAvatar($file);          // plain setter on a @Uploadable property
$em->persist($user);
$em->flush();                     // listener writes the file + stores the name

Prefer injecting a FileWriterInterface implementation backed by Laravel's Storage facade over the concrete writer, so tests can use a fake and production can use a disk — this is the package's main extension point.

Wrap listeners in your own subscriber. Don't scatter addEventSubscriber calls. One DoctrineExtensionsSubscriber (or a single provider method) that registers all of them keeps ordering explicit and makes it obvious in code review that flush behaviour is being modified globally.

Soft-delete pattern. If you use the soft-delete listener, pair it with a Doctrine SQLFilter and enable it on the EM:

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

With Laravel's laravel-doctrine the manager is often scoped, so enabling the filter in a middleware or a request-scoped provider is safer than doing it once at boot — otherwise long-running queues and Octane workers keep stale state.

Testing pattern. Listeners only fire through the Unit of Work. Use flush() + $em->clear() and assert against a fresh fetch; a mock EntityManager proves nothing. For uploads, point the writer at a temp dir and delete it in tearDown.

Migrations. This package assumes doctrine/migrations. If the app also uses Eloquent migrations, decide per-bounded-context: Doctrine entities get Doctrine migrations, Eloquent models get Laravel's. Mixing them in one schema is the fastest route to drift.


Gotchas and Tips

  • Abandonment risk is the headline. Last release August 2021, 3 stars, 0 dependents. Treat it as vendored code: pin the exact version, and be ready to fork the two directories you actually use. Budget for this in the estimate.
  • Doctrine ORM 3 will break it. ClassMetadata, EventManager, and several listener signatures changed. Pin doctrine/orm:^2.14 and verify PHP 8.2+ compatibility before committing — the annotation reader chain (doctrine/annotations) is also deprecated in favour of attributes.
  • updatedAt doesn't fire when nothing changed. Doctrine only computes a change set for modified fields; preUpdate won't run on a no-op flush. Don't write tests that assume otherwise, and don't use it as a proxy for "this request touched the row".
  • Timestamps are naive. The column type is datetime, so timezone is whatever date_default_timezone_set says at flush time. Standardise on UTC in AppServiceProvider and store datetime_immutable where you can — the listener's \DateTime default fights you slightly, so override or normalise.
  • Uploads are written in postPersist, which is inside the flush boundary. If your surrounding transaction rolls back, you get orphan files on disk with no DB row pointing at them. Either move uploads outside the transaction or run a reconciliation job keyed on a UUID you generate before persisting.
  • Clearing a File property does not delete the file. Removal only happens on postRemove. Explicitly delete through the listener/FileWriter when a user clears an avatar.
  • Metadata is cached. After changing entity mappings, run php artisan doctrine:clear-metadata-cache (or cache:clear) — otherwise loadClassMetadata never re-runs and the File column mapping silently stays stale.
  • Debug registration with $em->getEventManager()->getListeners('preUpdate') — a listener that "isn't working" is usually a listener that was attached to a different EntityManager instance than the one your controller resolved.
  • Listener ordering matters if you also attach your own onFlush subscriber; timestamps should be applied before your changelog logic reads the change set.
  • Native SQL and DQL bypass soft-delete filters. Raw queries will happily return "deleted" rows. Audit any $em->getConnection()->executeQuery() in the codebase.
  • Extension points worth knowing: FileWriterInterface / FileReaderInterface (swap the storage backend), MimeTypeGuesser (uploads validation), and subclassing UploadableListener to override property resolution if your naming conventions differ from the defaults.
  • Don't reach for this when a Laravel-native solution is one line. created_at/updated_at on Eloquent, Storage::put for files, and a deleted_at global scope cover 90% of what this package does — with community support. Choose it because you're already on Doctrine, not to avoid writing five lines.
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.
3mdikram/worldmoneywords
snowsoft/config
itmosoft/laravel-subscriptions
larva/laravel-ai-qwen
diephp/sequences
users-au/socialite-provider
twidpay/twid-logger
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