Skip to content

phil/doctrine-behaviours

alpha

Twelve Doctrine behaviours, from timestamps and trees to positions, slugs, translations and versions, each one opt-in.

phil/doctrine-behaviours gives Doctrine ORM entities twelve behaviours. Timestamps stamp when an entity is created and updated, in UTC. Blame records who created and last changed it, as a user identifier. Soft delete hides a removed entity behind a filter instead of deleting it, and removing it a second time deletes it. Trees keep each node's materialized path and depth, and the Tree reader gives roots, children, descendants, ancestors and the hierarchy. Deactivation switches a record off and back on behind a filter of its own. An owner is a stable identifier, never an e-mail address. Publication is a state of symfony/workflow with its dates. A position orders records within a group, and a record moves before or after another. A slug made by phil/slug is kept unique in its table, or within a scope such as a tenant, a locale or a parent. Translations keep the text of a record in a row per locale, read along a declared fallback chain and never copied from one locale to another. Versions freeze what a record held, fork the next edit, and go back under a right of their own. A dated identifier is a UUID v7.

It is for any application on Doctrine ORM that needs these columns kept without writing listeners of its own. Each behaviour stands alone, so an application registers only the listeners its entities use. Trees are materialized paths: moving a branch is one update instead of renumbering the tree. In a Symfony application, phil/doctrine-behaviours-bundle registers the listeners, the filter and the blame of the authenticated user. A record that should leave its table, ready to be brought back, belongs in phil/trash-bundle instead.

It is a plain PHP library on Doctrine ORM, DBAL, psr/clock and phil/locale-dimensions; phil/slug, symfony/uid and symfony/workflow are needed only by the behaviours that name them. Each behaviour is an interface, a trait that maps its columns, and a listener that keeps them. Who is acting comes from a BlameResolver the application provides, such as FixedBlame for a command that acts for a known account, or a test.

Status

PartState
Contract\Timestampable, TimestampableEntity, TimestampListenerImplemented and tested
Contract\Blameable, BlameableEntity, BlameListener, Contract\BlameResolver, FixedBlameImplemented and tested
BlameEraser: a user's name found and replaced in every blamed tableImplemented and tested
Contract\SoftDeletable, SoftDeletableEntity, SoftDeleteListener, SoftDeletedFilterImplemented and tested
Contract\TreeNode, TreeNodeEntity, TreeListener, Tree, BranchImplemented and tested
Deactivatable, DeactivatableEntity, DeactivationListener, DeactivatedFilterImplemented and tested
OwnerId, Ownable, OwnableEntity, OwnerListener, OwnerResolver, FixedOwner, OwnerEraserImplemented and tested
Publishable, PublishableEntity, PublicationWorkflow, PublicationListenerImplemented and tested
Contract\Positionable, PositionableEntity, PositionListener, Positions, groupsImplemented and tested
Contract\Sluggable, SluggableEntity, ScopedSluggableEntity, SlugListener, scopesImplemented and tested
Contract\Translatable, Contract\Translation, TranslationEntity, TranslationListener, TranslationsImplemented and tested
Versionable, VersionableEntity, VersionListener, Versions, RecordVersion, VersionRestoreGuardImplemented and tested
DatedIdentifiable, DatedIdentifierEntity, DatedIdentifierListenerImplemented and tested

Documentation

DocumentWhat it covers
Timestamps and blameWho and when, what an entity was given and keeps, privacy and erasure.
Soft deleteRemoval as a date, the filter, restoring, deleting for good.
TreesPaths, moves of a branch, reading a tree.
DeactivationSwitching a record off and on, the filter, how it differs from soft delete.
OwnerA stable identifier, the resolver, handing over, erasure.
PublicationThe state machine, its dates, publishing at a set time.
PositionOrdering within a group, moving before and after, the index.
SlugWhen a slug is made, how it stays unique, in the table or within a scope.
TranslationA row per locale, read along a fallback chain, a list in one query, writing without copying, slugs and versions per locale.
VersionsFreezing, forking, going back under a right of its own.
Dated identifierA UUID v7 as the primary key.
IntegrationRegistering the listeners and the filter without Symfony.
Extension pointsThe ports, arguments and data an application chooses or replaces.
TestingHow the library is tested.

Quick start

#[ORM\Entity]
class Category implements Timestampable, Blameable, SoftDeletable, TreeNode
{
    use TimestampableEntity;
    use BlameableEntity;
    use SoftDeletableEntity;
    use TreeNodeEntity;

    #[ORM\ManyToOne(targetEntity: self::class)]
    private ?self $parent = null;

    public function treeId(): string { return (string) $this->id; }
    public function treeParent(): ?self { return $this->parent; }
}
$tree->hierarchy(Category::class);    // list<Branch>, each node with its children
$tree->ancestors($category);          // the root down to the parent

In a Symfony application, phil/doctrine-behaviours-bundle registers the listeners and the filter.

Requirements

PHP 8.5 or later, Doctrine ORM 3.7 or later, DBAL 4.5, doctrine/collections, psr/clock and phil/locale-dimensions; phil/slug for slugs, symfony/uid for dated identifiers, and symfony/workflow for publication.

Contributing

Contributions are welcome on the PHIL monorepo: read the contribution guide and the code of conduct. Report a vulnerability privately, as the security policy explains, never in a public issue.

License

MIT, © Philippe Gamache, doing business as Le Gnome Archiviste; a Parler Haut Interagir Librement project. See LICENSE.