Override association mapping of a trait property in Doctrine 2.6
Asked Answered
C

3

10

Prerequisites:

  • PHP 7.1.8
  • Symfony 3.3.9
  • Doctrine 2.6.x-dev

I wonder if it's possible to override an inversedBy attribute of a property association mapping that's taken from a trait.

An interface that I use as a concrete user entity placeholder:

ReusableBundle\ModelEntrantInterface.php

interface EntrantInterface
{
    public function getEmail();

    public function getFirstName();

    public function getLastName();
}
  1. The following architecture works just fine (need to create User entity that implements EntrantInterface and all other entities that are derived from these abstract classes in AppBundle):

ReusableBundle\Entity\Entry.php

/**
 * @ORM\MappedSuperclass
 */
abstract class Entry
{
    /**
     * @var EntrantInterface
     *
     * @ORM\ManyToOne(targetEntity="ReusableBundle\Model\EntrantInterface", inversedBy="entries")
     * @ORM\JoinColumn(name="user_id")
     */
    protected $user;

    // getters/setters...
}

ReusableBundle\Entity\Timestamp.php

/**
 * @ORM\MappedSuperclass
 */
abstract class Timestamp
{
    /**
     * @var EntrantInterface
     *
     * @ORM\ManyToOne(targetEntity="ReusableBundle\Model\EntrantInterface", inversedBy="timestamps")
     * @ORM\JoinColumn(name="user_id")
     */
    protected $user;

    // getters/setters...
}

And couple more entities with similar structure that utilize EntranInterface

  1. And this is what I want to achieve - UserAwareTrait to be reusable across several entities:

ReusableBundle\Entity\Traits\UserAwareTrait.php

trait UserAwareTrait
{
    /**
     * @var EntrantInterface
     *
     * @ORM\ManyToOne(targetEntity="ReusableBundle\Model\EntrantInterface")
     * @ORM\JoinColumn(name="user_id")
     */
    protected $user;

    // getter/setter...
}

In Doctrine 2.6 if I would use super class and wanted to override its property I'd do this:

/**
 * @ORM\MappedSuperclass
 * @ORM\AssociationOverrides({
 *     @ORM\AssociationOverride({name="property", inversedBy="entities"})
 * })
 */
abstract class Entity extends SuperEntity
{
    // code...
}

But if I want that Entity to use UserAwareTrait and override association mapping of a property...

/**
 * @ORM\MappedSuperclass
 * @ORM\AssociationOverrides({
 *     @ORM\AssociationOverride({name="user", inversedBy="entries"})
 * })
 */
abstract class Entry
{
    use UserAwareTrait;
    // code...
}

... and run php bin/console doctrine:schema:validate I see this error in the console:

[Doctrine\ORM\Mapping\MappingException]
Invalid field override named 'user' for class 'ReusableBundle\Entity\Entry'.

Is there a workaround that I could follow to achieve the desired result?

  1. Use trait to store shared properties

  2. Override assotiation mapping or (possibly) attributes mapping in the class that uses that trait

Chief answered 21/9, 2017 at 23:54 Comment(2)
Well I have a similar implementation but in my case the class that uses the trait is not a MappedSuperclass. Did you tried this with deleting the @ORM\MappedSuperclass line over your abstract class Entry ? And more important: You try to override the a relation without specifing it (in this case ManyToOne. Try: @ORM\AssociationOverrides({ @ORM\AssociationOverride(name="user", manyToMany=@ORM\ManyToOne(targetEntity="ReusableBundle\Model\EntrantInterface", inversedBy="entities")) }).Epi
Also in your last code block it is inversedBy="entries" which is the same as the original and in your second last it is inversedBy="entities"...Epi
B
2

TL;DR You should change the access modificator from protected to private. Don't forget that you will not be able to directly manipulate the private property in a subclass and will need a getter.

The exception appears due to the bug (I believe, or a quirk of the behavior) in the AnnotationDriver.

foreach ($class->getProperties() as $property) {
    if ($metadata->isMappedSuperclass && ! $property->isPrivate()
        ||
        ...) {
        continue;
    }

It skips all non-private properties for MappedSuperclass letting them to compose metadata on the subclass parsing. But when it comes to overriding the driver tries to do it at a MappedSuperclass level, it doesn't remember that the property was skipped, fails to find it in the metadata and raise an exception.

I made a detailed explanation at the issue. You can find there also the link to the unit tests that highlight the case.

Balsamic answered 29/9, 2017 at 2:10 Comment(0)
P
0

You'll have to try this in your own code to see, but it could be possible.

As an experiment, I overridden a trait in a class, then checked for the trait using class_uses() http://php.net/manual/en/function.class-uses.php

<?php

trait CanWhatever
{
    public function doStuff()
    {
        return 'result!';
    }
}

class X
{
    use CanWhatever;

    public function doStuff()
    {
        return 'overridden!';
    }
}

$x = new X();
echo $x->doStuff();
echo "\n\$x has ";
echo (class_uses($x, 'CanWhatever')) ? 'the trait' : 'no trait';

This outputs:

overridden! 
$x has the trait

Which you can see here https://3v4l.org/Vin2H

However, Doctrine Annotations may still pick up the DocBlock from the trait proper rather than the overridden method, which is why I can't give you a definitive answer. You just need to try it and see!

Paiz answered 25/9, 2017 at 15:2 Comment(1)
Thanks for the answer, this is not what I need. I don’t want to override a method (I would just define it in each class then and annotate individually, and this exactly what I wish to avoid to do). And there’s nothing to override in the body of methods, those just getter and setter.Chief
P
0

I had a similiar problem and solve it by override the property it self:

use UserAwareTrait;
/**
 * @var EntrantInterface
 * @ORM\ManyToOne(targetEntity="ReusableBundle\Model\EntrantInterface"inversedBy="entries")
 */
protected $user;
Pelargonium answered 29/9, 2017 at 15:2 Comment(2)
Please read carefully, I want to reuse the trait in multiple entities, therefore there’s no chance to override it in trait, inversedBy must have different values for each entityChief
Yes, my answer means to override the trait property in each entity, to set a proper inversed byPelargonium

© 2022 - 2024 — McMap. All rights reserved.