Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Doctrine Discriminator Bundle Laravel Package

levelab/doctrine-discriminator-bundle

A Symfony bundle that simplifies Doctrine discriminator mapping for entity inheritance, reducing boilerplate when configuring discriminator maps. Useful for single-table inheritance setups and cleaner Doctrine configuration in Symfony applications.

View on GitHub
Deep Wiki
Context7

Getting Started

Reality check before anything else. This is a Symfony bundle targeting Symfony 2.x/3.x and Doctrine ORM 2.5-era APIs. It has no Laravel ServiceProvider, no package discovery, and depends on symfony/http-kernel. If you are on Laravel + Eloquent, there is no discriminator concept to leverage and you should stop here. If you are on Laravel with laravel-doctrine/orm, or on a legacy Symfony app, the practical path is to read the bundle as a reference and port the mechanism, not to composer require it.

Minimal steps (legacy Symfony, if you must install as-shipped):

  1. composer require levelab/doctrine-discriminator-bundle

  2. Register the bundle in app/AppKernel.php:

    $bundles[] = new Levelab\DoctrineDiscriminatorBundle\LevelabDoctrineDiscriminatorBundle();
    
  3. Look first at two files: the Doctrine event subscriber (the loadClassMetadata listener that synthesises the map) and the DependencyInjection extension (the compiler pass that wires it). Everything else is Symfony boilerplate you will delete during a port.

  4. Annotate each subclass with the bundle's discriminator-value annotation and remove the discriminatorMap array from the root entity:

    /** @DiscriminatorValue("card") */
    class CardPayment extends Payment {}
    
    /** @DiscriminatorValue("bank_transfer") */
    class BankTransferPayment extends Payment {}
    

First use case. A Payment (or Notification, Shipment, InvoiceType) hierarchy where each subtype lives in its own module/package and you want adding a subtype to touch exactly one new file. If you only have three subclasses, keep the explicit discriminatorMap — it is greppable, static-analysis-friendly, and has zero dependency risk.

If you are on Laravel, the first thing you build is the attribute + subscriber, not the bundle:

#[Attribute(Attribute::TARGET_CLASS)]
final class DiscriminatorValue
{
    public function __construct(public readonly string $value) {}
}
final class DiscriminatorMapSubscriber implements EventSubscriber
{
    public function __construct(private array $subclasses) {}

    public function getSubscribedEvents(): array
    {
        return [Events::loadClassMetadata];
    }

    public function loadClassMetadata(LoadClassMetadataEventArgs $event): void
    {
        $meta = $event->getClassMetadata();
        if ($meta->rootEntityName !== $meta->name) {
            return; // only touch the root
        }
        // discover #[DiscriminatorValue] on known subclasses, populate:
        // $meta->discriminatorMap and $meta->subClasses
    }
}

Register it against the EntityManager's event manager inside a service provider.

Implementation Patterns

1. Decentralized map via loadClassMetadata. The core pattern is a single Doctrine subscriber that runs on Events::loadClassMetadata for a root entity and populates ClassMetadata::$discriminatorMap and $subClasses. This is the mechanism worth copying, because it is genuinely Doctrine-idiomatic and works with both STI and CTI. Everything Symfony-specific (the bundle class, the DI extension) is disposable.

2. Per-package registration (the actual reason to do this). The value is realized when a plugin/composer package ships its own PricingRule subclass and registers it without patching core:

// In the package's own service provider
$subclasses[] = PartnerPricingRule::class; // or discovered via attribute scan

That is the workflow to design around: composer require vendor/partner-module → module registers its discriminator value → zero edits to the host app's base entity.

3. Static list, not runtime scanning. Discovery-by-scanning is expensive and fragile (autoload ordering, cache warmth). Prefer a config-driven list of subclasses keyed by discriminator, populated at container build time and cached. Scan-at-request-time is the pattern's main failure mode.

4. Build-time assertion that the resolved map is what you expect. This is the one non-negotiable pattern. Decentralization removes the single readable source of truth; replace it with a test/command:

$meta = $em->getClassMetadata(Payment::class);
self::assertSame([
    'card' => CardPayment::class,
    'bank_transfer' => BankTransferPayment::class,
], $meta->discriminatorMap);

Run this in CI so a missing or duplicated discriminator value fails the build, not a hydration path in production.

5. A debug:discriminator-map artisan command. Since no one edits a central map anymore, give the team one place to read it. Enumerate resolved maps per root entity, print discriminator values and classes, and diff against the expected set on deploy.

6. Migrations stay explicit. The bundle only affects metadata; your SQL schema needs ALTER TABLE payment ADD COLUMN work for new subtype-specific columns. Keep migrations owning the discriminator column and any added nullable columns — do not let a new subclass require a schema change at the moment it is registered.

7. Cache warming. Warm metadata (or your ported discovery cache) in a post-deploy step keyed by a deploy-stamped build ID. In Laravel, artisan doctrine:clear-metadata-cache (or your equivalent) alongside config:cache.

Gotchas and Tips

  • Metadata caching silently breaks discovery. After adding a subclass, ClassNotFound or hydration errors can persist until you clear the Doctrine metadata cache (cache:clear, doctrine:cache:clear-metadata, or the Laravel equivalent). This is the number-one production-debugging pitfall: it works locally (cache cold) and fails in staging (cache warm).
  • Every class in discriminatorMap must be autoloadable. Doctrine resolves each class eagerly during metadata building. A subclass from a disabled/removed package leaves a dangling reference that breaks the entire root entity, not just that subtype. Gate registration on the package actually being installed.
  • The root entity must stay abstract and its discriminator column stable. Doctrine has no supported API to rename or restructure a discriminator column after data exists. Treat it as a schema constant.
  • discriminatorMap cannot be appended after loadClassMetadata returns. The moment passes; do all synthesis inside the listener. Modifying it later throws or is silently ignored depending on Doctrine version.
  • PHP 8 / Doctrine 3 will fatal on this bundle. 2016-era signatures (no return types, annotation reader, Symfony\Component\HttpKernel\Bundle\Bundle coupling) emit deprecations and likely fatals. Do not attempt composer require --ignore-platform-reqs; port instead.
  • Use PHP 8 attributes, not doctrine/annotations. The annotation reader is deprecated and Doctrine 3 drops it. A #[DiscriminatorValue('card')] attribute plus a small subscriber removes both the annotations dependency and the Symfony DI coupling in one step.
  • Values are the map values; the annotation value is the key. The discriminator value string on the subclass becomes the array key; the class becomes the value. Off-by-one key/class confusion here produces "Unknown discriminator value" exceptions only for one subtype.
  • No tests exist upstream. Treat the repository as unverified source material. Write a fixture hierarchy that asserts the resolved map matches an equivalent explicit map, plus a test that a subclass added from a fresh package is picked up without editing the root.
  • Discoverability is the trade you are making. You trade one greppable array for "the map is everywhere." The mitigation is the CI assertion and debug command above; without them, debugging a wrong discriminator in a large hierarchy is materially worse than the baseline.
  • Extension point to prefer: a DiscriminatorRegistry service injected into the subscriber (or a SubclassesCompilerPass/config key in Laravel). Keep the subscriber a pure consumer of the registry so tests can swap a fixed list in without booting Doctrine.
  • Do not fork the bundle to modernize it. Forking a 12-star, zero-dependent, 2016 bundle costs more than a 150–250 line first-party reimplementation and leaves the Symfony kernel baggage in place as a liability.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
3mdikram/worldmoneywords
snowsoft/config
itmosoft/laravel-subscriptions
larva/laravel-ai-qwen
diephp/sequences
users-au/socialite-provider
twidpay/twid-logger
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment
uni-method/symfony-lib