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

nick4fake/doctrine-behaviors

Bring Doctrine behavior extensions to Laravel with nick4fake/doctrine-behaviors. Add timestampable, blameable, soft-delete, sluggable, and tree support through reusable traits, keeping entity code clean and consistent. Easy to install and configure for Doctrine projects.

View on GitHub
Deep Wiki
Context7

Getting Started

Before you write a line of code, diff this fork against upstream. nick4fake/doctrine-behaviors is a republished fork of the well-known knplabs/doctrine-behaviors trait suite (Timestampable, SoftDeletable, Sluggable, Translatable, Tree, Blameable, Loggable, Uuidable). With 2 stars and 0 dependents, there is no community surface to lean on — the authoritative usage docs live in the upstream KnpLabs README, and this repo's value is whatever delta the author applied (usually PHP 8 / newer Doctrine ORM compat). Do that comparison first:

composer show -a nick4fake/doctrine-behaviors   # versions, requires
# then clone both and diff src/
diff -r vendor/knplabs/doctrine-behaviors/src vendor/nick4fake/doctrine-behaviors/src

Where to look first

  1. composer.json — the require block tells you which Doctrine ORM, PHP, and Symfony versions this fork actually targets. If it caps ORM at ^2.x and you're on ORM 3, stop.
  2. src/ layout — confirm whether it exposes a Symfony bundle (DependencyInjection/) or is a plain traits+subscribers library requiring manual service registration.
  3. src/Model/* — the traits you'll actually use in your entities.
  4. src/Contract/Entity/* (or src/Model/*/*Interface.php) — the interfaces the listeners type-check against. This is the single most important directory: in modern Knp-style versions, using the trait without implementing the interface silently disables the behavior.
  5. tests/ — the fastest source of working examples; treat it as your documentation.

Minimal install

composer require nick4fake/doctrine-behaviors

First use case — Timestampable on one entity. Pick a low-risk entity, add the trait + interface, register the subscriber, and verify on INSERT/UPDATE:

namespace App\Entity;

use Knp\DoctrineBehaviors\Contract\Entity\TimestampableInterface;
use Knp\DoctrineBehaviors\Model\Timestampable\TimestampableTrait;

class Article implements TimestampableInterface
{
    use TimestampableTrait;

    // no createdAt/updatedAt properties needed — the trait declares them
}

Verify the root namespace in src/ before copying — forks frequently keep Knp\DoctrineBehaviors\... to stay import-compatible, but some rename it to the fork owner's namespace.

Register the subscriber (exact class name from src/ORM/):

# config/services.yaml
services:
    Knp\DoctrineBehaviors\ORM\Timestampable\TimestampableSubscriber:
        tags: [doctrine.event_subscriber]

Then $em->persist($article); $em->flush(); and assert createdAt/updatedAt were set. Once that round-trips, roll out the rest of the traits.

Implementation Patterns

Always pair trait + interface

This is the core contract of the whole package. The listeners/subscribers check instanceof <Behavior>Interface, not method_exists. A missing interface means no error and no behavior — the classic silent failure.

class Post implements SluggableInterface, SoftDeletableInterface, TimestampableInterface
{
    use SluggableTrait, SoftDeletableTrait, TimestampableTrait;
}

Timestampable — the default for every entity

Apply it broadly; it costs two columns and gives you free audit ordering. For "who," pair with Blameable:

class Post implements BlameableInterface { use BlameableTrait; }

Blameable needs the user supplied at runtime — in Symfony, a kernel/request listener that calls the subscriber's setUser() (or the bundle's built-in user-callable config) before flush. In queue workers and CLI commands, set it explicitly or you'll get nulls; don't assume the request stack is populated.

SoftDeletable — read the query-filter behavior before adopting

$em->remove($post);   // only sets deletedAt
$em->flush();
// all normal queries now exclude it
$post->restore();     // or the repo-specific restore method

Day-to-day pattern: use soft delete for user-visible content where "undo" matters, and reserve hard deletes for GDPR erasure via native SQL (which bypasses the listener entirely). Wrap restore in an admin action rather than exposing it in the main UI.

Sluggable — generate once, regenerate deliberately

Default behavior derives the slug from a configured property and deduplicates with a suffix. Keep slug generation off on update for permalink stability:

class Post implements SluggableInterface
{
    use SluggableTrait;

    public function getSluggableFields(): array
    {
        return ['title'];
    }
}

If the fork supports it, disable regeneration on update; otherwise guard in your service layer before flush(). For i18n sites, combine with Translatable so each locale gets its own slug.

Translatable — the biggest conceptual lift

Knp-style Translatable is not Gedmo's translatable: it uses a companion PostTranslation entity with a locale column and a translate($locale) accessor.

$post->translate('en')->setTitle('Hello');
$post->translate('fr')->setTitle('Bonjour');
$post->mergeNewTranslations();

Practical workflow: one translation entity per translatable class, forms built as a collection keyed by locale, and repository queries that join the translation table with the current locale filter. Budget real time for the query side — this is where most of the debugging lives.

Tree — for category/menu structures only

Nested-set writes are expensive on large trees (a node move rewrites the subtree). Use it for taxonomy and menus with dozens-to-hundreds of nodes, not thousands. After moving a node, flush and re-fetch children rather than trusting in-memory collections.

Loggable / Uuidable — set-and-forget

Uuidable is a good first migration target if you want UUID PKs without hand-rolling prePersist. Loggable adds an audit table; make sure the LoggableListener is registered for the entities you care about and that its table is migrated before traffic.

Gotchas and Tips

  • The fork itself is the biggest risk. No dependents means no downstream bug reports, no security review, no guarantee of updates. Options, in order of preference: (a) use upstream knplabs/doctrine-behaviors if it covers your versions; (b) adopt this fork but vendor it into src/ and treat it as your own code; (c) pin the exact commit in composer.json ("nick4fake/doctrine-behaviors": "dev-master#<sha>") rather than a floating constraint.
  • Run the repo's own test suite against your Doctrine/PHP matrix before committing. composer test on a scratch checkout against your platform versions catches incompatibilities faster than production will.
  • Silent no-ops. If a behavior "doesn't work," check in this order: interface implemented → subscriber tagged doctrine.event_subscriber → subscriber actually loaded ($em->getEventManager()->getListeners('prePersist')). Nine out of ten failures are #1 or #2.
  • Soft-deleted rows still violate unique constraints. A deleted user with email = [email protected] blocks re-registration. Either null out the unique field on delete or drop the DB-level unique index in favor of an application check.
  • Sluggable + SoftDeletable collide the same way — two rows wanting my-post. Know whether the dedup check ignores soft-deleted rows (usually it does not by default).
  • Query filters and native SQL. Soft-delete filtering lives in a Doctrine filter; $conn->executeQuery() and raw SQL see everything. Similarly, DQL DELETE statements skip onFlush listeners — use $em->remove().
  • Timestamps use new \DateTime() with PHP's default timezone while the DB column may be DATETIME in UTC. Standardize one side (set date_default_timezone_set('UTC') app-wide, use datetime_immutable if the fork's trait supports it) or you'll get off-by-hours bugs that only appear in one environment.
  • mergeNewTranslations() / translate() are not automatic. Add a small TranslatableSubscriber or an entity lifecycle hook if translations are populated from forms, or you'll end up with orphan translations and no error.
  • Extension points are thin. Because there's no bundle config beyond basic registration, customize by subclassing the subscriber (e.g., overriding getSlug or loadClassMetadata) and registering your subclass instead. That's the cleanest seam and it survives a fork upgrade better than editing vendor code.
  • Blameable in CLI/workers needs an explicit user; without it, columns silently stay null. Set a system user in your console command bootstrap.
  • Migrations before features. Add all trait columns in a single migration pass per entity and backfill created_at/updated_at from existing data; not-null timestamp columns added to populated tables will otherwise break the deploy.
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