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.
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
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.src/ layout — confirm whether it exposes a Symfony bundle (DependencyInjection/) or is a plain traits+subscribers library requiring manual service registration.src/Model/* — the traits you'll actually use in your entities.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.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 keepKnp\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.
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;
}
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.
$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.
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.
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.
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.
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.
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.composer test on a scratch checkout against your platform versions catches incompatibilities faster than production will.doctrine.event_subscriber → subscriber actually loaded ($em->getEventManager()->getListeners('prePersist')). Nine out of ten failures are #1 or #2.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.my-post. Know whether the dedup check ignores soft-deleted rows (usually it does not by default).$conn->executeQuery() and raw SQL see everything. Similarly, DQL DELETE statements skip onFlush listeners — use $em->remove().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.getSlug or loadClassMetadata) and registering your subclass instead. That's the cleanest seam and it survives a fork upgrade better than editing vendor code.created_at/updated_at from existing data; not-null timestamp columns added to populated tables will otherwise break the deploy.How can I help you explore Laravel packages today?