Buy

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:

117 lines src/AppBundle/Controller/GenusController.php
... 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:

37 lines src/AppBundle/DataFixtures/ORM/fixtures.yml
... 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:

180 lines src/AppBundle/Entity/Genus.php
... 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:

180 lines src/AppBundle/Entity/Genus.php
... 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:

117 lines src/AppBundle/Controller/GenusController.php
... 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?

118 lines src/AppBundle/Controller/GenusController.php
... 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:

184 lines src/AppBundle/Entity/Genus.php
... 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!

  • 2018-02-13 Diego Aguiar

    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!

  • 2018-02-12 sebastian

    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.

  • 2018-02-09 Diego Aguiar

    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

  • 2018-02-09 sebastian

    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

  • 2018-02-08 sebastian

    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

  • 2018-02-08 Diego Aguiar

    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!

  • 2018-02-08 sebastian

    Diego Aguiar 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

  • 2017-09-27 Diego Aguiar

    Hey maxii123

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

  • 2017-09-27 maxii123

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

  • 2017-05-10 Victor Bocharsky

    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!

  • 2017-05-09 sokphea chea

    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')="">

  • 2017-05-08 Diego Aguiar

    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!

  • 2017-05-07 sokphea chea

    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')=""> ?

  • 2017-01-26 Michael

    It works perfect!! Thanks a lot!!

  • 2017-01-26 Victor Bocharsky

    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!

  • 2017-01-25 Michael

    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! :-)

  • 2017-01-25 Victor Bocharsky

    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!

  • 2017-01-24 Michael

    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.

  • 2017-01-24 Victor Bocharsky

    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!

  • 2017-01-23 Michael

    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

  • 2016-11-09 Victor Bocharsky

    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!

  • 2016-11-08 Johan

    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.