Skip to content

Referencing Entities with Abstract Classes and Interfaces

Edit this page

In applications where functionality is organized in layers or modules with minimal concrete dependencies, such as monoliths split into multiple modules, it can be challenging to avoid tight coupling between entities.

Doctrine provides a utility called the ResolveTargetEntityListener to solve this issue. It works by intercepting certain calls within Doctrine and rewriting targetEntity parameters in your metadata mapping at runtime. This allows you to reference an interface or abstract class in your mappings and have it resolved to a concrete entity at runtime.

This makes it possible to define relationships between entities without creating hard dependencies. This feature also works with the EntityValueResolver as explained in the main Doctrine article.

7.3

Support for target entity resolution in the EntityValueResolver was introduced Symfony 7.3

Background

Suppose you have an application with two modules: an Invoice module that provides invoicing functionality, and a Customer module that handles customer management. You want to keep these modules decoupled, so that neither is aware of the other's implementation details.

In this case, your Invoice entity has a relationship to the interface InvoiceSubjectInterface. Since interfaces are not valid Doctrine entities, the goal is to use the ResolveTargetEntityListener to replace all references to this interface with a concrete class that implements it.

Set up

This article uses two basic (incomplete) entities to demonstrate how to set up and use the ResolveTargetEntityListener.

A Customer entity:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// src/Entity/Customer.php
namespace App\Entity;

use App\Entity\CustomerInterface as BaseCustomer;
use App\Model\InvoiceSubjectInterface;
use Doctrine\ORM\Mapping as ORM;

#[ORM\Entity]
#[ORM\Table(name: 'customer')]
class Customer extends BaseCustomer implements InvoiceSubjectInterface
{
    // In this example, any methods defined in the InvoiceSubjectInterface
    // are already implemented in the BaseCustomer
}

An Invoice entity:

1
2
3
4
5
6
7
8
9
10
11
12
13
// src/Entity/Invoice.php
namespace App\Entity;

use App\Model\InvoiceSubjectInterface;
use Doctrine\ORM\Mapping as ORM;

#[ORM\Entity]
#[ORM\Table(name: 'invoice')]
class Invoice
{
    #[ORM\ManyToOne(targetEntity: InvoiceSubjectInterface::class)]
    protected InvoiceSubjectInterface $subject;
}

The interface representing the subject used in the invoice:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// src/Model/InvoiceSubjectInterface.php
namespace App\Model;

/**
 * An interface that the invoice Subject object should implement.
 * In most circumstances, only a single object should implement
 * this interface as the ResolveTargetEntityListener can only
 * change the target to a single object.
 */
interface InvoiceSubjectInterface
{
    // List any additional methods that your InvoiceBundle
    // will need to access on the subject so that you can
    // be sure that you have access to those methods.

    public function getName(): string;
}

Now configure the resolve_target_entities option to tell Doctrine how to replace the interface with the concrete class:

1
2
3
4
5
6
7
# config/packages/doctrine.yaml
doctrine:
    # ...
    orm:
        # ...
        resolve_target_entities:
            App\Model\InvoiceSubjectInterface: App\Entity\Customer

Final Thoughts

Using ResolveTargetEntityListener allows you to decouple your modules while still defining relationships between their entities. This makes your codebase more modular and easier to maintain over time.

This work, including the code samples, is licensed under a Creative Commons BY-SA 3.0 license.
TOC
    Version