16
votes

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.

4
does it work if you drop - create the schema?jere
Nope. I've tried with empty database, the effect is identical. If I have JoinTable annotation in both entities as in example above, doctrine:schema:update command wont work. 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. Documentation tells that you can have two owning sides in ManyToMany relation, but doesn't explain it a lot. Anyone can give an example?Darrarski
I have never had really good luck with ManyToMany. Always seem to run into some sort of problem. Consider explicitly creating your ServiceServiceGroup entity/table and setting up OneToMany relations with it. Not sure if will need to adjust your forms or not. It also allows adding additional attributes to the join entity.Cerad
You linked to 2.0 documentation. Be sure to be looking at 2.1. I didn't see anything in the docs which would imply that you can have two owning sides. Just the opposite. "For ManyToMany bidirectional relationships EITHER side may be the owning side (the side that defines the @JoinTable and/or does not make use of the mappedBy attribute, thus using a default join table)."Cerad
You're right, I have read somewhere else that in bidirectional relationships BOTH sides may be owning side, which apparently is not true. So, how to force Doctrine to save changed relation when saving inverse side entity? I've configured my relation exactly like in documentation example. Relation table is updated when saving a service (owning side entity), but not when saving a group (inverse side entity). Any ideas?Darrarski

4 Answers

2
votes

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

0
votes

Example of ensuring that the owning side (Service) always gets updated.

class ServiceGroup
{
    public function addService($service)
    {
        if (!$service) return;

        $this->services[] = $service;
        $service->addGroup($this);
    }
}
0
votes

Have you tried coding it with an intermediate entity and many to one associations instead?

While it might not be what you want to have, it might provide insights on how to make it work with many to many.

(And frequently you end up realizing that the intermediate entity deserves to be there because of some attributes of the association.)