PHPackages                             pechynho/polymorphic-doctrine - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Database &amp; ORM](/categories/database)
4. /
5. pechynho/polymorphic-doctrine

ActiveSymfony-bundle[Database &amp; ORM](/categories/database)

pechynho/polymorphic-doctrine
=============================

Doctrine polymorphic relations for Symfony applications.

v0.2.0(3mo ago)13MITPHPPHP &gt;=8.4

Since Jul 16Pushed 3mo agoCompare

[ Source](https://github.com/Pechynho/polymorphic-doctrine)[ Packagist](https://packagist.org/packages/pechynho/polymorphic-doctrine)[ RSS](/packages/pechynho-polymorphic-doctrine/feed)WikiDiscussions main Synced today

READMEChangelog (1)Dependencies (39)Versions (5)Used By (0)

Polymorphic Doctrine
====================

[](#polymorphic-doctrine)

A Symfony bundle that brings polymorphic relations to Doctrine ORM. It allows a single entity property to reference different entity types — with type safety, IDE support, and optional foreign key constraints.

[![License: MIT](https://camo.githubusercontent.com/fdf2982b9f5d7489dcf44570e714e3a15fce6253e0cc6b5aa61a075aac2ff71b/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4c6963656e73652d4d49542d79656c6c6f772e737667)](https://opensource.org/licenses/MIT)

The Problem
-----------

[](#the-problem)

Doctrine ORM doesn't natively support a property that needs to reference multiple unrelated entity types. Imagine a `Payment` entity with a `subject` property that can point to either an `EshopItem` or a `Subscription`. Standard Doctrine associations require a fixed target type.

This bundle solves it with two approaches:

ModeDB ColumnsForeign KeysBest For**Dynamic**`{property}_type` + `{property}_id`NoFlexible scenarios where FK constraints aren't needed**Explicit**`{property}_type` + one ID column per mapped typeYesScenarios requiring referential integrityInstallation
------------

[](#installation)

```
composer require pechynho/polymorphic-doctrine
```

Without Symfony Flex, add the bundle to `config/bundles.php`:

```
return [
    // ...
    Pechynho\PolymorphicDoctrine\PechynhoPolymorphicDoctrineBundle::class => ['all' => true],
];
```

Quick Start
-----------

[](#quick-start)

### 1. Mark the entity and define polymorphic properties

[](#1-mark-the-entity-and-define-polymorphic-properties)

```
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;

    #[DynamicPolymorphicProperty([
        'eshop_item' => EshopItem::class,
        'subscription' => Subscription::class,
    ])]
    public PolymorphicValueInterface $dynamicSubject;

    #[ExplicitPolymorphicProperty([
        'eshop_item' => EshopItem::class,
        'subscription' => Subscription::class,
    ])]
    public PolymorphicValueInterface $explicitSubject;
}
```

### 2. Generate reference classes (explicit mode only)

[](#2-generate-reference-classes-explicit-mode-only)

```
php bin/console pechynho:polymorphic-doctrine:generate-reference-classes
```

### 3. Work with polymorphic values

[](#3-work-with-polymorphic-values)

```
class PaymentService
{
    public function __construct(
        private readonly EntityManagerInterface $em,
        private readonly PolymorphicValueFactoryInterface $polymorphicValueFactory,
    ) {}

    public function createPayment(EshopItem $item): void
    {
        $payment = new Payment();

        // Initialize with a value
        $payment->dynamicSubject = $this->polymorphicValueFactory->create(
            Payment::class, 'dynamicSubject', $item
        );

        // Initialize as null
        $payment->explicitSubject = $this->polymorphicValueFactory->create(
            Payment::class, 'explicitSubject'
        );

        $this->em->persist($payment);
        $this->em->flush();
    }
}
```

Polymorphic Modes
-----------------

[](#polymorphic-modes)

### Dynamic Mode

[](#dynamic-mode)

Creates two columns per polymorphic property:

iddynamic\_subject\_typedynamic\_subject\_id1eshop\_item1232subscription4563NULLNULL**Pros:** Simple setup, no class generation needed. **Cons:** No foreign keys — the database cannot enforce referential integrity.

### Explicit Mode

[](#explicit-mode)

Creates a type column plus a dedicated ID column for each mapped entity type:

idexplicit\_subject\_typeexplicit\_subject\_eshop\_item\_idexplicit\_subject\_subscription\_id1eshop\_item123NULL2subscriptionNULL4563NULLNULLNULL**Pros:** Foreign key on each ID column, referential integrity enforced by the database. **Cons:** Requires reference class generation, more columns per table.

Working with Polymorphic Values
-------------------------------

[](#working-with-polymorphic-values)

### Creating and Initializing

[](#creating-and-initializing)

```
// With a value
$payment->subject = $this->polymorphicValueFactory->create(Payment::class, 'subject', $entity);

// As null
$payment->subject = $this->polymorphicValueFactory->create(Payment::class, 'subject');
```

### Updating

[](#updating)

```
// Set a new value
$payment->subject->update($newEntity);

// Set to null
$payment->subject->setNull();
// or: $payment->subject->update(null);
```

### Reading

[](#reading)

```
// Check status
$payment->subject->isNull();       // Is the value null?
$payment->subject->isResolvable(); // Does it have a valid type and ID?
$payment->subject->isLoaded();     // Is the entity already loaded in memory?

// Get the entity (lazy-loaded from DB on first access)
$entity = $payment->subject->getValue();

if ($entity instanceof EshopItem) {
    // ...
} elseif ($entity instanceof Subscription) {
    // ...
}
```

> **Tip:** Always check `isNull()` before calling `getValue()`.

### Type-Narrowing with `getValueAs()`

[](#type-narrowing-with-getvalueas)

The `getValueAs()` method provides a convenient way to retrieve the referenced entity with a narrowed return type. It accepts a `class-string` and returns `T`, giving you full IDE autocompletion and static analysis support (PHPStan / Psalm).

```
// Instead of manual instanceof checks:
$item = $payment->subject->getValueAs(EshopItem::class);
// $item is now typed as EshopItem — full IDE support, no manual narrowing needed
```

The method throws a `ReferenceResolutionException` if:

- The value is `null` — always check `isNull()` first, or handle the exception.
- The resolved entity is not an instance of the requested class.

Searching Polymorphic Values
----------------------------

[](#searching-polymorphic-values)

The bundle provides two approaches for querying polymorphic properties via QueryBuilder.

### Applier — Simple Approach

[](#applier--simple-approach)

Directly modifies the QueryBuilder. Best for straightforward queries.

```
class PaymentService
{
    public function __construct(
        private readonly EntityManagerInterface $em,
        private readonly PolymorphicSearchExprApplierFactoryInterface $applierFactory,
    ) {}

    public function findByEntity(object $entity): array
    {
        $qb = $this->em->createQueryBuilder()
            ->select('p')->from(Payment::class, 'p');

        $applier = $this->applierFactory->create(Payment::class, 'dynamicSubject', 'p');
        $applier->eq($qb, $entity);

        return $qb->getQuery()->getResult();
    }

    public function findByType(string $entityClass): array
    {
        $qb = $this->em->createQueryBuilder()
            ->select('p')->from(Payment::class, 'p');

        $applier = $this->applierFactory->create(Payment::class, 'explicitSubject', 'p');
        $applier->isInstanceOf($qb, $entityClass);

        return $qb->getQuery()->getResult();
    }
}
```

### Builder — Advanced Approach

[](#builder--advanced-approach)

Returns expression objects with parameters that can be composed into complex queries.

```
$builder = $this->builderFactory->create(Payment::class, 'dynamicSubject', 'p');

$eqResult = $builder->eq($item);
$instanceOfResult = $builder->isInstanceOf(EshopItem::class);

$qb->where($qb->expr()->orX($eqResult->expr, $instanceOfResult->expr));

foreach ([$eqResult, $instanceOfResult] as $result) {
    foreach ($result->params as $key => $value) {
        $qb->setParameter($key, $value);
    }
}
```

### Available Operations

[](#available-operations)

MethodDescription`eq($entity)`Matches the given entity`neq($entity)`Does not match the given entity`in(...$entities)`Matches any of the given entities`notIn(...$entities)`Does not match any of the given entities`isNull()`Value is null`isNotNull()`Value is not null`isInstanceOf(...$classes)`Is an instance of the given type(s)`isNotInstanceOf(...$classes)`Is not an instance of the given type(s)Configuration
-------------

[](#configuration)

```
# config/packages/pechynho_polymorphic_doctrine.yaml
pechynho_polymorphic_doctrine:
    # Directory for generated reference classes (explicit mode)
    references_directory: '%kernel.cache_dir%/pechynho/polymorphic-doctrine/references'

    # Namespace for generated classes
    references_namespace: 'Pechynho\PolymorphicDoctrine\AutogeneratedReference'

    # Entity discovery settings
    discover:
        cache_directory: '%kernel.cache_dir%/pechynho/polymorphic-doctrine/discover'
        directories:
            - '%kernel.project_dir%/src'
```

API Reference
-------------

[](#api-reference)

### Attributes

[](#attributes)

#### `#[EntityWithPolymorphicRelations]`

[](#entitywithpolymorphicrelations)

Marks an entity as containing polymorphic relations. Required for entity discovery.

#### `#[DynamicPolymorphicProperty(array $mapping)]`

[](#dynamicpolymorphicpropertyarray-mapping)

Defines a dynamic polymorphic property (two columns: type + ID).

ParameterTypeDescription`$mapping``array`Map of type keys to entity class names`$idProperty``?string`Custom ID property name`$enableDiscriminatorIndex``?bool`Add index on discriminator column`$enablePairIndex``?bool`Add index on type + ID pair#### `#[ExplicitPolymorphicProperty(array $mapping)]`

[](#explicitpolymorphicpropertyarray-mapping)

Defines an explicit polymorphic property (type column + dedicated ID column per mapped type).

ParameterTypeDescription`$mapping``array`Map of type keys to entity class names or detailed config`$idProperty``?string`Default ID property name`$idPropertyType``?string`Default ID property type`$onDelete``?string`Foreign key ON DELETE action`$onUpdate``?string`Foreign key ON UPDATE action`$enableDiscriminatorIndex``?bool`Add index on discriminator column`$enablePairIndex``?bool`Add index on type + ID pairPer-type detailed configuration in the mapping:

```
#[ExplicitPolymorphicProperty([
    'product' => Product::class,
    'service' => [
        'fqcn' => Service::class,
        'idProperty' => 'serviceId',
        'onDelete' => 'CASCADE',
    ],
])]
public PolymorphicValueInterface $subject;
```

### PolymorphicValueInterface

[](#polymorphicvalueinterface)

MethodReturn TypeDescription`isNull()``bool`Is the value null?`isResolvable()``bool`Can the value be resolved to an entity?`isLoaded()``bool`Is the entity already loaded in memory?`getValue()``?object`Returns the referenced entity (lazy-loaded)`getValueAs(string $fqcn)``T`Returns the entity narrowed to the given type, or throws`update(?object $value)``void`Updates the reference to another entity or null`setNull()``void`Sets the reference to null### Services

[](#services)

ServiceDescription`PolymorphicValueFactoryInterface`Creates polymorphic value instances`PolymorphicSearchExprBuilderFactoryInterface`Creates search expression builders`PolymorphicSearchExprApplierFactoryInterface`Creates appliers that directly modify QueryBuilderConsole Commands
----------------

[](#console-commands)

```
# Generate reference classes for explicit polymorphic properties
php bin/console pechynho:polymorphic-doctrine:generate-reference-classes

# Clear cache (discovery + reference classes)
php bin/console pechynho:polymorphic-doctrine:cache-clear
```

> **Important:** After any change to explicit polymorphic property definitions, you must re-run the reference class generation command.

Troubleshooting
---------------

[](#troubleshooting)

ProblemSolutionReference classes not found (explicit mode)Run `php bin/console pechynho:polymorphic-doctrine:generate-reference-classes` then `php bin/console cache:clear`Polymorphic properties not workingVerify the `#[EntityWithPolymorphicRelations]` attribute is on the entity, the bundle is registered, and discovery directories are configuredSlow queriesEnable indexes via `enableDiscriminatorIndex` and `enablePairIndex` on your attributesRequirements
------------

[](#requirements)

- PHP &gt;= 8.4
- Symfony 6.4 / 7.x / 8.x
- Doctrine ORM 2.19+ or 3.3+

License
-------

[](#license)

MIT — see [LICENSE](LICENSE).

Author
------

[](#author)

**Jan Pech** —

###  Health Score

37

—

LowBetter than 81% of packages

Maintenance82

Actively maintained with recent releases

Popularity5

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity47

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~87 days

Total

4

Last Release

91d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/7fb132d3b04bd0147ff66099090bc420927ee103372ca86283a87143e4c6f190?d=identicon)[Pechynho](/maintainers/Pechynho)

---

Top Contributors

[![Pechynho](https://avatars.githubusercontent.com/u/40825746?v=4)](https://github.com/Pechynho "Pechynho (10 commits)")

###  Code Quality

TestsPHPUnit

Static AnalysisPHPStan, Rector

Code StylePHP CS Fixer

Type Coverage Yes

### Embed Badge

![Health badge](/badges/pechynho-polymorphic-doctrine/health.svg)

```
[![Health](https://phpackages.com/badges/pechynho-polymorphic-doctrine/health.svg)](https://phpackages.com/packages/pechynho-polymorphic-doctrine)
```

###  Alternatives

[easycorp/easyadmin-bundle

Admin generator for Symfony applications

4.3k17.9M386](/packages/easycorp-easyadmin-bundle)[sylius/sylius

E-Commerce platform for PHP, based on Symfony framework.

8.5k5.9M737](/packages/sylius-sylius)[oro/platform

Business Application Platform (BAP)

645143.5k115](/packages/oro-platform)[shopware/core

Shopware platform is the core for all Shopware ecommerce products.

585.6M574](/packages/shopware-core)[contao/core-bundle

Contao Open Source CMS

1231.6M2.8k](/packages/contao-core-bundle)[open-dxp/opendxp

Content &amp; Product Management Framework (CMS/PIM)

9421.6k61](/packages/open-dxp-opendxp)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
