Chapters
-
Course Code
Subscribe to download the code!Compatible PHP versions: ^7.1.3
Subscribe to download the code!Compatible PHP versions: ^7.1.3
-
This Video
Subscribe to download the video!
Subscribe to download the video!
-
Subtitles
Subscribe to download the subtitles!
Subscribe to download the subtitles!
-
Course Script
Subscribe to download the script!
Subscribe to download the script!
Inserting into a ManyToMany
Scroll down to the script below, click on any sentence (including terminal blocks) to jump to that spot in the video!
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
:
Show Lines
|
// ... lines 1 - 13 |
class GenusController extends Controller | |
{ | |
Show Lines
|
// ... lines 16 - 18 |
public function newAction() | |
{ | |
Show Lines
|
// ... lines 21 - 38 |
$user = $em->getRepository('AppBundle:User') | |
->findOneBy(['email' => 'aquanaut1@example.org']); | |
Show Lines
|
// ... lines 41 - 51 |
} | |
Show Lines
|
// ... lines 53 - 115 |
} |
That'll work thanks to our handy-dandy fixtures file! We have scientists with emails aquanaut
, 1-10@example.org
:
Show Lines
|
// ... lines 1 - 22 |
AppBundle\Entity\User: | |
Show Lines
|
// ... lines 24 - 28 |
user.aquanaut_{1..10}: | |
email: aquanaut<current()>@example.org | |
Show Lines
|
// ... 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:
Show Lines
|
// ... lines 1 - 14 |
class Genus | |
{ | |
Show Lines
|
// ... lines 17 - 174 |
public function addGenusScientist(User $user) | |
{ | |
Show Lines
|
// ... 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:
Show Lines
|
// ... lines 1 - 14 |
class Genus | |
{ | |
Show Lines
|
// ... lines 17 - 174 |
public function addGenusScientist(User $user) | |
{ | |
$this->genusScientists[] = $user; | |
} | |
} |
Then back in the controller, call that: $genus->addGenusScientist()
and pass it $user
:
Show Lines
|
// ... lines 1 - 13 |
class GenusController extends Controller | |
{ | |
Show Lines
|
// ... lines 16 - 18 |
public function newAction() | |
{ | |
Show Lines
|
// ... lines 21 - 38 |
$user = $em->getRepository('AppBundle:User') | |
->findOneBy(['email' => 'aquanaut1@example.org']); | |
$genus->addGenusScientist($user); | |
Show Lines
|
// ... lines 42 - 51 |
} | |
Show Lines
|
// ... 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?
Show Lines
|
// ... lines 1 - 13 |
class GenusController extends Controller | |
{ | |
Show Lines
|
// ... lines 16 - 18 |
public function newAction() | |
{ | |
Show Lines
|
// ... lines 21 - 40 |
$genus->addGenusScientist($user); | |
$genus->addGenusScientist($user); // duplicate is ignored! | |
Show Lines
|
// ... lines 43 - 52 |
} | |
Show Lines
|
// ... 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:
Show Lines
|
// ... lines 1 - 14 |
class Genus | |
{ | |
Show Lines
|
// ... 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.
34 Comments
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!
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
Hey Roman R.
I'm not sure to understand your problem. Could you give me a bit more of context?
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
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?
Yes! ))) Exactly what I need)
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!
Product must have collection of operations and some operations may call as one and two or more times
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
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
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!
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
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!
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
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
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
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.
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!
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
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!
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.
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!
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! :-)
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!
It works perfect!! Thanks a lot!!
Can't help but thinking using the isScientist restriction could have been relevant here.
Hey Richard
You mean checking if the user is a scientist instead of checking if exists in the genusScientists collection?
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.
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!
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')=""> ?
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!
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')="">
Hey Chea,
First of all, you're missing comma before 'abstract' and have some extra chars from example, the correct example should be:
avatarUri: <imageUrl(100, 100, 'abstract')>
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!
"Houston: no signs of life"
Start the conversation!
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
}
}
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!