arthem/object-reference-bundle
Doctrine ORM bundle that stores references to any entity as a type and ID pair, using the ObjectReference attribute and lazy closures so relations stay lightweight and framework-agnostic.
Installation:
composer require arthem/object-reference-bundle
Ensure your project uses Symfony 6.3+ or 7.x and Doctrine ORM 2.5+.
Enable the Bundle:
Add to config/bundles.php:
return [
// ...
Arthem\ObjectReferenceBundle\ArthemObjectReferenceBundle::class => ['all' => true],
];
First Use Case:
Define an entity with a polymorphic reference using the #[ObjectReference] attribute:
use Arthem\ObjectReferenceBundle\Mapping\Attribute\ObjectReference;
#[ORM\Entity]
class Story
{
#[ORM\Column(type: Types::STRING, length: 36, nullable: true)]
#[ObjectReference(keyLength: 15)]
private \Closure|Actor $actor;
private $actorId; // Required, even if unused
private $actorType; // Required, even if unused
// Getter/setter logic as shown in README
}
Database Migration: Run:
php bin/console doctrine:migrations:diff
php bin/console doctrine:migrations:migrate
Use ObjectReference for polymorphic associations (e.g., Actor could be User, Guest, or custom types):
#[ObjectReference(keyLength: 15)]
private \Closure|(User|Guest) $actor;
Leverage closures for deferred loading (e.g., fetching related data only when accessed):
public function getActor(): ?User
{
if ($this->actor instanceof \Closure) {
$this->actor = $this->actor->call($this); // Resolves on first access
}
return $this->actor;
}
Bind polymorphic fields in Symfony forms:
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
$builder->add('actor', EntityType::class, [
'class' => [User::class, Guest::class],
'choice_label' => 'name',
'placeholder' => 'Select an actor...',
]);
Query polymorphic references in repositories:
public function findByActorType(string $type, string $id)
{
return $this->createQueryBuilder('s')
->andWhere('s.actorType = :type')
->andWhere('s.actorId = :id')
->setParameter('type', $type)
->setParameter('id', $id)
->getQuery()
->getResult();
}
Override default key generation (e.g., UUIDs) by extending the bundle or using a custom closure:
#[ObjectReference(keyLength: 36, keyGenerator: new \Ramsey\Uuid\Uuid())]
private \Closure|Actor $actor;
Listen for lifecycle events to manage closures:
use Doctrine\ORM\Events;
$entityManager->getEventManager()->addEventListener(Events::postLoad, function ($event) {
$entity = $event->getObject();
if (method_exists($entity, 'initializeClosures')) {
$entity->initializeClosures();
}
});
Exclude closures from serialization (e.g., API Platform):
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\ApiProperty;
#[ApiResource]
class Story
{
#[ApiProperty(serialize: false)]
private \Closure|Actor $actor;
}
Mock closures in unit tests:
$story = new Story();
$story->setActor(function () {
return $this->createMock(Actor::class);
});
actorId/actorType FieldsObjectReference requires these fields, even if unused. Doctrine will throw errors if omitted.serialize: false in API Platform or implement __serialize()/__unserialize() in your entity.keyLength in #[ObjectReference] doesn’t match the column length, Doctrine may fail to map data.Types::STRING column length ≥ keyLength.Story → Actor → Story) can cause infinite loops.getActor():
if ($this->actor instanceof \Closure && $this->isInCycle()) {
return null; // or throw an exception
}
postLoad (as shown in integration tips) or use #[ORM\HasLifecycleCallbacks].Add debug logs to track when closures are resolved:
public function getActor(): ?Actor
{
if ($this->actor instanceof \Closure) {
\Log::debug('Resolving actor closure for Story ID: ' . $this->id);
$this->actor = $this->actor->call($this);
}
return $this->actor;
}
Check entity state after hydration:
$entityManager->getEventManager()->addEventListener(Events::postLoad, function ($event) {
$entity = $event->getObject();
if ($entity instanceof Story && $entity->getActor() instanceof \Closure) {
throw new \RuntimeException('Actor closure not resolved!');
}
});
Enable Doctrine query logging:
# config/packages/dev/doctrine.yaml
doctrine:
dbal:
logging: true
profiling: true
Extend the bundle to support custom key generators:
// src/KeyGenerator/CustomUuidGenerator.php
namespace App\KeyGenerator;
use Arthem\ObjectReferenceBundle\KeyGenerator\KeyGeneratorInterface;
use Ramsey\Uuid\Uuid;
class CustomUuidGenerator implements KeyGeneratorInterface
{
public function generate(object $object): string
{
return Uuid::uuid4()->toString();
}
}
Use it in your entity:
#[ObjectReference(keyGenerator: new \App\KeyGenerator\CustomUuidGenerator())]
Listen for object_reference.load events to intercept closure resolution:
use Arthem\ObjectReferenceBundle\Event\ObjectReferenceEvents;
$eventDispatcher->addListener(ObjectReferenceEvents::LOAD, function ($event) {
if ($event->getObject() instanceof Story) {
$event->setResolvedObject($event->getClosure()->call($event->getObject()));
}
});
Override the default Types::STRING mapping for specific use cases:
// src/DependencyInjection/Configuration.php
$builder->node('object_reference')
->children()
->scalarNode('default_type')->defaultValue('string')->end()
->end();
Add validation for polymorphic references:
use Symfony\Component\Validator\Constraints as Assert;
class Story
{
#[Assert\Valid]
#[ObjectReference]
private \Closure|Actor $actor;
}
How can I help you explore Laravel packages today?