danilovl/entity-traits-bundle
Installation
composer require danilovl/entity-traits-bundle
Register the bundle in config/bundles.php (if not using Symfony Flex):
Danilovl\EntityTraitsBundle\EntityTraitsBundle::class => ['all' => true],
Configure
Create config/packages/danilovl_entity_traits.yaml with basic settings:
danilovl_entity_traits:
identity:
default_strategy: uuid
timestampable:
enabled: true
timezone: 'UTC'
soft_delete:
enabled: true
First Use Case
Add traits to an entity (e.g., Article):
use Danilovl\EntityTraitsBundle\Trait\Required\Identity\UuidTrait;
use Danilovl\EntityTraitsBundle\Trait\Optional\Timestampable\TimestampableTrait;
#[ORM\Entity]
class Article
{
use UuidTrait;
use TimestampableTrait;
}
Run php bin/console doctrine:schema:update --force to generate the database schema.
Entity Boilerplate Reduction
Replace repetitive fields (e.g., createdAt, updatedAt, slug) with traits:
use Danilovl\EntityTraitsBundle\Trait\Optional\Timestampable\TimestampableTrait;
use Danilovl\EntityTraitsBundle\Trait\Optional\Seo\SlugTrait;
class BlogPost
{
use TimestampableTrait;
use SlugTrait;
}
Audit Trails Track creation/modification by users:
use Danilovl\EntityTraitsBundle\Trait\Optional\Audit\BlameableTrait;
class Product
{
use BlameableTrait;
}
Configure blameable.user_class to point to your User entity.
Soft Deletes Enable soft deletes globally or per-entity:
danilovl_entity_traits:
soft_delete:
enabled: true
Use SoftDeletableTrait in entities:
use Danilovl\EntityTraitsBundle\Trait\Optional\Timestampable\SoftDeletableTrait;
class Comment
{
use SoftDeletableTrait;
}
Validation Add validators directly to properties:
use Danilovl\EntityTraitsBundle\Validator\Email;
class User
{
#[Email]
protected ?string $email = null;
}
Hierarchical Data
Use TreePathTrait + #[Tree] attribute for nested structures:
use Danilovl\EntityTraitsBundle\Trait\Optional\Sorting\TreePathTrait;
use Danilovl\EntityTraitsBundle\Attribute\Tree;
#[Tree]
class Category
{
use TreePathTrait;
}
Presets
Leverage pre-built trait combinations (e.g., BlogPostTrait):
use Danilovl\EntityTraitsBundle\Trait\Preset\BlogPostTrait;
class Article
{
use BlogPostTrait;
}
Doctrine Filters
Enable global filters (e.g., SoftDeleteFilter) via config or manually:
doctrine:
orm:
filters:
soft_delete:
class: Danilovl\EntityTraitsBundle\Doctrine\Filter\SoftDeleteFilter
enabled: true
Event Listeners
Extend default listeners (e.g., SluggableListener) by overriding methods in a custom listener:
class CustomSluggableListener extends SluggableListener
{
protected function generateSlug(string $value): string
{
return strtolower(parent::generateSlug($value));
}
}
Register it as a service with danilovl_entity_traits.sluggable_listener tag.
Custom Traits Extend existing traits for domain-specific logic:
trait CustomTimestampableTrait extends TimestampableTrait
{
public function isRecent(int $hours = 24): bool
{
return $this->getCreatedAt()->getTimestamp() > strtotime("-{$hours} hours");
}
}
Configuration Overrides
Override bundle config per-environment (e.g., config/packages/dev/danilovl_entity_traits.yaml):
danilovl_entity_traits:
timestampable:
timezone: 'Europe/Paris'
Testing Mock listeners in tests:
$container->get('danilovl_entity_traits.timestampable_listener')->setTimezone('UTC');
Identity Strategy Conflicts
IdTrait, UuidTrait, and UlidTrait in the same entity causes conflicts.UuidTrait for all UUID-based entities).Required vs. Optional Traits
Required and Optional traits for the same field type (e.g., NameTrait vs. Optional\Content\NameTrait) can lead to type mismatches.Optional for nullable fields).Soft Delete Filter Auto-Registration
soft_delete.filter_auto_enable means the filter won’t apply globally.filter_auto_enable: true in config or register manually.Blameable Without User Class
blameable.enabled: true without setting blameable.user_class throws exceptions.user_class when blameable.enabled is true.Timestampable Timezone Mismatches
UTC in config but expecting local time in queries.->setTimezone(new \DateTimeZone('UTC'))).TreePathListener Performance
path/depth on every flush for large hierarchies.#[Tree(autoRebuild: false)] and manually trigger updates.Validator Overrides
validator.constraint_validator:
services:
App\Validator\CustomEmailValidator:
tags: ['validator.constraint_validator']
Attribute Conflicts
#[AutoSlug] attributes on one entity.Listener Debugging
$this->get('danilovl_entity_traits.timestampable_listener')->setDebug(true);
Schema Validation
doctrine:schema:validate to catch mapping issues:
php bin/console doctrine:schema:validate
Event Dispatching
$dispatcher->addListener(Events::PRE_PERSIST, function ($event) {
error_log('Event triggered for: ' . get_class($event->getObject()));
});
Trait Overrides
if (method_exists($this, 'getSlug')) {
// Method exists (either from trait or parent)
}
Custom Listeners
Extend base listeners (e.g., TimestampableListener) to add logic:
class CustomTimestampableListener extends TimestampableListener
{
public function prePersist(LifecycleEventArgs $args): void
{
parent::prePersist($args);
// Add custom logic
}
}
Register it as a service with the same tag as the original listener.
Dynamic Trait Loading
Use PHP’s class_uses() to inspect traits at runtime:
if (in_array(\Danilovl\EntityTraitsBundle\Trait\Optional\Audit\BlameableTrait::class, class_uses($entity))) {
// Entity has BlameableTrait
}
Attribute Interceptors Create custom attributes to extend functionality:
#[Attribute]
class AutoHash
{
public function __construct(public string $property, public string $algorithm = 'sha256') {}
}
Register a listener to handle the attribute.
Doctrine Events Hook into Doctrine events for custom logic:
$
How can I help you explore Laravel packages today?