pechynho / polymorphic-doctrine
Doctrine polymorphic relations for Symfony applications.
Installs: 0
Dependents: 0
Suggesters: 0
Security: 0
Stars: 0
Watchers: 0
Forks: 0
Open Issues: 0
Type:symfony-bundle
Requires
- php: >=8.4
- doctrine/doctrine-bundle: ^2.12
- doctrine/orm: ^2.19 || ^3.3
- nette/php-generator: ^4.1
- spatie/php-structure-discoverer: ^2.3
- symfony/cache-contracts: ^3.6
- symfony/config: 6.4.* || ^7.3
- symfony/console: 6.4.* || ^7.3
- symfony/dependency-injection: 6.4.* || ^7.3
- symfony/filesystem: 6.4.* || ^7.3
- symfony/http-kernel: 6.4.* || ^7.3
- symfony/options-resolver: 6.4.* || ^7.3
- symfony/property-access: 6.4.* || ^7.3
- symfony/service-contracts: ^3.6
- symfony/string: 6.4.* || ^7.3
- webmozart/assert: ^1.11
README
A Symfony bundle that provides polymorphic relations for Doctrine ORM, allowing you to create flexible associations where a single property can reference different entity types.
Features
- Two polymorphic modes: Dynamic and Explicit
- Type-safe polymorphic relations with proper IDE support
- Foreign key constraints preservation (explicit mode)
- Query builder integration for searching polymorphic values
- Automatic reference class generation for explicit mode
- Caching support for improved performance
Installation
Install the bundle via Composer:
composer require pechynho/polymorphic-doctrine
If you're not using Symfony Flex, add the bundle to your config/bundles.php
:
<?php return [ // ... Pechynho\PolymorphicDoctrine\PechynhoPolymorphicDoctrineBundle::class => ['all' => true], ];
Configuration
The bundle can be configured in your config/packages/pechynho_polymorphic_doctrine.yaml
:
pechynho_polymorphic_doctrine: # Directory where reference classes are generated (explicit mode) references_directory: '%kernel.cache_dir%/pechynho/polymorphic-doctrine/references' # Namespace for generated reference classes references_namespace: 'Pechynho\PolymorphicDoctrine\AutogeneratedReference' # Entity discovery configuration discover: cache_directory: '%kernel.cache_dir%/pechynho/polymorphic-doctrine/discover' directories: - '%kernel.project_dir%/src'
Polymorphic Modes
Dynamic Mode
Uses two database columns: one for the entity type and another for the entity ID. This approach is more flexible but doesn't preserve foreign key constraints.
For a dynamic polymorphic property, two columns are created:
{property_name}_type
- Stores the entity type key{property_name}_id
- Stores the entity ID
Example table structure for Payment entity with dynamicSubject
property:
Sample data:
id | dynamic_subject_type | dynamic_subject_id |
---|---|---|
1 | eshop_item | 123 |
2 | subscription | 456 |
3 | NULL | NULL |
Explicit Mode
Creates one column for the entity type and separate columns for each possible entity type's ID. This preserves foreign key constraints and provides better database integrity.
For an explicit polymorphic property, multiple columns are created:
{property_name}_type
- Stores the entity type key{property_name}_{type_key}_id
- One column for each mapped entity type
Example table structure for Payment entity with explicitSubject
property:
Sample data:
id | explicit_subject_type | explicit_subject_eshop_item_id | explicit_subject_subscription_id |
---|---|---|---|
1 | eshop_item | 123 | NULL |
2 | subscription | NULL | 456 |
3 | NULL | NULL | NULL |
Foreign key constraints are automatically created for explicit mode columns, ensuring referential integrity.
Usage
1. Define Your Entities
First, create the entities that will be referenced polymorphically:
<?php namespace App\Entity; use Doctrine\ORM\Mapping as ORM; #[ORM\Entity] class EshopItem { #[ORM\Id] #[ORM\GeneratedValue] #[ORM\Column] private ?int $id = null; // ... other properties and methods }
<?php namespace App\Entity; use Doctrine\ORM\Mapping as ORM; #[ORM\Entity] class Subscription { #[ORM\Id] #[ORM\GeneratedValue] #[ORM\Column] private ?int $id = null; // ... other properties and methods }
2. Create Entity with Polymorphic Relations
<?php namespace App\Entity; use Doctrine\ORM\Mapping as ORM; use Pechynho\PolymorphicDoctrine\Attributes\DynamicPolymorphicProperty; use Pechynho\PolymorphicDoctrine\Attributes\EntityWithPolymorphicRelations; use Pechynho\PolymorphicDoctrine\Attributes\ExplicitPolymorphicProperty; use Pechynho\PolymorphicDoctrine\Contract\PolymorphicValueInterface; #[ORM\Entity] #[EntityWithPolymorphicRelations] class Payment { #[ORM\Id] #[ORM\GeneratedValue] #[ORM\Column] private ?int $id = null; #[ExplicitPolymorphicProperty([ 'eshop_item' => EshopItem::class, 'subscription' => Subscription::class, ])] public PolymorphicValueInterface $explicitSubject; #[DynamicPolymorphicProperty([ 'eshop_item' => EshopItem::class, 'subscription' => Subscription::class, ])] public PolymorphicValueInterface $dynamicSubject; }
3. Generate Reference Classes (Explicit Mode)
If you're using explicit polymorphic properties, you need to generate reference classes for the defined polymorphic properties. Run the following command:
php bin/console pechynho:polymorphic-doctrine:generate-reference-classes
4. Working with Polymorphic Values
This section provides comprehensive examples of how to work with polymorphic values, covering initialization, updates, null handling, and property reading.
<?php namespace App\Service; use App\Entity\EshopItem; use App\Entity\Payment; use App\Entity\Subscription; use Doctrine\ORM\EntityManagerInterface; use Pechynho\PolymorphicDoctrine\Contract\PolymorphicSearchExprApplierFactoryInterface; use Pechynho\PolymorphicDoctrine\Contract\PolymorphicValueFactoryInterface; class PaymentService { public function __construct( private readonly EntityManagerInterface $em, private readonly PolymorphicValueFactoryInterface $polymorphicValueFactory, private readonly PolymorphicSearchExprApplierFactoryInterface $searchExprApplierFactory, ) {} /** * Example 1: Initializing polymorphic properties with values */ public function createPaymentWithValues(): void { // First, create and persist the entities you want to reference $eshopItem = new EshopItem(); $this->em->persist($eshopItem); $subscription = new Subscription(); $this->em->persist($subscription); $this->em->flush(); // Create payment with polymorphic properties initialized with values $payment = new Payment(); // Initialize with specific entities - third parameter is the entity to reference $payment->dynamicSubject = $this->polymorphicValueFactory->create(Payment::class, 'dynamicSubject', $eshopItem); $payment->explicitSubject = $this->polymorphicValueFactory->create(Payment::class, 'explicitSubject', $subscription); $this->em->persist($payment); $this->em->flush(); } /** * Example 2: Initializing polymorphic properties with null values */ public function createPaymentWithNullValues(): void { $payment = new Payment(); // Initialize with null values - omit the third parameter or pass null explicitly $payment->dynamicSubject = $this->polymorphicValueFactory->create(Payment::class, 'dynamicSubject'); $payment->explicitSubject = $this->polymorphicValueFactory->create(Payment::class, 'explicitSubject', null); $this->em->persist($payment); $this->em->flush(); } /** * Example 3: Updating polymorphic values */ public function updatePolymorphicValues(Payment $payment): void { // Create new entities to update to $newEshopItem = new EshopItem(); $this->em->persist($newEshopItem); $newSubscription = new Subscription(); $this->em->persist($newSubscription); $this->em->flush(); // Update polymorphic values using the update() method $payment->dynamicSubject->update($newSubscription); $payment->explicitSubject->update($newEshopItem); // You can also update to null // $payment->dynamicSubject->update(null); $this->em->persist($payment); $this->em->flush(); } /** * Example 4: Setting polymorphic values to null */ public function setPolymorphicValuesToNull(Payment $payment): void { // Method 1: Using setNull() method (recommended) $payment->dynamicSubject->setNull(); $payment->explicitSubject->setNull(); // Method 2: Using update() with null // $payment->dynamicSubject->update(null); // $payment->explicitSubject->update(null); $this->em->persist($payment); $this->em->flush(); } /** * Example 5: Reading polymorphic properties and checking their status */ public function readPolymorphicProperties(Payment $payment): void { // Check if the polymorphic value is null if ($payment->dynamicSubject->isNull()) { echo "Dynamic subject is null\n"; return; } // Check if the value can be resolved (has valid type and ID) if (!$payment->dynamicSubject->isResolvable()) { echo "Dynamic subject cannot be resolved (invalid type or ID)\n"; return; } // Check if the entity is already loaded in memory if ($payment->dynamicSubject->isLoaded()) { echo "Dynamic subject is already loaded\n"; } else { echo "Dynamic subject will be loaded from database when accessed\n"; } // Get the actual entity value (this will load it from database if not already loaded) $value = $payment->dynamicSubject->getValue(); // Type-check and handle different entity types if ($value instanceof EshopItem) { echo "Dynamic subject is an EshopItem with ID: " . $value->getId() . "\n"; // Handle eshop item specific logic } elseif ($value instanceof Subscription) { echo "Dynamic subject is a Subscription with ID: " . $value->getId() . "\n"; // Handle subscription specific logic } // You can also work with explicit polymorphic properties the same way if (!$payment->explicitSubject->isNull()) { $explicitValue = $payment->explicitSubject->getValue(); switch (true) { case $explicitValue instanceof EshopItem: echo "Explicit subject is an EshopItem\n"; break; case $explicitValue instanceof Subscription: echo "Explicit subject is a Subscription\n"; break; default: echo "Explicit subject is of unknown type\n"; } } } /** * Example 6: Complete workflow - create, update, read, and nullify */ public function completeWorkflow(): void { // Step 1: Create entities $eshopItem = new EshopItem(); $subscription = new Subscription(); $this->em->persist($eshopItem); $this->em->persist($subscription); $this->em->flush(); // Step 2: Create payment with initial values $payment = new Payment(); $payment->dynamicSubject = $this->polymorphicValueFactory->create(Payment::class, 'dynamicSubject', $eshopItem); $payment->explicitSubject = $this->polymorphicValueFactory->create(Payment::class, 'explicitSubject', $subscription); $this->em->persist($payment); $this->em->flush(); // Step 3: Read and verify initial values echo "Initial dynamic subject: " . get_class($payment->dynamicSubject->getValue()) . "\n"; echo "Initial explicit subject: " . get_class($payment->explicitSubject->getValue()) . "\n"; // Step 4: Update values $payment->dynamicSubject->update($subscription); $payment->explicitSubject->update($eshopItem); $this->em->persist($payment); $this->em->flush(); // Step 5: Read updated values echo "Updated dynamic subject: " . get_class($payment->dynamicSubject->getValue()) . "\n"; echo "Updated explicit subject: " . get_class($payment->explicitSubject->getValue()) . "\n"; // Step 6: Set to null $payment->dynamicSubject->setNull(); $payment->explicitSubject->setNull(); $this->em->persist($payment); $this->em->flush(); // Step 7: Verify null state echo "Dynamic subject is null: " . ($payment->dynamicSubject->isNull() ? 'true' : 'false') . "\n"; echo "Explicit subject is null: " . ($payment->explicitSubject->isNull() ? 'true' : 'false') . "\n"; } }
Key Points for Working with Polymorphic Values:
-
Initialization with values: Use
$this->polymorphicValueFactory->create(EntityClass::class, 'propertyName', $entity)
where the third parameter is the entity to reference. -
Initialization with null: Use
$this->polymorphicValueFactory->create(EntityClass::class, 'propertyName')
or passnull
as the third parameter. -
Updating values: Use the
update($entity)
method on the polymorphic value object. You can pass any entity that matches the configured mapping ornull
. -
Setting to null: Use the
setNull()
method (recommended) orupdate(null)
. -
Reading values: Always check
isNull()
first, then optionally checkisResolvable()
andisLoaded()
before callinggetValue()
. -
Status methods:
isNull()
: Returns true if the polymorphic value is nullisResolvable()
: Returns true if the value has valid type and ID that can be resolved to an entityisLoaded()
: Returns true if the entity is already loaded in memory (avoids database query)getValue()
: Returns the actual entity object (loads from database if needed)
-
Persistence: Always call
$this->em->persist($entity)
and$this->em->flush()
after modifying polymorphic values to save changes to the database.
API Reference
Attributes
#[EntityWithPolymorphicRelations]
Mark an entity class to indicate it contains polymorphic relations. This attribute is required for entity discovery.
#[DynamicPolymorphicProperty(array $mapping)]
Defines a dynamic polymorphic property that uses two database columns (type and ID).
Parameters:
$mapping
- Array mapping type keys to entity class names$iddProperty
- Custom ID property name (optional)$enableDiscriminatorIndex
- Enable index on discriminator column (optional)$enablePairIndex
- Enable index on type+ID pair (optional)
Example:
#[DynamicPolymorphicProperty([ 'product' => Product::class, 'service' => Service::class, ])] public PolymorphicValueInterface $subject;
#[ExplicitPolymorphicProperty(array $mapping)]
Defines an explicit polymorphic property that creates separate columns for each entity type, preserving foreign key constraints.
Parameters:
$mapping
- Array mapping type keys to entity class names or detailed configuration$idProperty
- Default ID property name (optional)$idPropertyType
- Default ID property type (optional)$onDelete
- Foreign key ON DELETE action (optional)$onUpdate
- Foreign key ON UPDATE action (optional)$enableDiscriminatorIndex
- Enable index on discriminator column (optional)$enablePairIndex
- Enable index on type+ID pair (optional)
Example:
#[ExplicitPolymorphicProperty([ 'product' => Product::class, 'service' => [ 'fqcn' => Service::class, 'idProperty' => 'serviceId', 'onDelete' => 'CASCADE', ], ])] public PolymorphicValueInterface $subject;
PolymorphicValueInterface
The main interface for working with polymorphic values.
Methods
isNull(): bool
- Check if the polymorphic value is nullisResolvable(): bool
- Check if the value can be resolved to an entityisLoaded(): bool
- Check if the entity is already loadedsetNull(): void
- Set the polymorphic value to nullupdate(?object $value): void
- Update the polymorphic value with a new entitygetValue(): ?object
- Get the actual entity object
Services
PolymorphicValueFactoryInterface
Factory for creating polymorphic value instances.
public function create(string $fqcn, string $property, ?object $value = null): PolymorphicValueInterface
PolymorphicSearchExprApplierFactoryInterface
Factory for creating search expression appliers for querying polymorphic values.
$applier = $this->searchExprApplierFactory->create(Entity::class, 'propertyName', 'alias'); $applier->eq($queryBuilder, $entity); // Add WHERE condition
Console Commands
pechynho:polymorphic-doctrine:cache-clear
Clears the cache for polymorphic relations.
php bin/console pechynho:polymorphic-doctrine:cache-clear
This command clears both the polymorphic locator cache and generated reference classes cache.
pechynho:polymorphic-doctrine:generate-reference-classes
Generates reference classes for explicit polymorphic properties.
php bin/console pechynho:polymorphic-doctrine:generate-reference-classes
Important: This command must be run after each change to polymorphic property definitions when using explicit mode. The generated classes are required for proper foreign key constraint handling.
Searching Polymorphic Values
The bundle provides powerful search capabilities for polymorphic values through two main interfaces: builders and appliers. Both support the same search operations but differ in how they're used.
Search Operations
All search interfaces support these operations:
eq(entity)
- Find records where polymorphic value equals the given entityneq(entity)
- Find records where polymorphic value does not equal the given entityisNull()
- Find records where polymorphic value is nullisNotNull()
- Find records where polymorphic value is not nullisInstanceOf(class...)
- Find records where polymorphic value is instance of given class(es)isNotInstanceOf(class...)
- Find records where polymorphic value is not instance of given class(es)in(entities...)
- Find records where polymorphic value is one of the given entitiesnotIn(entities...)
- Find records where polymorphic value is not one of the given entities
Search Expression Builders
PolymorphicSearchExprBuilderInterface returns expression objects and parameters that you can use to manually build complex queries.
Usage Example
use Pechynho\PolymorphicDoctrine\Contract\PolymorphicSearchExprBuilderFactoryInterface; class PaymentService { public function __construct( private readonly EntityManagerInterface $em, private readonly PolymorphicSearchExprBuilderFactoryInterface $builderFactory, ) {} public function findComplexPayments(EshopItem $item, Subscription $subscription): array { $qb = $this->em->createQueryBuilder(); $qb->select('p')->from(Payment::class, 'p'); // Create builder for the polymorphic property $builder = $this->builderFactory->create(Payment::class, 'dynamicSubject', 'p'); // Build complex conditions $eqResult = $builder->eq($item); $instanceOfResult = $builder->isInstanceOf(EshopItem::class, Subscription::class); $inResult = $builder->in($item, $subscription); // Combine expressions manually $qb->where($qb->expr()->orX( $eqResult->expr, $instanceOfResult->expr, $inResult->expr )); // Set parameters foreach ([$eqResult, $instanceOfResult, $inResult] as $result) { foreach ($result->params as $key => $value) { $qb->setParameter($key, $value); } } return $qb->getQuery()->getResult(); } }
Search Expression Appliers
PolymorphicSearchExprApplierInterface directly modifies the QueryBuilder, making it easier to use for simple queries.
Usage Example
use Pechynho\PolymorphicDoctrine\Contract\PolymorphicSearchExprApplierFactoryInterface; class PaymentService { public function __construct( private readonly EntityManagerInterface $em, private readonly PolymorphicSearchExprApplierFactoryInterface $applierFactory, ) {} public function findPaymentsByEntity(object $entity): array { $qb = $this->em->createQueryBuilder(); $qb->select('p')->from(Payment::class, 'p'); // Create applier and directly apply condition $applier = $this->applierFactory->create(Payment::class, 'dynamicSubject', 'p'); $applier->eq($qb, $entity); return $qb->getQuery()->getResult(); } public function findPaymentsByType(string $entityClass): array { $qb = $this->em->createQueryBuilder(); $qb->select('p')->from(Payment::class, 'p'); $applier = $this->applierFactory->create(Payment::class, 'explicitSubject', 'p'); $applier->isInstanceOf($qb, $entityClass); return $qb->getQuery()->getResult(); } public function findNullPayments(): array { $qb = $this->em->createQueryBuilder(); $qb->select('p')->from(Payment::class, 'p'); $applier = $this->applierFactory->create(Payment::class, 'dynamicSubject', 'p'); $applier->isNull($qb); return $qb->getQuery()->getResult(); } public function findPaymentsExcluding(object ...$entities): array { $qb = $this->em->createQueryBuilder(); $qb->select('p')->from(Payment::class, 'p'); $applier = $this->applierFactory->create(Payment::class, 'dynamicSubject', 'p'); $applier->notIn($qb, ...$entities); return $qb->getQuery()->getResult(); } }
Best Practices
- Use explicit mode when you need foreign key constraints and database integrity
- Use dynamic mode when you need maximum flexibility and don't require foreign key constraints
- Always run the generate-reference-classes command after modifying explicit polymorphic properties
- Clear cache after making configuration changes
- Use meaningful type keys in your mapping arrays for better readability
Troubleshooting
Common Issues
Reference classes not found (explicit mode)
- Run
php bin/console pechynho:polymorphic-doctrine:generate-reference-classes
- Clear cache with
php bin/console cache:clear
Polymorphic properties not working
- Ensure the entity is marked with
#[EntityWithPolymorphicRelations]
- Check that the bundle is properly registered
- Verify your entity discovery configuration
Performance issues
- Enable appropriate database indexes using the
enableDiscriminatorIndex
andenablePairIndex
options - Consider using explicit mode for better query performance with foreign key constraints
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
License
This bundle is released under the MIT License. See the LICENSE file for details.
Author
- Jan Pech - pechynho@gmail.com