承接 pechynho/polymorphic-doctrine 相关项目开发

从需求分析到上线部署,全程专人跟进,保证项目质量与交付效率

邮箱:yvsm@zunyunkeji.com | QQ:316430983 | 微信:yvsm316

pechynho/polymorphic-doctrine

Composer 安装命令:

composer require pechynho/polymorphic-doctrine

包简介

Doctrine polymorphic relations for Symfony applications.

README 文档

README

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

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:

Mode DB Columns Foreign Keys Best For
Dynamic {property}_type + {property}_id No Flexible scenarios where FK constraints aren't needed
Explicit {property}_type + one ID column per mapped type Yes Scenarios requiring referential integrity

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

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)

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

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

Dynamic Mode

Creates two columns per polymorphic property:

id dynamic_subject_type dynamic_subject_id
1 eshop_item 123
2 subscription 456
3 NULL NULL

Pros: Simple setup, no class generation needed. Cons: No foreign keys — the database cannot enforce referential integrity.

Explicit Mode

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

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

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

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

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

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

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()

The getValueAs() method provides a convenient way to retrieve the referenced entity with a narrowed return type. It accepts a class-string<T> 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

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

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

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

Method Description
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

# 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

Attributes

#[EntityWithPolymorphicRelations]

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

#[DynamicPolymorphicProperty(array $mapping)]

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

Parameter Type Description
$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)]

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

Parameter Type Description
$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 pair

Per-type detailed configuration in the mapping:

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

PolymorphicValueInterface

Method Return Type Description
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

Service Description
PolymorphicValueFactoryInterface Creates polymorphic value instances
PolymorphicSearchExprBuilderFactoryInterface Creates search expression builders
PolymorphicSearchExprApplierFactoryInterface Creates appliers that directly modify QueryBuilder

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

Problem Solution
Reference classes not found (explicit mode) Run php bin/console pechynho:polymorphic-doctrine:generate-reference-classes then php bin/console cache:clear
Polymorphic properties not working Verify the #[EntityWithPolymorphicRelations] attribute is on the entity, the bundle is registered, and discovery directories are configured
Slow queries Enable indexes via enableDiscriminatorIndex and enablePairIndex on your attributes

Requirements

  • PHP >= 8.4
  • Symfony 6.4 / 7.x / 8.x
  • Doctrine ORM 2.19+ or 3.3+

License

MIT — see LICENSE.

Author

Jan Pechpechynho@gmail.com

pechynho/polymorphic-doctrine 适用场景与选型建议

pechynho/polymorphic-doctrine 是一款 基于 PHP 开发的 Composer 扩展包,目前已累计 3 次下载、GitHub Stars 达 1, 最近一次更新时间为 2025 年 07 月 16 日, 在 PHP 生态内属于活跃度较高的组件。

我们在过去多个企业项目中使用过 pechynho/polymorphic-doctrine 或与其功能相近的方案,如果你在选型或落地过程中遇到问题,例如 版本兼容、二次改造、私有化封装、与内部系统对接、生产 BUG 排查,欢迎联系我们协助评估。

围绕 pechynho/polymorphic-doctrine 我们能提供哪些服务?
定制开发 / 二次开发

基于 pechynho/polymorphic-doctrine 在你已有业务上做功能扩展、字段裁剪、UI 适配、与内部账号 / 权限 / 日志系统的深度对接。

BUG 修复 & 性能优化

线上偶发问题、内存泄漏、慢查询、并发异常等排查修复;针对高流量场景做缓存、队列、索引层面的调优。

项目外包 & 长期维护

承接完整的项目从需求 → 设计 → 开发 → 上线 → 长期运维;也可按月提供技术保姆服务。

yvsm@zunyunkeji.com QQ:316430983 微信:yvsm316 西安尊云信息科技 · 专注 PHP / Go / 分布式系统研发

统计信息

  • 总下载量: 3
  • 月度下载量: 0
  • 日度下载量: 0
  • 收藏数: 1
  • 点击次数: 17
  • 依赖项目数: 0
  • 推荐数: 0

GitHub 信息

  • Stars: 1
  • Watchers: 0
  • Forks: 0
  • 开发语言: PHP

其他信息

  • 授权协议: MIT
  • 更新时间: 2025-07-16