Installation
composer require eightmarq/doctrine-behaviors
(Note: The package is a fork of knplabs/doctrine-behaviors; ensure compatibility with your Doctrine version.)
First Use Case: SoftDeletable
Add the trait to your entity and configure the deletedAt field:
use Knp\DoctrineBehaviors\Model\Blameable\BlameableTrait;
use Knp\DoctrineBehaviors\Model\Timestampable\TimestampableTrait;
use Knp\DoctrineBehaviors\Model\SoftDeletable\SoftDeletableTrait;
class Post
{
use SoftDeletableTrait;
/**
* @ORM\Column(type="datetime", nullable=true)
*/
private ?\DateTimeInterface $deletedAt = null;
}
Now, Post::delete() soft-deletes records instead of hard-deleting them.
First Query: Filtering Deleted Entities
Use SoftDeletableListener in your repository or query:
$queryBuilder->andWhere('entity.deletedAt IS NULL');
Blameable (Audit Tracking) Track who created/updated an entity:
use Knp\DoctrineBehaviors\Model\Blameable\BlameableInterface;
class Post implements BlameableInterface
{
use BlameableTrait;
/**
* @ORM\ManyToOne(targetEntity="App\Entity\User")
* @ORM\JoinColumn(nullable=false)
*/
private ?User $createdBy = null;
/**
* @ORM\ManyToOne(targetEntity="App\Entity\User")
* @ORM\JoinColumn(nullable=false)
*/
private ?User $updatedBy = null;
}
BlameableInterface::setUserValue() from the authenticated user.Sluggable (URL-Friendly Slugs) Auto-generate slugs from titles:
use Knp\DoctrineBehaviors\Model\Sluggable\SluggableInterface;
class Post implements SluggableInterface
{
use SluggableTrait;
/**
* @ORM\Column(type="string", length=255, unique=true)
*/
private string $slug;
private string $title;
}
$post->updateSlug() after setting $post->title.Tree (Hierarchical Data) Manage nested categories:
class Category
{
use TreeTrait;
// Required fields for TreeTrait
private ?Category $parent = null;
private ?Category $left = null;
private ?Category $right = null;
private ?Category $root = null;
private ?Category $level = null;
}
class CategoryRepository extends EntityRepository
{
use TreeRepositoryTrait;
}
$repository->getSubtree($parentCategory);
$repository->getChildren($category);
Translatable (Multilingual Content) Store translations in a separate table:
use Knp\DoctrineBehaviors\Model\Translatable\TranslatableInterface;
class Post implements TranslatableInterface
{
use TranslatableTrait;
/**
* @ORM\OneToMany(targetEntity="App\Entity\Translation", mappedBy="translatable", cascade={"all"}, orphanRemoval=true)
*/
private Collection $translations;
}
$post->translate('en')->setTitle('Hello');
$post->translate('es')->setTitle('Hola');
prePersist/preUpdate to trigger behaviors (e.g., slug generation).BlameableListener to auto-populate createdBy/updatedBy.deletedAt columns for SoftDeletable in a separate migration.SoftDeletable + Unique Constraints
deletedAt IS NULL in queries.(slug, deletedAt) if needed.Tree Behavior Quirks
MaterializedPathTrait for large hierarchies.parent/children to prevent infinite loops.EntityRepository (not a custom base class) for TreeRepositoryTrait.Translatable Serialization
translate().hasTranslation($locale) before accessing $post->translate($locale).Blameable Without Users
createdBy/updatedBy are nullable, set a default user (e.g., system) in prePersist:
$entity->setCreatedBy($entity->getUpdatedBy() ?? new SystemUser());
Slug Collisions
SluggableTrait appends a suffix (e.g., -2) on duplicates. For custom logic, override generateSlug():
public function generateSlug(): string
{
return strtolower(parent::generateSlug()) . '-' . $this->id;
}
SoftDeletable Not Working
deletedAt is nullable in the database schema.SoftDeletableListener in your Doctrine config (config/packages/doctrine.yaml):
doctrine:
orm:
event_subscribers:
- Knp\DoctrineBehaviors\ORM\SoftDeletable\SoftDeletableListener
Tree Traversal Issues
getSubtree() with fetchChildren: true to debug hierarchy:
$subtree = $repository->getSubtree($root, true);
$entityManager->getConnection()->getConfiguration()->setSQLLogger(new \Doctrine\DBAL\Logging\EchoSQLLogger());
Translatable Not Saving
translatable and translation entities have proper mappedBy/inversedBy:
// Translation.php
/**
* @ORM\ManyToOne(targetEntity="Post", inversedBy="translations")
* @ORM\JoinColumn(name="translatable_id", referencedColumnName="id")
*/
private ?Post $translatable = null;
Custom Behaviors
versionable logic to TimestampableTrait):
trait VersionableTrait
{
private ?int $version = 1;
public function incrementVersion(): void
{
$this->version++;
}
}
Override Default Logic
SluggableTrait:
use Knp\DoctrineBehaviors\Model\Sluggable\SluggableTrait;
class CustomSluggableTrait
{
use SluggableTrait {
generateSlug as private generateDefaultSlug;
}
public function generateSlug(): string
{
return 'custom-' . $this->generateDefaultSlug();
}
}
Event Subscribers
preUpdate):
$entityManager->getEventManager()->addEventSubscriber(new class {
public function postUpdate(LifecycleEventArgs $args): void
{
$entity = $args->getObject();
if ($entity instanceof LoggableInterface) {
$entity->logChange('updated');
}
}
});
PHPStan Extensions
// phpstan.neon
includes:
- vendor/knplabs/doctrine-behaviors/extension.neon
How can I help you explore Laravel packages today?