Installation:
composer require knplabs/doctrine-behaviors
Add the bundle to config/bundles.php (Symfony) or register the extension in config/services.php (Laravel via DoctrineBundle).
First Use Case: Add a behavior trait to an entity. For example, to enable Blameable (track creator/updater):
// src/Entity/Post.php
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
use Knp\DoctrineBehaviors\Model\Blameable\BlameableTrait;
#[ORM\Entity]
class Post
{
use BlameableTrait;
#[ORM\Column(type: 'string', length: 255)]
private $title;
// ... other fields
}
Configure the blameable behavior in config/packages/doctrine_behaviors.yaml (Symfony) or config/doctrine_behaviors.php (Laravel):
knp_doctrine_behaviors:
orm:
blameable:
creator_field: createdBy
updater_field: updatedBy
Verify:
Run migrations (php bin/console doctrine:migrations:diff → php bin/console doctrine:migrations:migrate) and test CRUD operations. Check if createdBy/updatedBy fields auto-populate.
Entity-Level Behaviors:
Timestampable: Auto-manage createdAt/updatedAt:
use Knp\DoctrineBehaviors\Model\Timestampable\TimestampableTrait;
class Post { use TimestampableTrait; }
Configure in doctrine_behaviors.yaml:
knp_doctrine_behaviors:
orm:
timestampable: true
SoftDeletable: Add isDeleted flag and delete() method:
use Knp\DoctrineBehaviors\Model\SoftDeletable\SoftDeletableTrait;
class Post { use SoftDeletableTrait; }
Query soft-deleted records:
$repository->createQueryBuilder('p')
->where('p.isDeleted = :val')
->setParameter('val', false);
Repository-Level Behaviors:
// src/Repository/CategoryRepository.php
use Knp\DoctrineBehaviors\ORM\Tree\TreeTrait;
class CategoryRepository extends EntityRepository { use TreeTrait; }
Configure in doctrine_behaviors.yaml:
knp_doctrine_behaviors:
orm:
tree:
repository_class: App\Repository\CategoryRepository
Use methods like getChildren(), getParent(), or move().Translatable Entities:
Translation entity and link it to the main entity:
// src/Entity/PostTranslation.php
use Knp\DoctrineBehaviors\Model\Translatable\TranslationInterface;
class PostTranslation implements TranslationInterface { ... }
// src/Entity/Post.php
use Knp\DoctrineBehaviors\Model\Translatable\TranslatableTrait;
class Post { use TranslatableTrait; }
Configure locales and fields:
knp_doctrine_behaviors:
orm:
translatable:
locale_field: locale
translation_class: App\Entity\PostTranslation
translations: { title: Title, content: Content }
Access translations:
$post->translate()->setTitle('Français', 'Nouveau titre');
Sluggable:
use Knp\DoctrineBehaviors\Model\Slugable\SlugableTrait;
class Post { use SlugableTrait; }
Configure in doctrine_behaviors.yaml:
knp_doctrine_behaviors:
orm:
sluggable:
fields: ['title']
class Post {
use BlameableTrait;
public function setCreatedBy(?User $user): self { /* custom logic */ }
}
// src/EventListener/PostListener.php
use Doctrine\ORM\Event\PreUpdateEventArgs;
class PostListener {
public function preUpdate(Post $post, PreUpdateEventArgs $args) {
if ($post->isDirty('title')) {
$post->setSlug($post->getTitle());
}
}
}
SoftDeletable for RESTful soft deletes).Migration Order:
createdAt or isDeleted must exist before the trait is used.Circular References:
Translation entity is properly mapped with a OneToMany/ManyToOne relationship to avoid infinite loops in serialization (e.g., JSON API).Repository Conflicts:
TreeTrait is loaded after parent traits to avoid method conflicts. Use parent::method() explicitly if needed.Locale Handling:
translatable config or via setTranslatableLocale(). Missing locales cause NULL translations.Slug Collisions:
post-2). Customize via sluggable config:
knp_doctrine_behaviors:
orm:
sluggable:
unique_suffix_max_length: 4
UUID Generation:
uuid field is defined as string(36) or binary(16) in the entity mapping. Auto-generation requires ramsey/uuid:
composer require ramsey/uuid
doctrine_behaviors.yaml config matches entity field names (e.g., createdBy vs created_by).TreeException if nodes are orphaned or paths are invalid. Use getPath() to debug:
$category->getPath(); // Should return "/1/4/7/" for materialized path
# config/packages/dev/doctrine.yaml
doctrine:
dbal:
logging: true
logging_format: '%%SQL%%'
Custom Behaviors:
DoctrineBehaviors\ORM\BehaviorInterface.Event Subscribers:
onFlush for Timestampable):
use Knp\DoctrineBehaviors\ORM\Event\LifecycleEventArgs;
class CustomSubscriber implements EventSubscriber {
public function prePersist(LifecycleEventArgs $args) {
$entity = $args->getEntity();
if ($entity instanceof TimestampableInterface) {
$entity->setCreatedAt(new \DateTime());
}
}
}
QueryBuilder Extensions:
// src/Repository/TreeRepository.php
trait TreeRepository {
public function getSiblings(EntityInterface $entity) {
return $this->createQueryBuilder('e')
->where('e.parent = :parent')
->andWhere('e.id != :id')
->setParameter('parent', $entity->getParent())
->setParameter('id', $entity->getId())
->getQuery()
->getResult();
}
}
PHPStan:
composer require --dev knplabs/doctrine-behaviors-phpstan
TranslatableInterface::getTranslations() now returns TranslatableTranslationCollection instead of array.config/services.php:
$container->loadFromExtension('doctrine_behaviors', [
'orm' => [
'timestampable' => true,
'bl
How can I help you explore Laravel packages today?