6

Is it possible to resolve a target entity over multiple entity managers?

I have a class person (in a re-usable bundle):

/**
 *
 * @ORM\Entity
 * @ORM\Table(name="my_vendor_person")
 */
class Person
{ 
    /**
     * Unique Id
     *
     * @var integer $id
     *
     * @ORM\Column(name="id", type="integer")
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    protected $id;

    /**
     * First Name
     *
     * @var string $name
     *
     * @ORM\Column(name="first_name", type="string", length=32)
     */
    protected $firstName;
    // etc...

And a class user (in my main application):

/**
 * @ORM\Entity
 *
 * @ORM\Table(name="my_financial_user")
 *
 */
class User extends BaseUser
{

    /**
     * @ORM\OneToOne(targetEntity="My\FinancialBundle\Model\PersonInterface")
     * @var PersonInterface
     */
    protected $person;

Basically I want to couple the user to a person from the re-usable bundle.

I had set resolve target entity option in doctrine's configuration which I thought it allows me to do this:

doctrine:
  orm:
      auto_generate_proxy_classes: "%kernel.debug%"
      default_entity_manager: default
      resolve_target_entities:
          My\FinanceBundle\Model\PersonInterface: My\VendorBundle\Entity\Person
      entity_managers:
          default:
              naming_strategy: doctrine.orm.naming_strategy.underscore
              connection: default
              mappings:
                  MyFinanceBundle: ~
          second:
              naming_strategy: doctrine.orm.naming_strategy.underscore
              auto_mapping: false
              connection: second
              mappings:
                  MyVendorBundle: ~
                  MyVendorUserBundle: ~

Also, the user class in the main bundle, extends a base user in the vendor bundle. The user class of course is maintained, in the main-applications db.

With this configuration it gives me an error.

[Doctrine\Common\Persistence\Mapping\MappingException]                                                                                       
The class 'My\VendorBundle\Entity\Person' was not found in the chain configured namespaces My\FinanceBundle\Entity, FOS\UserBundle\Model 

Does anyone know how to solve this?

3
  • 1
    See these related questions: stackoverflow.com/questions/14403863/… and stackoverflow.com/questions/9330018/… Commented Mar 10, 2016 at 15:23
  • 1
    Nope. Entity managers do not talk to each other. You will need to find a way to include all the needed entities in a single entity manager. Commented Mar 10, 2016 at 17:12
  • that is unfortunate. I think this is legitimate user case, hope support will be there in the future.. thanks both for your comment. for now what I will do is dynamically assign the person to the user in the controllers where it is needed ($user = $this->getUser(); $user->person = $person) Commented Mar 11, 2016 at 5:03

1 Answer 1

3

As mentioned in the comment the entity managers don't talk to each other so you have to work around it. One way is to use a doctrine listener and insert a callback that can be invoked by the getPerson method.

AppBundle\Doctrine\InsertPersonListener

use Doctrine\Common\Persistence\Event\LifecycleEventArgs;
use My\FinancialBundle\Entity\Person;
use My\VendorBundle\Entity\User;
use Symfony\Bridge\Doctrine\RegistryInterface;

class InsertPersonListsner
{
    /**
     * @var RegistryInterface
     */
    private $registry;

    /**
     * Insert the registry into the listener rather than the entity manager
     * to avoid a cyclical dependency issue
     *
     * @param RegistryInterface $registry
     */
    public function __construct(RegistryInterface $registry)
    {
        $this->registry = $registry;
    }

    /**
     * On postLoad insert person callback
     *
     * @var LifecycleEventArgs $args
     */
    public function postLoad(LifecycleEventArgs $args)
    {
        $user = $args->getObject();

        // If not a user entity ignore
        if (!$user instanceof User) {
            return;
        }

        $reflectionClass = new \ReflectionClass(User::class);

        $property = $reflectionClass->getProperty('personId');
        $property->setAccessible(true);

        // if personId is not set ignore
        if (null === $personId = $property->getValue($user)) {
            return;
        }

        // get the repository for your person class
        // - changed to your version from the comments
        $repository = $this->registry
            ->getManagerForClass(Person::class)
            ->getRepository('MyVendorBundle:Person');

        // set the value as a callback rather than the entity
        // so it's not called unless necessary
        $property = $reflectionClass->getProperty('personCallback');
        $property->setAccessible(true);
        $property->setValue($user, function() use ($repository, $personId) {
            return $repository->find($personId);
        });
    }
}

My\VendorBundle\Entity\User

use My\FinancialBundle\Entity\Person;

class User
{
    //..

    /**
     * @var Person
     */
    private $person;

    /**
     * @var integer
     */
    private personId;

    /**
     * @var callable
     */
    private $personCallback;

    /**
     * Set person
     *
     * @param Person|null $person
     * @return $this
     */
    public function setPerson(Person $person = null)
    {
        $this->person = $person;

        // if the person is null reset the personId and
        // callback so that it's not called again
        if (null === $person) {
            $this->personId = null;
            $this->personCallback = null;
        } else {
            // set the personId to be stored on the db for the next postLoad event
            $this->personId = $person->getId();
        }

        return null;
    }

    /**
     * Get person
     *
     * @return Person|null
     */
    public function getPerson()
    {
        // if the person has not been set and the callback is callable,
        // call the function and set the result (of $repository->find($personId))
        // to the person property and then return it. The callback is
        // reset to stop unnecessary repository calls in case of a null response 
        if (null === $this->person && is_callable($this->personCallback)) {
            $this->person = call_user_func($this->personCallback);
            $this->personCallback = null;
        }

        return $this->person;
    }
}

AppBundle\Resources\services.yml

app.listener.insert_person:
    class: AppBundle\Doctrine\InsertPersonListener
    arguments:
        - '@doctrine'
    tags:
        - { name: doctrine.event_listener, event: postLoad }

UPDATE

After looking for an example of this in real life I found this. From this I saw that you could change the

$reflectionClass = new \ReflectionClass(User::class)

to

$reflectionClass = $args->getObjectManager()
    ->getClassMetadata(User::class)
    ->reflClass

which, I assume, would mean one less new \ReflectionClass(...) call per load.

Sign up to request clarification or add additional context in comments.

4 Comments

this is really good! not only a practical answer but learned a lot. thanks for posting this. hope this will help others that face the same problem.
I assume in postLoad, $property->getValue($user), should be $property->getValue($entity), as $user is not defined. also $args->getEntity(), could be $args->getObject() as getEntity is deprecated.
My bad. For some reason I decided to write it directly into the text box rather than using an IDE which would have made all of these really obvious. I've also renamed $entity to $user so it's more explicit and added the services.yml config file that I forgot.
ok. thanks! also I had to change: $repository = $this->registry->getRepository(Person::class) to: $em = $this->registry->getManagerForClass(Person::class); $repository = $em->getRepository('MyVendorBundle:Person'); as I would get the same "not found in chain" error otherwise.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.