7

How can I define a Doctrine property in a parent class and override the association in a class which extends the parent class? When using annotation, this was implemented by using AssociationOverride, however, I don't think they are available when using PHP 8 attributes

Why I want to:

I have a class AbstractTenantEntity whose purpose is to restrict access to data to a given Tenant (i.e. account, owner, etc) that owns the data, and any entity which extends this class will have tenant_id inserted into the database when created and all other requests will add the tenant_id to the WHERE clause. Tenant typically does not have collections of the various entities which extend AbstractTenantEntity, but a few do. When using annotations, I handled it by applying Doctrine's AssociationOverride annotation to the extended classes which should have a collection in Tenant, but I don't know how to accomplish this when using PHP 8 attributes?


My attempt described below was unsuccessful as I incorrectly thought that the annotation class would magically work with attributes if modified appropriately, but now I see other code must be able to apply the appropriate logic based on the attributes. As such, I abandoned this approach and just made the properties protected and duplicated them in the concrete class.

My attempt:

Tenant entity

use Doctrine\ORM\Mapping\Entity;
use Doctrine\ORM\Mapping\Column;
use Doctrine\ORM\Mapping\Id;
use Doctrine\ORM\Mapping\OneToMany;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;

#[Entity()]
class Tenant
{
    #[Id, Column(type: "integer")]
    #[GeneratedValue]
    private ?int $id = null;

    #[OneToMany(targetEntity: Asset::class, mappedBy: 'tenant')]
    private array|Collection|ArrayCollection $assets;

    // Other properties and typical getters and setters
}

AbstractTenantEntity entity

use Doctrine\ORM\Mapping\ManyToOne;
use Doctrine\ORM\Mapping\JoinColumn;

abstract class AbstractTenantEntity implements TenantInterface
{
    /**
     * inversedBy performed in child where required
     */
    #[ManyToOne(targetEntity: Tenant::class)]
    #[JoinColumn(nullable: false)]
    protected ?Tenant $tenant = null;

    // Typical getters and setters
}

This is the part which has me stuck. When using annotation, my code would be as follows:

use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity()
 * @ORM\AssociationOverrides({
 *     @ORM\AssociationOverride(name="tenant", inversedBy="assets")
 * })
 */
class Asset extends AbstractTenantEntity
{
    // Various properties and typical getters and setters
}

But AssociationOverrides hasn't been modified to work with attributes, so based on the official class, I created my own class similar to the others which Doctrine has updated:

namespace App\Mapping;

use Attribute;
use Doctrine\Common\Annotations\Annotation\NamedArgumentConstructor;
use Doctrine\ORM\Mapping\Annotation;

/**
 * This annotation is used to override association mapping of property for an entity relationship.
 *
 * @Annotation
 * @NamedArgumentConstructor()
 * @Target("ANNOTATION")
 */
#[Attribute(Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)]
final class AssociationOverride implements Annotation
{
    /**
     * The name of the relationship property whose mapping is being overridden.
     *
     * @var string
     */
    public $name;

    /**
     * The join column that is being mapped to the persistent attribute.
     *
     * @var array<\Doctrine\ORM\Mapping\JoinColumn>
     */
    public $joinColumns;

    /**
     * The join table that maps the relationship.
     *
     * @var \Doctrine\ORM\Mapping\JoinTable
     */
    public $joinTable;

    /**
     * The name of the association-field on the inverse-side.
     *
     * @var string
     */
    public $inversedBy;

    /**
     * The fetching strategy to use for the association.
     *
     * @var string
     * @Enum({"LAZY", "EAGER", "EXTRA_LAZY"})
     */
    public $fetch;

    public function __construct(
        ?string $name = null,
        ?array  $joinColumns = null,
        ?string $joinTable = null,
        ?string $inversedBy = null,
        ?string $fetch = null
    ) {
        $this->name    = $name;
        $this->joinColumns = $joinColumns;
        $this->joinTable = $joinTable;
        $this->inversedBy = $inversedBy;
        $this->fetch = $fetch;
        //$this->debug('__construct',);
    }

    private function debug(string $message, string $file='test.json', ?int $options = null)
    {
        $content = file_exists($file)?json_decode(file_get_contents($file), true):[];
        $content[] = ['message'=>$message, 'object_vars'=>get_object_vars($this), 'debug_backtrace'=>debug_backtrace($options)];
        file_put_contents($file, json_encode($content, JSON_PRETTY_PRINT));
    }
}

When validating the mapping, Doctrine complains that target-entity does not contain the required inversedBy. I've spent some time going through the Doctrine source code but have not made much progress.

Does my current approach have merit and if so please fill in the gaps. If not, however, how would you recommend meeting this need?

yivi
  • 42,438
  • 18
  • 116
  • 138
user1032531
  • 24,767
  • 68
  • 217
  • 387

2 Answers2

2

It has been resolved by this pr: https://github.com/doctrine/orm/pull/9241

ps: PHP 8.1 is required

#[AttributeOverrides([
new AttributeOverride(
    name: "id",
    column: new Column(name: "guest_id", type: "integer", length: 140)
),
new AttributeOverride(
    name: "name",
    column: new Column(name: "guest_name", nullable: false, unique: true, length: 240)
)]
)]
Ben Yan
  • 36
  • 2
0

Override Field Association Mappings In Subclasses

Sometimes there is a need to persist entities but override all or part of the mapping metadata. Sometimes also the mapping to override comes from entities using traits where the traits have mapping metadata. This tutorial explains how to override mapping metadata, i.e. attributes and associations metadata in particular. The example here shows the overriding of a class that uses a trait but is similar when extending a base class as shown at the end of this tutorial.

Suppose we have a class ExampleEntityWithOverride. This class uses trait ExampleTrait:

<?php
/**
 * @Entity
 *
 * @AttributeOverrides({
 *      @AttributeOverride(name="foo",
 *          column=@Column(
 *              name     = "foo_overridden",
 *              type     = "integer",
 *              length   = 140,
 *              nullable = false,
 *              unique   = false
 *          )
 *      )
 * })
 *
 * @AssociationOverrides({
 *      @AssociationOverride(name="bar",
 *          joinColumns=@JoinColumn(
 *              name="example_entity_overridden_bar_id", referencedColumnName="id"
 *          )
 *      )
 * })
 */
class ExampleEntityWithOverride
{
    use ExampleTrait;
}

/**
 * @Entity
 */
class Bar
{
    /** @Id @Column(type="string") */
    private $id;
}

The docblock is showing metadata override of the attribute and association type. It basically changes the names of the columns mapped for a property foo and for the association bar which relates to Bar class shown above. Here is the trait which has mapping metadata that is overridden by the annotation above:

<?php
/**
 * Trait class
 */
trait ExampleTrait
{
    /** @Id @Column(type="string") */
    private $id;

    /**
     * @Column(name="trait_foo", type="integer", length=100, nullable=true, unique=true)
     */
    protected $foo;

    /**
     * @OneToOne(targetEntity="Bar", cascade={"persist", "merge"})
     * @JoinColumn(name="example_trait_bar_id", referencedColumnName="id")
     */
    protected $bar;
}

The case for just extending a class would be just the same but:

<?php
class ExampleEntityWithOverride extends BaseEntityWithSomeMapping
{
    // ...
}

Overriding is also supported via XML and YAML (examples).

Dharman
  • 30,962
  • 25
  • 85
  • 135
AaYan Yasin
  • 566
  • 3
  • 15
  • 1
    Thanks AaYan, Good tutorial when using annotation but I am looking how to do similarly when using https://www.php.net/manual/en/language.attributes.php – user1032531 Aug 15 '21 at 16:19