chamber-orchestra / metadata-bundle
Symfony bundle for extending Doctrine ORM entities with custom attribute-driven metadata — cacheable mapping, embedded entity support, and autoconfigured drivers
Installs: 69
Dependents: 3
Suggesters: 0
Security: 0
Stars: 0
Watchers: 0
Forks: 0
Open Issues: 0
Type:symfony-bundle
pkg:composer/chamber-orchestra/metadata-bundle
Requires
- php: ^8.5
- doctrine/common: ^3.0
- doctrine/doctrine-bundle: 3.2.*
- doctrine/orm: 3.6.*
- symfony/config: 8.0.*
- symfony/dependency-injection: 8.0.*
- symfony/framework-bundle: 8.0.*
- symfony/runtime: 8.0.*
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.94
- phpstan/phpstan: ^2.1
- phpunit/phpunit: ^13.0
- symfony/test-pack: ^1.2
Conflicts
README
ChamberOrchestra Metadata Bundle
A Symfony bundle for extending Doctrine ORM entities with custom attribute-driven metadata. Define PHP attributes on your entities and let autoconfigured mapping drivers turn them into cacheable, serializable metadata — with full support for embedded classes and multiple entity managers.
Features
- PHP attribute-based mapping — define custom metadata using native PHP 8 attributes on entity classes and properties
- Cacheable metadata — multi-level cache (PSR-6 + in-memory) with serialization support for production performance
- Embedded entity support — automatic recursive metadata resolution for Doctrine embeddables with lazy field initialization
- Autoconfigured mapping drivers — implement
MappingDriverInterfaceand drivers are auto-tagged via Symfony DI - Doctrine event integration — hooks into
loadClassMetadatato load extension metadata alongside Doctrine's own metadata - Multiple EntityManager support — cache isolation per EntityManager via
spl_object_idscoping - Zero configuration — install the bundle and start writing drivers; no YAML/XML configuration required
Installation
composer require chamber-orchestra/metadata-bundle
Quick Start
1. Define a Mapping Driver
Create a mapping driver by extending AbstractMappingDriver. Override getClassAttribute() or getPropertyAttribute() to declare which PHP attributes your extension requires:
use ChamberOrchestra\MetadataBundle\Mapping\Driver\AbstractMappingDriver; use ChamberOrchestra\MetadataBundle\Mapping\ExtensionMetadataInterface; class TimestampableDriver extends AbstractMappingDriver { protected function getClassAttribute(): string|null { return Timestampable::class; } public function loadMetadataForClass(ExtensionMetadataInterface $extensionMetadata): void { // Read attributes and populate your metadata configuration } }
Drivers implementing MappingDriverInterface are automatically tagged and registered by the bundle.
2. Create a Metadata Factory
Extend AbstractExtensionMetadataFactory to define how your extension metadata is created and loaded:
use ChamberOrchestra\MetadataBundle\Mapping\AbstractExtensionMetadataFactory; use ChamberOrchestra\MetadataBundle\Mapping\ExtensionMetadataInterface; use Doctrine\Persistence\Mapping\ClassMetadata; class TimestampableMetadataFactory extends AbstractExtensionMetadataFactory { protected function newClassMetadataInstance(ClassMetadata $metadata): ExtensionMetadataInterface { return new ExtensionMetadata($metadata); } protected function doLoadMetadata(ExtensionMetadataInterface $class): void { // Delegate to your mapping drivers } }
3. React to Doctrine Events
Use AbstractDoctrineListener to access extension metadata during Doctrine lifecycle events:
use ChamberOrchestra\MetadataBundle\EventSubscriber\AbstractDoctrineListener; use Doctrine\Bundle\DoctrineBundle\Attribute\AsDoctrineListener; use Doctrine\ORM\Event\PrePersistEventArgs; use Doctrine\ORM\Events; #[AsDoctrineListener(event: Events::prePersist)] class TimestampableListener extends AbstractDoctrineListener { public function prePersist(PrePersistEventArgs $args): void { foreach ($this->getScheduledEntityInsertions($args->getEntityManager(), TimestampableConfiguration::class) as $metadataArgs) { $metadataArgs->extensionMetadata->setFieldValue( $metadataArgs->entity, 'createdAt', new \DateTimeImmutable() ); } } }
Architecture
MetadataSubscriber (Doctrine loadClassMetadata event)
└── MetadataReader
└── AbstractExtensionMetadataFactory
├── MappingDriverInterface[] (attribute-based mapping drivers)
├── ExtensionMetadataInterface (per-entity extension metadata)
│ ├── MetadataConfigurationInterface[] (per-driver configurations)
│ └── Embedded metadata (recursive for embeddables)
└── PSR-6 Cache (serialized metadata storage)
Key Abstractions
| Class / Interface | Role |
|---|---|
MappingDriverInterface |
Extension point — implement to define attribute-driven metadata |
AbstractMappingDriver |
Base driver with AttributeReader and supports() logic |
MetadataConfigurationInterface |
Stores field mappings; serializable for Doctrine cache |
AbstractDoctrineListener |
Base for Doctrine listeners that need extension metadata |
MetadataArgs |
DTO bundling EntityManager, metadata, configuration, and entity |
Requirements
- PHP 8.5+
- Symfony 8.0
- Doctrine ORM 3.6+
- Doctrine Bundle 3.2+
Development
composer install # Install dependencies composer test # Run the test suite
License
MIT