joschi127/doctrine-entity-override-bundle
A Symfony bundle that lets you override Doctrine entities and their mappings from third-party bundles without touching vendor code. Extend or replace entity classes, customize mappings, and keep your changes clean and upgrade-safe—no more forking vendor packages.
1. Install and register the bundle
composer require joschi127/doctrine-entity-override-bundle
The bundle predates Symfony Flex recipes for most setups, so add it manually to config/bundles.php:
Joschi127\DoctrineEntityOverrideBundle\DoctrineEntityOverrideBundle::class => ['all' => true],
2. Read the metadata listener before anything else
Open vendor/joschi127/doctrine-entity-override-bundle/ and read the class that subscribes to Doctrine's loadClassMetadata event. That single class is the whole bundle: it turns the overridden parent entity into a MappedSuperclass and rewrites every targetEntity reference pointing at the parent so it points at your subclass instead. Understanding that one file answers 90% of the "why is my mapping wrong" questions later.
3. Declare the override
# config/packages/doctrine_entity_override.yaml
doctrine_entity_override:
entities:
'Vendor\ShopBundle\Entity\Product': 'App\Entity\Product'
Confirm the exact key name (entities vs. a list of pairs) against the README for the tag you install — the bundle was last released in 2019 and the config shape is the thing most likely to have drifted from blog posts you find.
4. Write the subclass
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
use Vendor\ShopBundle\Entity\Product as BaseProduct;
/**
* @ORM\Entity(repositoryClass=ProductRepository::class)
* @ORM\Table(name="vendor_product")
*/
class Product extends BaseProduct
{
/** @ORM\Column(type="string", length=64, nullable=true) */
private ?string $internalSku = null;
public function getInternalSku(): ?string { return $this->internalSku; }
public function setInternalSku(?string $sku): self { $this->internalSku = $sku; return $this; }
}
5. Your first sanity check: diff the schema
bin/console cache:clear
bin/console doctrine:schema:update --dump-sql
The correct outcome is: one new column on the existing vendor table, and no second table created. If you instead see a brand-new table, the override didn't apply — usually stale metadata cache or a wrong FQCN in the map.
First real use case: a third-party bundle ships Product with no sku field, you need sku, and you don't want to fork the bundle. This is exactly the scenario the package exists for — additive fields on a vendor entity, reusing the same table and the same rows.
Additive field extension (the 80% case). Subclass, add nullable columns, inherit everything else. Keep the parent's table name explicitly via @ORM\Table(name="...") so you don't depend on inheritance rules to land on the right table. Do not redeclare @ORM\Id — the parent's ID mapping is inherited.
Rewriting relations transparently. The bundle's real value is that you don't have to touch the vendor's other entities. If Vendor\Order has @ORM\ManyToOne(targetEntity="Product"), the listener rewrites it to App\Entity\Product. Verify with:
bin/console doctrine:mapping:info
bin/console doctrine:mapping:debug Vendor\\ShopBundle\\Entity\\Product # isMappedSuperclass should be true
bin/console doctrine:mapping:debug App\\Entity\\Product # fields list should include vendor fields
Overriding behaviour, not just data. Override getters to change formatting, add domain methods, or swap the repository:
class Product extends BaseProduct
{
public function getDisplayName(): string
{
return $this->internalSku ?? parent::getDisplayName();
}
}
Extending queries through the repository. Set repositoryClass on the subclass and extend the vendor repository (if it has one). Existing calls to $em->getRepository(Product::class) inside the vendor bundle now resolve to your repository once the metadata rewrite lands — a clean hook for adding scoped finders.
Migration workflow. Treat the first migration after adding an override as a review checkpoint. doctrine:migrations:diff should generate ALTER TABLE ... ADD COLUMN, never CREATE TABLE. If it creates a table, stop and fix the mapping before committing.
Test and fixture symmetry. Register the override in config/packages/test/doctrine_entity_override.yaml too. If your test kernel boots a different bundle set, the schema built for tests will silently differ from dev/prod, and you'll chase phantom failures. Rebuild the test schema in CI from scratch rather than reusing a cached one.
Layering. Type-hint against the original class in your application code, not the subclass. Because the override is a genuine inheritance relationship, instanceof Vendor\Entity\Product stays true and your services don't need to know the override exists.
Keeping it discoverable. Put overrides in a dedicated namespace (App\Entity\Override\) and add a short comment linking to the vendor class. Six months from now, nobody remembers why App\Entity\Product extends a vendor class.
MappedSuperclass, new Vendor\Entity\Product() and $em->persist() on it will fail. Grep the vendor bundle for direct instantiations before you commit.EntityType form fields break. A form configured with 'class' => Vendor\Entity\Product::class will fail because mapped superclasses aren't queryable entities. Switch those to App\Entity\Product::class.bin/console cache:clear and bin/console doctrine:cache:clear-metadata. In dev with auto_mapping this still bites people.protected/public members. A vendor entity with private properties and no setters cannot be extended meaningfully. Check the parent's visibility first — it's a five-minute check that saves a day.final classes and private constructors cannot be overridden at all. Verify before designing around this bundle.@ORM\InheritanceType with a discriminator map, the bundle's rewrite fights with it. Test that specific entity early.get_class() comparisons and serialized payloads.@ORM\Id on the child produces confusing duplicate-column errors.loadClassMetadata API. Vendor it if your release process can't tolerate an abandoned dependency.Doctrine\ORM\Tools\ResolveTargetEntityListener is the first-party mechanism. This bundle is the right tool only when the vendor entity is a concrete class you can't change.loadClassMetadata listener registered at a lower priority rather than forking.How can I help you explore Laravel packages today?