Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Symfony2 Doctrine2 Many-To-Many relation with two Owning Sides and Doctrine cmd line tools

In my Symdony2 project I've two related entities: Service and ServiceGroup. This should be many-to-many relationship, because each group can have many services, and each service can belongs to many groups. Moreover, I need a user interface to manage services and groups. So, when editing a Service, user should be able to choose to which groups it belongs. Analogously, when editing a ServiceGroup user should be able to choose which services belongs to this group. I've already achieved this by setting up a Many-To-Many relation in my Doctrine entites. Everything is working like a charm, including user interface build on custom form types in Symfony2 (I've used "entity" form field type to allow user to select services in ServiceGroup editor and groups in Service editor). The only problem I've is that I cannot use Doctrine command line to update database schema anymore.

Here is part of my Service entity source code:

class Service
{
    /**
     * @var ArrayCollection $groups
     * @ORM\ManyToMany(targetEntity="ServiceGroup")
     * @ORM\JoinTable(
     *      name="service_servicegroup",
     *      joinColumns={@ORM\JoinColumn(name="service_id", referencedColumnName="id")},
     *      inverseJoinColumns={@ORM\JoinColumn(name="servicegroup_id", referencedColumnName="id")}
     * )
     */
    private $groups;
}

And here is part of my ServiceGroup entity source code:

class ServiceGroup
{
    /**
     * @var ArrayCollection $services
     * @ORM\ManyToMany(targetEntity="Service")
     * @ORM\JoinTable(
     *      name="service_servicegroup",
     *      joinColumns={@ORM\JoinColumn(name="servicegroup_id", referencedColumnName="id")},
     *      inverseJoinColumns={@ORM\JoinColumn(name="service_id", referencedColumnName="id")}
     * )
     */
    private $services;
}

I using JoinTable in both cases, because this is the only way I found working when it comes to save relations in user interface editors, which looks like this:

Service editor:

Service editor

Name: [ Service 1 ]

Groups to which this service belongs:

[x] Group A

[ ] Group B

[ ] Group C

[ SAVE ]

And ServiceGroup editor:

Group editor

Name: [ Group A ]

Services belongs to this group:

[x] Service 1

[ ] Service 2

[ ] Service 3

[ SAVE ]

With this Many-To-Many configuration I'm able to use this editors (forms) without a problem, when using Many-To-Many without JoinTable annotation, I'm able to use only one form completely, and the second one is not saving changes in "Groups to which this service belongs" or "Services belongs to this group" option (depends on in which direction I'm setting mappedBy and inversedBy parameters in Many-To-Many annotation statement).

The problem I have is connected with doctrine schema generation mechanism, when trying to update schema using Symfony2 command:

php app/console doctrine:schema:update --dump-sql

I'm getting this exception:

[Doctrine\DBAL\Schema\SchemaException]                      
The table with name 'service_servicegroup' already exists.

It looks like Doctrine trying to create 'service_servicegroup' table for each JoinTable statement. So, it's working on current schema, which I've build in database using the same command, but step-by-step, first when no Many-To-Many relation defined and next with only one Many-To-Many relation definition (for Service entity). When I've added Many-To-Many relation to second entity (ServiceGroup), my application seams to be working without a problem from user point of view, but I'm not able to use 'doctrine:schema:update' command anymore.

I've no idea what is wrong with my code, maybe this relation should be implemented different way, or maybe it's a Doctrine bug/limitation. Any help or suggestion would be appreciated.

UPDATE:

I've noticed that what I need is to configure ManyToMany relation to have two owning sides. Default is having one owning side and one inverse side. Doctrine documentation tells that you can have two owning sides in ManyToMany relation, but doesn't explain it a lot. Anyone can give an example?

WORKAROUND:

I've found a workaround solutions, that maybe isn't ideal but it's working for me. Since there is no way to have two owning sides in many-to-many relation, I've changed Doctrine annotation for my entites. Service entity is now the owning side:

class Service
{
    /**
     * @var ArrayCollection $groups
     * @ORM\ManyToMany(targetEntity="ServiceGroup", inversedBy="services", cascade={"all"})
     * @ORM\JoinTable(
     *      name="service_to_group_assoc",
     *      joinColumns={@ORM\JoinColumn(name="service_id", referencedColumnName="id")},
     *      inverseJoinColumns={@ORM\JoinColumn(name="group_id", referencedColumnName="id")}
     * )
     */
    private $groups;
}

And ServiceGroup entity is inverse side:

class ServiceGroup
{
    /**
     * @var ArrayCollection $services
     * @ORM\ManyToMany(targetEntity="Service", mappedBy="groups", cascade={"all"})
     */
    private $services;
}

Unfortunately, with this configuration, relation is updated only when updating Service entity. When I change $services in ServiceGroup object and persist it, the relation will be unchanged. So, I've change my Controller class, and with a help of small workaround solution, I've achieved expected result. This is a part of my Controller code, which is responsible for updating ServiceGroup entity (with a use of custom form type):

// before update - get copy of currently related services:
$services = clone $group->getServices();

// ...

// when $form->isValid() etc. updating the ServiceGroup entity:

// add selected services to group
foreach($group->getServices() as $service)
{
    $service->addServiceGroup($group);
    $services->removeElement($service);
}

// remove unselected services from group
foreach($services as $service)
{
    $service->removeServiceGroup($group);
}

This are implementations of addServiceGroup and removeServiceGroup methods of Service entity class:

/**
 * Add group
 *
 * @param ServiceGroup $groups
 */
public function addServiceGroup(ServiceGroup $groups)
{
    if(!in_array($groups, $this->groups->toArray()))
    {
        $this->groups[] = $groups;
    }
}

/**
 * Remove group
 *
 * @param ServiceGroup $groups
 */
public function removeServiceGroup(ServiceGroup $groups)
{
    $key = $this->groups->indexOf($groups);
    
    if($key!==FALSE) 
    {
        $this->groups->remove($key);
    }
}

Now I have working many-to-many relation with owning (Service) and inverse (ServiceGroup) side, and forms that updated both entity and relation when saving (default form for Service entity is enough, but for ServiceGroup I've provided above mentioned modifications). The Symfony/Doctrine console tools are working like a charm. This probably can be solved in better (simpler?) way, but for me this is enough for now.

like image 387
Darrarski Avatar asked Mar 06 '12 11:03

Darrarski


1 Answers

The default behavior of a many-to-many association will have an owning side and an inverse side. If you are dealing with the owning side of the association, then the association will be handled by the ORM. However if you are dealing with the inverse side, it is up to you to explicitly handle this.

In your case, you have mappedBy="groups" in the $services property of the ServiceGroup class. What this means is that, this association between the Service and the ServiceGroup is maintained by the $groups property of the Service entity. So, Service becomes the owning side of this association.

Now, say you are creating or updating a Service entity. Here when you add ServiceGroup entities to this Service entity, and persist the Service entity, all the entities and the relevant associations are automatically built by the ORM.

But, if you are creating or updating a ServiceGroup entity, when you add Service entities to it, and persist the ServiceGroup entity, the association is not built by the ORM. Here as a workaround, you have to explicitly add the ServiceGroup entity to the Service entity.

// in ServiceGroup.php

public function addService(Service $service)
{
    $service->addServiceGroup($this);
    $this->services->add($service);
}

// in Service.php
public function addServiceGroup(ServiceGroup $group)
{
    if (!$this->serviceGroups->contains($group)) {
        $this->serviceGroups->add($group);
    }
}

Things to keep in mind

  1. Make sure that you initialize your $services and $serviceGroups to doctrine collections at the time of entity creation i.e. in the constructors.
  2. You need to have 'by_reference' => false in your services field in the FormType for the ServiceGroup.

Reference: http://symfony.com/doc/2.8/cookbook/form/form_collections.html

like image 72
Sahan Jayawardana Avatar answered Oct 26 '22 02:10

Sahan Jayawardana