Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Inserting into a ManyToMany

Video not working?

It looks like your browser may not support the H264 codec. If you're using Linux, try a different browser or try installing the gstreamer0.10-ffmpeg gstreamer0.10-plugins-good packages.

Thanks! This saves us from needing to use Flash or encode videos in multiple formats. And that let's us get back to making more videos :). But as always, please feel free to message us.

The big question is: who is the best superhero of all time? Um, I mean, how can we insert things into this join table? How can we join a Genus and a User together?

Doctrine makes this easy... and yet... at the same time... kind of confusing! First, you need to completely forget that a join table exists. Stop thinking about the database! Stop it! Instead, your only job is to get a Genus object, put one or more User objects onto its genusScientists property and then save. Doctrine will handle the rest.

Setting Items on the Collection

Let's see this in action! Open up GenusController. Remember newAction()? This isn't a real page - it's just a route where we can play around and test out some code. And hey, it already creates and saves a Genus. Cool! Let's associate a user with it!

First, find a user with $user = $em->getRepository('AppBundle:User') then findOneBy() with email set to aquanaut1@example.org:

... lines 1 - 13
class GenusController extends Controller
{
... lines 16 - 18
public function newAction()
{
... lines 21 - 38
$user = $em->getRepository('AppBundle:User')
->findOneBy(['email' => 'aquanaut1@example.org']);
... lines 41 - 51
}
... lines 53 - 115
}

That'll work thanks to our handy-dandy fixtures file! We have scientists with emails aquanaut, 1-10@example.org:

... lines 1 - 22
AppBundle\Entity\User:
... lines 24 - 28
user.aquanaut_{1..10}:
email: aquanaut<current()>@example.org
... lines 31 - 37

We've got a User, we've got a Genus... so how can we smash them together? Well, in Genus, the genusScientists property is private. Add a new function so we can put stuff into it: public function: addGenusScientist() with a User argument:

... lines 1 - 14
class Genus
{
... lines 17 - 174
public function addGenusScientist(User $user)
{
... line 177
}
}

Very simply, add that User to the $genusScientists property. Technically, that property is an ArrayCollection object, but we can treat it like an array:

... lines 1 - 14
class Genus
{
... lines 17 - 174
public function addGenusScientist(User $user)
{
$this->genusScientists[] = $user;
}
}

Then back in the controller, call that: $genus->addGenusScientist() and pass it $user:

... lines 1 - 13
class GenusController extends Controller
{
... lines 16 - 18
public function newAction()
{
... lines 21 - 38
$user = $em->getRepository('AppBundle:User')
->findOneBy(['email' => 'aquanaut1@example.org']);
$genus->addGenusScientist($user);
... lines 42 - 51
}
... lines 53 - 115
}

We're done! We don't even need to persist anything new, because we're already persisting the $genus down here.

Try it out! Manually go to /genus/new. Ok, genus Octopus15 created. Next, head to your terminal to query the join table. I'll use:

./bin/console doctrine:query:sql "SELECT * FROM genus_scientist"

Oh yeah! The genus id 11 is now joined - by pure coincidence - to a user who is also id 11. This successfully joined the Octopus15 genus to the aquanaut1@example.org user.

If adding new items to a ManyToMany relationship is confusing... it's because Doctrine does all the work for you: add a User to your Genus, and just save. Don't over-think it!

Avoiding Duplicates

Let's do some experimenting! What if I duplicated the addGenusScientist() line?

... lines 1 - 13
class GenusController extends Controller
{
... lines 16 - 18
public function newAction()
{
... lines 21 - 40
$genus->addGenusScientist($user);
$genus->addGenusScientist($user); // duplicate is ignored!
... lines 43 - 52
}
... lines 54 - 116
}

Could this one new Genus be related to the same User two times? Let's find out!

Refresh the new page again. Alright! I love errors!

Duplicate entry '12-11' for key 'PRIMARY'

So this is saying:

Yo! You can't insert two rows into the genus_scientist table for the same genus and user.

And this is totally by design - it doesn't make sense to relate the same Genus and User multiple times. So that's great... but I would like to avoid this error in case this happens accidentally in the future.

To do that, we need to make our addGenusScientist() method a little bit smarter. Add if $this->genusScientists->contains()... remember, the $genusScientists property is actually an ArrayCollection object, so it has some trendy methods on it, like contains. Then pass $user. If genusScientists already has this User, just return:

... lines 1 - 14
class Genus
{
... lines 17 - 174
public function addGenusScientist(User $user)
{
if ($this->genusScientists->contains($user)) {
return;
}
$this->genusScientists[] = $user;
}
}

Now when we go back and refresh, no problems. The genus_scientist table now holds the original entry we created and this one new entry: no duplicates for us.

Next mission: if I have a Genus, how can I get and print of all of its related Users? AND, what if I have a User, how can I get its related Genuses? This will take us down the magical - but dangerous - road of inverse relationships.

Leave a comment!

34
Login or Register to join the conversation
Roman R. Avatar
Roman R. Avatar Roman R. | posted 5 years ago

Hello! What if I need to my entity have two or more same many to many relations. I have a product entity which have property processes and some poducts may have two or more identical processes. Please help me solve this!

Reply

Hey Roman R.

Can you tell me what's actually your problem?

If you need to create multiple relationships to the same entity is totally possible. This example may help you getting an idea of how to do it: http://docs.doctrine-projec...

Cheers!

Reply
Roman R. Avatar

Product already have many to many relations with processes and problem in that the product entity may have process which may repeats two or more times

Reply

Hey Roman R.

I'm not sure to understand your problem. Could you give me a bit more of context?

Reply
Roman R. Avatar

My entity Product have
/**
* @ORM\ManyToMany(targetEntity="CoreBundle\Entity\PostPrintOperation", inversedBy="products")
* @ORM\JoinTable(name="product_postPrint")
*/
private $postPrint;

but in some products I need have 2 or more identical $postPrint and when I try to save it I have Duplicate entry error

Reply

So, you need to have the same object into the same collection? In other words, Product::$postPrints is a collection of "PostPrintOperation" and in that collection you need to store the same object twice or more?

Reply
Roman R. Avatar

Yes! ))) Exactly what I need)

Reply

Hey Roman R.

I'm not sure why you need to do something like that but as far as I know that's a unique constraint violation. Maybe if you tell me a bit more about your use case I may be able to help you figuring out a workaround

Cheers!

Reply
Roman R. Avatar

Product must have collection of operations and some operations may call as one and two or more times

Reply

In that case you could create a service class that is on charge of deciding how many times an operation should be executed instead of adding the same operation multiple times to the same product

Reply
Roman R. Avatar

Thank you! Can you explain how realize this logic. This class it's a service or doctrine entity? And how in this case point count of operations when adding it to product

Reply

Well, it depends on your business logic. What events have to occur so you would add the same operation to the product?
If the answer requires data that you won't have in a further request, then you will have to store that information as an extra field of your ManyToMany relationship
If you don't know how to do that, here is a nice video that can guide you through: https://knpuniversity.com/s...

Cheers!

1 Reply
Default user avatar
Default user avatar sebastian | posted 5 years ago | edited

there can you help me? im having troubles persisting an entity (Solicitud) it has all of its associated objects already stored on its properties, when im passing the object to the entity manager for persist and flush.

The fully descripted relations should be...

Usuario-> Solicitudes (One to Many)
Solicitud-> Servicios (Many to Many)
Category->Servicios (One to Many)

i did generated the entities from my XML Mapping...

on my "solicitud" XML i have defined the relation as you can see below...and the intermediate table got effectively created when i executed the command:

vendor\bin\doctrine orm:schema-tool:update --force

with two columns and everything ok.

this is on "Solicitud"

<many-to-many field="servicios" target-entity="Servicio">
<cascade>
<cascade-persist/>
</cascade>
<join-table name="solicitud_servicio">
<join-columns>
<join-column name="id_solicitud" referenced-column-name="id"/>
</join-columns>
<inverse-join-columns>
<join-column name="id_servicio" referenced-column-name="id"/>
</inverse-join-columns>
</join-table>
</many-to-many>

and this on Servicio

<doctrine-mapping xmlns="http://doctrine-project.org..." xmlns:xsi="http://www.w3.org/2001/XMLS..." xsi:schemalocation="http://doctrine-project.org... http://doctrine-project.org...">
<entity name="Servicio" table="servicio">
<indexes>
<index name="id_categoria" columns="id_categoria"/>
</indexes>
<id name="id" type="integer" column="id">
<generator strategy="IDENTITY"/>
</id>
<field name="codigo" type="string" column="codigo" length="15" nullable="false"/>
<field name="nombre" type="string" column="nombre" length="70" nullable="false"/>
<field name="detalle" type="string" column="detalle" length="250" nullable="false"/>
<field name="precioPublico" type="integer" column="precio_publico" nullable="false"/>
<field name="precioPrivado" type="integer" column="precio_privado" nullable="false"/>
<field name="medida" type="string" column="medida" length="15" nullable="false"/>
<field name="planificacion" type="integer" column="planificacion" nullable="false"/>
<field name="fabricacion" type="integer" column="fabricacion" nullable="false"/>
<field name="fechaCreacion" type="datetime" column="fecha_creacion" nullable="true"/>
<field name="ultimaModificacion" type="datetime" column="ultima_modificacion" nullable="false"/>
<many-to-one field="idCategoria" target-entity="Categoria">
<join-columns>
<join-column name="id_categoria" referenced-column-name="id"/>
</join-columns>
</many-to-one>
</entity>
</doctrine-mapping>

Is there something i'm missing? maybe another sentence calling to persist on another of the classes involved/related?

i'm trying to persist through the "solicitud" entity in the entity manager sentences... i have them separated on a different layer as DAO, but it makes no problem, when the data arrives to this layer, i tried var_dump , and everything is okay.

in my "SolicitudService" i wrote this function to collect all the related objects needed for persist , i use VO (virtual objects) with functions inside to build the data form object to Json or from Json to Objects and toEntity Pattern

1. First i find the objects that i have on my SolicitudServicioVO The following three sentences after the try, then i create the new object Solicitud.
3. Then i set the objects founded via DAO entities, into the "Solicitud" new object (5 lines after the "//set Solicitud Properties" comment).
4. After that i iterate,($solicitudServicioVO->listadoServicio), through the listadoServicio ,(list of services coming from the front end), and create VO for each one, that are able in the next sentences to build themselves as an entity "or class of the needed type", in this case the ones that i mentioned in the relations definition at the beginning of this question.

5. i send to persist, on this sentence:

$this->solicitudDAO->create($solicitud);

that leads to the DAO that has...

function create($solicitud){
$this->em->persist($solicitud);
$this->em->flush();
return $solicitud;
}

This function couldd be more descriptive on its name by maybe calling it createSolicitud as its purpose its to create a new one, but nevermind...it also has to add a Solicitud to the user.. so i insist , nevermind that by now.

function addSolicitud($solicitudServicioVO){
try{
$usuario = $this->usuarioDAO->find($solicitudServicioVO->getIdUsuario()->id);
$direccion = $this->direccionDAO->find($solicitudServicioVO->getIdDireccion()->id);
$facturacion = $this->datoFacturacionDAO->find($solicitudServicioVO->getIdFacturacion()->id);

$solicitud = new Solicitud();

//set Solicitud Properties
$solicitud->setFechaCreacion(new DateTime());
$solicitud->setEstado("solicitado");
$solicitud->setIdFacturacion($facturacion);
$solicitud->setIdDireccion($direccion);
$solicitud->setIdUsuario($usuario);

foreach($solicitudServicioVO->listadoServicio as $servicioVO)
{
$categoriaServicioVO = $this->categoriaService->getCategoria($servicioVO->getCategoriaId());
$servicio = $servicioVO->toEntity();
$servicio->setIdCategoria($categoriaServicioVO->toEntity());
$solicitud->addServicio($servicio);
}

$resp = $this->solicitudDAO->create($solicitud);
$response = Utils::getInstancia()->httpResponseSuccess('001');
$response['data'] = $resp;
return $response;
}
catch(Exception $e){
$response = $e->getMessage();
}
}

So, i was the most descriptive that i could , im not bieng able to persist this data about the SOlicitud entity...do someone has already a similar problem or any extra ideas...maybe in the XML definition?, it does got trough the entities as annotations and nothing seems to be wrong.

Please help me , this many to many cancer has been taking almost a week without resolution jajaja

Reply

Hey sebastian

Can you tell me if you have any error messages?
As I could see your metadata looks fine, but would be nice to double check it, because, you know is not that easy reading xml from a comment :p

When you have a OneToMany relation and you are about to add a new entity to it, is recommendable that you setup both sides, e.g.


// Usuario
public function addSolicitud(Solicitud $solicitud)
{
    $this->solicitudes[] = $solicitud;
    // the inverse side
    $solicitud->setUser($this);
}

So, I believe you have a problem in somewhere setting up your relationships between objects. Try to split your process into smaller functions so you can debug step by step and be sure that everything is getting wired up as expected

Cheers!

Reply
Default user avatar

to contextualize i do divide into the smallest i can with many layers: controller/Service/DAO... the controller that ask a VO(virtual object) of the class
to be filled with buildFromJson (Json coming from the front end on a request),and it does it well, then i send that VO to the service, there i find the needed objects from another layer the DAO(data acces objects) of each related entity, from the front end i got "servicios" coming so i make a foreach with the same logic, a VO of the data coming in the respective iteration and then this VO turning itself into a entity of the respective class.

I think you already got that from the previous post jaja

so i worked everything to be well setted , don't think that i'm still losing some set or get, cause i have no errors such as property not defined,and the solicitud is getting filled with the right objects, at least thats what i see in the properties setted and var_dumped...inmediately befor calling to persist, i did that to show you what is being setted.

just first lines..

$solicitud->getIdUsuario();
Usuario
object(Usuario)#138 (15) {...

$solicitud->getIdDatofacturacion();
DatoFacturacion
object(DatoFacturacion)#199 (10) {...

$solicitud->getIdDireccion();
Direccion
object(Direccion)#181 (9) {...

$solicitud->getServicios();
object(Doctrine\Common\Collections\ArrayCollection)#188 (1) {
["elements":"Doctrine\Common\Collections\ArrayCollection":private]=>
array(1) {
[0]=>
object(Servicio)#224 (12) {
["codigo":"Servicio":private]=>
string(7) "7yasdfy" ...

in servicio entity it even has the category entity on its idCategoria, same for Direccion and Datofacturacion that knows its relation to the user.

so in th service im establishing the user in some point

$solicitud->setIdUsuario($usuario);

and to the other side

$usuario->addSolicitudes($solicitud);
$resp = $this->usuarioDAO->updateSerialize($usuario);

now im trying with merge...not persist, same, not resulting...when it return it spills NULL

Reply
Default user avatar

found a problem in teh relation between categoria and servicio, for some reason when i var dump $categoria->getServicios it does brings the arrayCollection but empty, without the services related...trying to fix that

Reply

Hey sebastian

I can think in 3 options:
1) Something is resetting your collection at some point in the execution flow
2) You forget to actually add a service to the collection
3) For some reason your code that calls "$solicitud->addServicio()" is not being called

Reply
Default user avatar

updating trouble status jaja:

"A new entity was found through the relationship 'Solicitud#servicios' that was not configured to cascade persist operations for entity: Servicio@00000000044008fd0000000000ad7488. To solve this issue: Either explicitly call EntityManager#persist() on this unknown entity or configure cascade persist this association in the mapping for example @ManyToOne(..,cascade={\"persist\"}). If you cannot find out which entity causes the problem implement

effectively , the entity Solicitud is a new one, cause im trying to create it, the data to create this new Solicitud is being sended from the front-end, with services that i already extracted from DB... i think im about to resolve it.

what do you think about this loop..., should i be asking for a find for each service, as i have the respective id?

foreach($solicitudServicioVO->listadoServicio as $servicioVO)
{
$categoria = $this->categoriaDAO->find($servicioVO->getCategoriaId());
//maybe change this next line
$servicio = $servicioVO->toEntity();
// for this next one?? ill try and tell you...
$servicio = $this->servicioDAO->find($servicioVO->getId());
$servicio->setIdCategoria($categoria);
$solicitud->getServicios();
$solicitud->addServicio($servicio);

}

as i show here, i have the service data inside the $servicioVO, maybe i can go to find it in the DB, on this same loop.

when im doing Solicitud-> addServicio($servicio), im adding one of this fron-end sended services,(servicio), that can construct themselves as object of the Servicio class, on a toEntity pattern function.

so, i understand that doctrine might be complaining cause it thinks that the services added to the arrayCollection are not the same that it already knows from its DB, even though im setting them with its respective ID, cause the error message directs me more to verify the service entity...than the new Solicitud one.

...i did resolved this way, im testing it now, and it works fine, so...if someone else falls into this pit, don't trust your own constructed objects jajaja, doctrine wants it their way, and you shall obey, do find or find by to get the right objects instead of building them, even if the data inside is the same, the persist function got confused, it´s better for this situation to use the direct queried db objects.

Reply

Hey sebastian

Yeah you are right, Doctrine's ORM is a bit selfish about how to fetch your objects from the DB, you can't add constructor arguments for example. Looking at your use case maybe you only need to use Doctrine's DQL and doing object's hydration by your own,
This course may give some good ideas: https://knpuniversity.com/s...

Cheers and thanks for sharing your solution!

Reply
Michael-K Avatar
Michael-K Avatar Michael-K | posted 5 years ago

In my first project;-) I have a the table "user", "member" and "weight". Each user is linked to only one member and each member is linked to many datas in the weight-table (how you have explained it).
Now, I'm trying to save a new weight-data from a form:
With "$this->getUser()->getId();" I can get the Id of the logged user and find the related member_id. Now, the problem is, the form isn't valid, because of the missing member_id. How can I get the member_id to the form and store it in the database?
Sorry for my bad english and my poor beginner question;-)
Cheers from a Knp-University-Fan from Switzerland

Reply

Hey Michael,

Looks like you don't need member_id in your form at all, so just try to remove this field from form. Then before saving a new weight-data, set the proper user and member IDs, which, as you said, you can get. I think it's the easiest solution of your problem. Does it help you? The another one solution is to "inject" member ID into the form type, for example with a hidden field, but since member is an entity - you will need Data Transformer for it. However, you have to do more extra work with validation in this case, so try the first suggestion in the first place.

P.S. Hello to Switzerland! ;)

Cheers!

Reply
Michael-K Avatar

Hi Victor
Thanks a lot for your answers!! I didn't add the field member_id to the form, it's just there. When I have a look in the Debug-tool > Forms > Submitted Data, there is field id, member and weight. But in my form I just added weight.

Reply

Hm, probably I misunderstand you... you say you have a form type, which holds only one field, i.e. weight and that's it, but when you render this form - you'll get an extra field, i.e. member_id, right? Are you sure you don't extend any other form type except the AbstractType in you custom form type? Could you show me what type has your weight field?

Cheers!

Reply
Michael-K Avatar

Yes, the form holds only the field weight and I won't get and other field.. But I found the mistake(?).. in the entity of the weight-table I seted the field member_id to "@Assert\NotBlank()" (I think you showed this in the tutorials, because its the "linked" field to the table memeber) and because of this the form wasn't valid. Is this ok so?
I like Symfony and your tutorials so much!! Keep on going! :-)

Reply

Yo Michael,

Yes, it totally makes sense! Good research. Then we can easily fix it I think. When you create a form, pass the entity with assigned member ID to it as the 2nd argument, I mean something like this:


$user = $this->getUser();
$member = $user->getMember();

$weight = new Weight();
$weight->setMember($member);

$form = $this->createForm(WeightType::class, $weight);
// And only then validate your form
$form->handleRequest($request);
if ($form->isValid()) {
     // persist and flush it!
}

Let me know if it works.

Cheers!

1 Reply
Michael-K Avatar

It works perfect!! Thanks a lot!!

Reply
Richard Avatar
Richard Avatar Richard | posted 5 years ago

Can't help but thinking using the isScientist restriction could have been relevant here.

Reply

Hey Richard

You mean checking if the user is a scientist instead of checking if exists in the genusScientists collection?

Reply
Default user avatar

Let's say in my application, I have a business rule that says that a Genus can only be studied by at most 5 scientists. Would it be fine to check for this in the addGenusScientist method? Wouldn't I be violating the SRP? I'm mixing storing/retrieving data with business logic.

Reply

Hey Johan,

Do you mean addGenusScientist() method on Genus entity here? I think it'll be better to check this rule in a controller, probably using Symfony validator with custom callback. But if we talk about Value Objects - I think it's fine to hold this business logic inside them.

Cheers!

1 Reply
Default user avatar
Default user avatar sokphea chea | posted 5 years ago

Hello, I got this error when run fixture:load "Could not determine how to assign avatarUri to a AppBundle\Entity\User object".
I'm coding along from the '"Getting Crazy with Form Themes"..and I already add new avatarUri property and add get and set for it. Is there's something to do with <imageurl(100, 100,="" 'abstract')=""> ?

Reply

Hey Sokphea!

Look's like you have some extra characters, try this:


AppBundle\Entity\User:
        avatarUri: <imageUrl(100, 100, 'abstract')>

Update:
Oh wait, look's like those characters are added by "Disquss"
Can you show me how your fixtures.yml and user class look's like ?

Cheers!

Reply
Default user avatar
Default user avatar sokphea chea | MolloKhan | posted 5 years ago

Hello Diego, here the the code and it should be correct since I just copy from project code
AppBundle\Entity\User:
user_{1..10}:
email: admin+<current()>@gmail.com
plainPassword: '123456'
roles: ['ROLE_ADMIN']
avatarUri: <imageurl(100, 100,="" 'abstract')="">

Reply

Hey Chea,

First of all, you're missing comma before 'abstract' and have some extra chars from example, the correct example should be:


avatarUri: &lt;imageUrl(100, 100, 'abstract')&gt;

Also notice capitalized "U" in "imageUrl()".

I bet it should work now, but if not - please, double check that you have User::$avatarUri property and proper setter for it. You also can dump($this->avatarUri) inside its setter to see what value is assigned by Alice on loading fixtures.

Cheers!

Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

This course is built on Symfony 3, but most of the concepts apply just fine to newer versions of Symfony.

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": "^7.1.3",
        "symfony/symfony": "3.4.*", // v3.4.49
        "doctrine/orm": "^2.5", // 2.7.5
        "doctrine/doctrine-bundle": "^1.6", // 1.12.13
        "doctrine/doctrine-cache-bundle": "^1.2", // 1.4.0
        "symfony/swiftmailer-bundle": "^2.3", // v2.6.7
        "symfony/monolog-bundle": "^2.8", // v2.12.1
        "symfony/polyfill-apcu": "^1.0", // v1.23.0
        "sensio/distribution-bundle": "^5.0", // v5.0.25
        "sensio/framework-extra-bundle": "^3.0.2", // v3.0.29
        "incenteev/composer-parameter-handler": "^2.0", // v2.1.4
        "composer/package-versions-deprecated": "^1.11", // 1.11.99.4
        "knplabs/knp-markdown-bundle": "^1.4", // 1.9.0
        "doctrine/doctrine-migrations-bundle": "^1.1", // v1.3.2
        "stof/doctrine-extensions-bundle": "^1.2" // v1.3.0
    },
    "require-dev": {
        "sensio/generator-bundle": "^3.0", // v3.1.7
        "symfony/phpunit-bridge": "^3.0", // v3.4.47
        "nelmio/alice": "^2.1", // v2.3.6
        "doctrine/doctrine-fixtures-bundle": "^2.3" // v2.4.1
    }
}
userVoice