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

toovalu-github/doctrine-behaviors

Doctrine Behaviors adds reusable entity traits and listeners to Doctrine ORM, including timestampable, sluggable, soft-deletable, blameable, and tree behavior, so common model logic stays out of your entities and your Laravel or PHP app stays clean.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and register the bundle first, then probe the vendor tree before writing a line of domain code — this is a fork, and namespaces/package names may differ from upstream knplabs/doctrine-behaviors.

composer require toovalu-github/doctrine-behaviors
grep -r "class DoctrineBehaviorsBundle" vendor/toovalu-github/doctrine-behaviors/src
ls vendor/toovalu-github/doctrine-behaviors/src/Model

Register the bundle in config/bundles.php (namespace per the grep above — most forks keep Knp\DoctrineBehaviors\Bundle\DoctrineBehaviorsBundle):

Knp\DoctrineBehaviors\Bundle\DoctrineBehaviorsBundle::class => ['all' => true],

There is no mandatory configuration: a behavior activates when an entity both implements the contract interface and uses the matching trait. That pair is the whole mental model.

First use case — Timestampable + Sluggable on a Post:

use Doctrine\ORM\Mapping as ORM;
use Knp\DoctrineBehaviors\Contract\Entity\SluggableInterface;
use Knp\DoctrineBehaviors\Contract\Entity\TimestampableInterface;
use Knp\DoctrineBehaviors\Model\Sluggable\SluggableTrait;
use Knp\DoctrineBehaviors\Model\Timestampable\TimestampableTrait;

#[ORM\Entity]
class Post implements SluggableInterface, TimestampableInterface
{
    use SluggableTrait;
    use TimestampableTrait;

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

    public function shouldRegenerateSlugOnUpdate(): bool
    {
        return false; // keep permalinks stable
    }
}

Then bin/console doctrine:migrations:diff and read the generated migration — you should see created_at, updated_at, and slug columns. If any are missing, the interface is missing.

Implementation Patterns

Pick trait vs. mapped superclass deliberately. TimestampableEntity and SoftDeletableEntity are ready-made mapped superclasses that already embed the trait and the column mapping. Don't use TimestampableTrait and extend TimestampableEntity; you'll get duplicate createdAt mappings.

Blameable adds createdBy/updatedBy relations to your user entity. Let the bundle resolve the current user from the security token for web requests, and inject an explicit user in CLI, fixtures, and messenger consumers — otherwise the listener no-ops and your nullable columns stay null (or non-nullable columns blow up).

SoftDeletable is a filter, not an override of remove(). You call $post->softDelete() / $post->restore(), and reads are filtered only where the filter is enabled:

$em->getFilters()->enable('soft_deletable'); // confirm exact name via getFilters()->getFilters()

Wrap this in a request listener or custom repository base class so it is consistent; enable it for admin "trash" screens only when you actually want deleted rows.

Translatable is a two-entity pattern: a parent with TranslatableTrait and a translation entity with TranslationTrait.

#[ORM\Entity]
class PostTranslation implements TranslationInterface
{
    use TranslationTrait;

    #[ORM\Column(nullable: true)]
    private ?string $title = null;

    // getters/setters
}

Then $post->translate('en')->setTitle('Hello') returns the existing or newly created translation, and $post->getTranslation('fr') reads it. Persistence flows through cascade: ['persist'] on the parent's translations association — verify the trait sets that, and add orphanRemoval yourself if you edit translations through forms.

Uuidable works best as a secondary public identifier next to an auto-increment primary key. Using it as the primary key forces you to solve UUID generation before flush in every test factory.

Sluggable is where you should spend your design budget: put a unique index on slug, and decide up front whether shouldRegenerateSlugOnUpdate() is true. For CMS content it should be false.

Tree is the least-exercised behavior in these packages. Before building navigation on it, write a throwaway integration test that adds children, moves a node, and reads it back through your Doctrine version.

Form integration for Translatable: a CollectionType of translation forms with by_reference => false, plus cascade: ['persist']. Render one tab per locale from a fixed locale list, not from existing translations, so editors can create the first translation.

Gotchas and Tips

  • Fork risk is the headline gotcha. Zero stars, zero dependents, and a package name that isn't the upstream one. Pin an exact version in composer.json and diff vendor/toovalu-github/doctrine-behaviors against knplabs/doctrine-behaviors before adopting. If the delta is one or two patches, prefer a cweagans/composer-patches entry or a VCS repository pointed at a commit — you keep upstream's fixes and your own diff visible.
  • Missing interface = silent no-op. Implement SluggableInterface but forget SluggableTrait and you get no slug column, no error. Symptom: "the listener never fires." Check the interface first, always.
  • Migrations need two passes. The first diff after adding a behavior sometimes misses association changes (blameable user FKs, translation tables). Run diff, review, then diff again to confirm it's empty.
  • Soft-delete + unique slug collide. A deleted post still occupies my-title. Either make the unique index composite (slug, deleted_at) or query for uniqueness including soft-deleted rows in your slug factory.
  • updatedAt doesn't move on bulk DQL. UPDATE Post p SET ... bypasses listeners entirely. If audit accuracy matters, use entity-level updates or set the timestamp in the same DQL.
  • Translatable N+1. Fetching 50 posts and rendering getTranslation($locale) per row is 50 queries. Join translations in the repository query with a locale condition, or use a query hint to eager-load.
  • Blameable in workers. Symfony Messenger consumers have no token. Either make createdBy nullable, or configure a callable/system user so audit rows are attributable.
  • Fixtures. Disable the blameable listener or set a user before doctrine:fixtures:load; otherwise a half-populated audit column will look like a data bug three weeks later.
  • Extension points worth knowing: override getSluggableFields() to compute the slug source dynamically (e.g. include a category prefix), decorate the slug generator service for custom transliteration, and override setUpdatedAt()/setCreatedAt() in your entity if you need to backfill legacy data.
  • Loggable needs a retention decision on day one. It writes audit rows into a companion table that grows unbounded; decide the pruning job before production, not after.
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.
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
robertmarney/lara-hierarchial-collections
simonschaufi/rule-doc-generator
emissaoja/emissaoja
blackbricksoftware/gravityforms-civicrm-form-processor
al-rahabi/fancy-crud
jinas/dhivatar-component
mltstephane/laravel-quality
bernskiold/laravel-talentlms
bernskioldmedia/laravel-form-architect