1
votes

I'm working in my first distributable bundle and I have some questions about how to reuse the "exists user entity" in my bundle. The aim of my bundle is to relate the ticket entity with the user entity.

I'm trying to relate entities between different bundles when the 'referencedColumnName' of the TargetEntity is different to 'id'.

I'm following the Resolve Target Entity Docs and works fine to me Only when the primary key of the "targetEntity" is named exactly as "id".

Example:

// file: vendor\Kdrmklabs\Bundle\TicketBundle\Entity\Ticket.php

class Ticket {

    /**
     * @ORM\ManyToOne(targetEntity="\Kdrmklabs\Bundle\TicketBundle\Model\UserInterface")
     */
    private $user;
}
// file: vendor\Kdrmklabs\Bundle\TicketBundle\Model\UserInterface.php;

interface UserInterface {

    public function getId();

}
# file: app/config/config.yml

doctrine:
    orm:
        auto_mapping: true
        resolve_target_entities:
            Kdrmklabs\Bundle\TicketBundle\Model\UserInterface: AppBundle\Entity\User

If the primary key of the entity AppBundle/Entity/User names "id", the relationship between my bundle and this entity works fine.

// file: src/AppBundle/Entity/User.php

class User implements \Kdrmklabs\Bundle\TicketBundle\Model\UserInterface
{
    /**
     * @var integer
     *
     * @ORM\Column(name="id", type="integer")
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    private $id;
}

but otherwise, an exception occurs:

// file: src/AppBundle/Entity/User.php

class User implements \Kdrmklabs\Bundle\TicketBundle\Model\UserInterface
{
    /**
     * @var integer
     *
     * @ORM\Column(name="id_customer", type="integer")
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    private $id;
}
Column name 'id' referenced for relation from \Kdrmklabs\Bundle\TicketBundle\Entity\Ticket towards AppBundle\Entity\User does not exists.

I know if you specify the value of the foreign key in the annotation @ORM\JoinColumn(referencedColumnName="id_of_the_external_user_entity") this will works great, But from my bundle I have no way of knowing the name of the foreign key with my actual configuration.

  1. Is there any way to create a relationship between two entities regardless of the name of these primary keys ?

  2. May be I can request to the user specify in the app/config/config.yml the name of the primary key of him "user entity" and somehow specify this value to doctrine to create the right relationship between the entities of my bundle and entities of the user that installed my bundle. But, How can I do it?

Thank you. regards

Thanks a lot in advance!!

More details:

Repository of my distributable bundle is: https://github.com/KdrMkLabs/TicketBundle

1

1 Answers

1
votes

After several hours analyzing this problem , I have concluded that a correct way to create relationships between entities that are unknown to each other, is by using the doctrine eventlisteners. I'll explain how to do it below.


You can solve your conflicts with a Doctrine event listener to map your entities using PHP instead of annotations.

The EntityManager and UnitOfWork trigger a bunch of events during the life-time of their registered entities.

Here you have a list of Lifecycle Events that you can use

In this case we'll listen the event loadClassMetadata to make the correct doctrine mapping and create a good relationship between the user entity and your bundle entity.

1. Create a Doctrine EventListener in your bundle

// file: Kdrmklabs\Bundle\TicketBundle\EventListener\LoadMetadata.php

namespace Kdrmklabs\Bundle\TicketBundle\EventListener;
use Doctrine\ORM\Event\LoadClassMetadataEventArgs;

class LoadMetadata {
    protected $userRepository;
    protected $primary_key;

    public function __construct($userRepository, $primary_key)
    {
        $this->userRepository = $userRepository;
        $this->primary_key = $primary_key;
    }

    public function getSubscribedEvents()
    {
        return ['loadClassMetadata',];
    }

    public function loadClassMetadata(LoadClassMetadataEventArgs $eventArgs)
    {
        $classMetadata = $eventArgs->getClassMetadata();
        $class_name = $classMetadata->getName();

        if($class_name == "Kdrmklabs\Bundle\TicketBundle\Entity\Ticket") {

            // The following is to map ORM with PHP
            $mapping = array(
                'targetEntity' => $this->userRepository,
                'fieldName' => 'user',
                'joinColumns' => array(
                    array(
                        'name' => 'user_id',
                        'referencedColumnName' => $this->primary_key
                    )
                )
            );

            $classMetadata->mapManyToOne($mapping);
        }
    }
}

Note that the value of 'fieldName' is the one that corresponds to the name of the class attribute that refers to the $user in your Ticket entity. The above mapping array is similar to do doctrine ORM mapping with annotation, as you can see below:

class Ticket {

    /**
     * @ORM\ManyToOne(targetEntity="\Kdrmklabs\Bundle\TicketBundle\Model\UserInterface")
     * @ORM\JoinColumn(name="user_id", referencedColumnName="?id_from_external_entity?")
     */
    private $user;
}

Here is more information about mapping in doctrine 2 with PHP

2. Register the eventListener as a service

# file: Kdrmklabs\Bundle\TicketBundle\Resources\config\services.yml

services:
    kdrmklabs_ticket.listener:
        class: Kdrmklabs\Bundle\TicketBundle\EventListener\LoadMetadata
        arguments:
            - %kdrmklabs_ticket.model.user.class%
            - %kdrmklabs_ticket.model.user.primary_key%
        tags:
            - { name: doctrine.event_listener, event: loadClassMetadata }

Do not forget inject the new parameter kdrmklabs_ticket.model.user.primary_key from your DependencyInjection, for example:

// file: Kdrmklabs\Bundle\TicketBundle\DependencyInjection\Configuration.php
class Configuration implements ConfigurationInterface
{
    public function getConfigTreeBuilder()
    {
        $treeBuilder = new TreeBuilder();
        $rootNode = $treeBuilder->root('kdrmklabs_ticket');

        $rootNode->children()
            ->scalarNode('user_class')->isRequired()->cannotBeEmpty()->end()
            ->scalarNode('user_primay_key')->isRequired()->cannotBeEmpty()->end()
        ->end();

        return $treeBuilder;
    }
}
// file: Kdrmklabs\Bundle\TicketBundle\DependencyInjection\KdrmklabsTicketExtension.php

class KdrmklabsTicketExtension extends Extension
{
    public function load(array $configs, ContainerBuilder $container)
    {
        $configuration = new Configuration();
        $config = $this->processConfiguration($configuration, $configs);

        $loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
        $loader->load('services.yml');

        $container->setParameter('kdrmklabs_ticket.model.user.class', $config['user_class']);
        $container->setParameter('kdrmklabs_ticket.model.user.primary_key', $config['user_primay_key']);
    }
}

3. Finally, inject the parameter 'user_primay_key' from the project config.yml file.

# file: app\config\config.yml

kdrmklabs_ticket:
    user_class: AppBundle\Entity\User
    user_primay_key: id

That is all. Whenever you update the doctrine scheme, Doctrine automatically create the relationship between your Ticket entity and the external User entity that is specified in the project config.yml (app/config/config.yml)