teamq-ec/teamq-doctrine-behaviors
TeamQ Doctrine Behaviors adds reusable Doctrine ORM behavior extensions to PHP projects, bundling common entity traits like timestamps, slugs, soft deletes, and blameable fields into one lightweight package. Drop-in traits and listeners keep models consistent and cut boilerplate.
teamq-ec/teamq-doctrine-behaviors v2.0.0 is a major-version bump whose only release note is "Upgrade version" (PR #3). There is no changelog entry describing breaking changes, so treat the v1 → v2 jump as potentially breaking until proven otherwise and budget an hour to verify before adopting it in a production branch.
Start here, in order:
composer.json of the package — check the require block for the Doctrine ORM / laravel-doctrine/orm constraint and the PHP version floor. A "version upgrade" release in a small package almost always means a raised dependency floor (Doctrine ORM 2.x → 3.x, PHP 8.1 → 8.2, or Laravel 10 → 11). That constraint is the real content of this release.config/doctrine-behaviors.php (or whatever it is named) is where global listeners/subscribers for behaviors are registered. Diff this file against your published copy; config keys are the most common silent breakage in a major bump.src/ for the behavior classes you actually use — typically Timestampable, Blameable, Sluggable, SoftDeleteable, Tree, Loggable, Translatable. Confirm the attribute/annotation names you use in your entities still exist.vendor/bin/phpunit inside the package. If the package ships tests for each behavior, they are your fastest spec.First use case: add #[Timestampable] (or the mapped equivalent) to a single throwaway entity, persist it, and assert createdAt/updatedAt are populated. That one smoke test exercises the event-subscriber wiring, which is the part most likely to break between majors.
composer require teamq-ec/teamq-doctrine-behaviors:^2.0 --dry-run # inspect resolution first
composer why-not teamq-ec/teamq-doctrine-behaviors 2.0.0 # see what blocks the upgrade
Behavior-per-entity, declared close to the mapping. Doctrine behaviors work by attaching an event subscriber that mutates the entity on prePersist/preUpdate/onFlush. Keep the declaration on the entity itself rather than in a central registry, so a reader of the entity can see why a field is being written:
#[ORM\Entity]
#[Timestampable] // fills createdAt / updatedAt
class Product { /* ... */ }
If the package exposes a fluent/attribute-config form with options (field names, on: ["create", "update"]), prefer explicit options over defaults — defaults are exactly what a major version is most likely to change.
Blameable + an auth resolver. The one integration point that genuinely needs your code is the user resolver. Most Doctrine behavior packages ship a BlameableListener you must hand a user instance or a callable. Register it in a service provider so it follows the current request:
$this->app->resolving(BlameableListener::class, function ($listener) {
$listener->setUserCallable(fn () => auth()->user()?->getAuthIdentifier());
});
Do this lazily. Calling auth()->user() at boot will break queue workers, console commands, and any request without a session.
Soft deletes as a query-scope concern, not a controller concern. If you use SoftDeleteable, wrap read access in repository methods that make the filter explicit (->filters()->disable('soft-deleteable') when you genuinely need deleted rows, e.g. an admin restore screen or an audit export). Don't scatter filter toggles across controllers — they leak into the next query on the same EntityManager if you forget to re-enable.
Slugs and uniqueness. For Sluggable, always configure the unique constraint and a collision suffix strategy. Generate the slug in the same flush as the insert; a two-step "save, then slug, then save" pattern produces a window where a row has a null slug and blows up on NOT NULL or on a redirect lookup.
Ordering with Timestampable in tests. When seeding fixtures, inject a clock or freeze time (Carbon::setTestNow()) if the package reads the system clock. Otherwise tests that assert on updatedAt > createdAt flake at sub-second resolution.
git diff v1.0.0..v2.0.0 -- src/ config/ composer.json. Read the composer.json and config diffs first; they carry the breaking change.@Timestampable → #[Timestampable], or a field option becoming property). A rename fails silently in some Doctrine setups — the entity simply stops having timestamps and you find out in production when a NOT NULL column is empty.onFlush. Sluggable needs the entity's identifying fields populated; Timestampable sets them in prePersist. If you upgrade both the ORM and this package, verify the order — a changed priority shows up as blank slugs, not as an exception.EntityManager is stateful and long-lived. In Octane or queue workers, a filter disabled for one job (soft-delete, tenant filter) stays disabled for the next. Wrap risky code in try/finally and re-enable the filter.createdBy is either null (violating NOT NULL) or falls back to whatever stale value the resolver last returned. Set the resolver to return null explicitly in console context, and consider a dedicated "system" user ID for seeding.php artisan config:cache can freeze a resolver closure into a serialized config that cannot be unserialized at runtime. Register closures in a service provider, not in config/.^2.0 on a package that shipped a major with no documented changes means the next silent major-shaped change could arrive on composer update. Consider an exact constraint plus a scheduled review, or at least commit the composer.lock.gedmo/doctrine-extensions and knplabs/doctrine-behaviors; the listeners map closely enough that a rollback path exists. Keep the v1 constraint noted somewhere so you can revert without archaeology.How can I help you explore Laravel packages today?