Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

OneToMany: Inverse Side of the Relation

Keep on Learning!

If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.

Start your All-Access Pass
Buy just this tutorial for $12.00

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

We have the Genus object. So how can we get the collection of related GenusNote Well, the simplest way is just to make a query - in fact, you could fetch the GenusNote repository and call findBy(['genus' => $genus]). It's really that simple.

Tip

You can also pass the Genus's ID in queries, instead of the entire Genus object.

But what if we could be even lazier? What if we were able to just say $genus->getNotes()? That'd be cool! Let's hook it up!

Setting up the OneToMany Side

Open up GenusNote. Remember, there are only two types of relationships: ManyToOne and ManyToMany. For this, we needed ManyToOne.

But actually, you can think about any relationship in two directions: each GenusNote has one Genus. Or, each Genus has many GenusNote. And in Doctrine, you can map just one side of a relationship, or both. Let me show you.

Open Genus and add a new $notes property:

... lines 1 - 11
class Genus
{
... lines 14 - 48
private $notes;
... lines 50 - 109
}

This is the inverse side of the relationship. Above this, add a OneToMany annotation with targetEntity set to GenusNote and a mappedBy set to genus - that's the property in GenusNote that forms the main, side of the relation:

... lines 1 - 11
class Genus
{
... lines 14 - 45
/**
* @ORM\OneToMany(targetEntity="GenusNote", mappedBy="genus")
*/
private $notes;
... lines 50 - 109
}

But don't get confused: there's still only one relation in the database: but now there are two ways to access the data on it: $genusNote->getGenus() and now $genus->getNotes().

Add an inversedBy set to notes on this side: to point to the other property:

... lines 1 - 10
class GenusNote
{
... lines 13 - 39
/**
* @ORM\ManyToOne(targetEntity="Genus", inversedBy="notes")
* @ORM\JoinColumn(nullable=false)
*/
private $genus;
... lines 45 - 99
}

I'm not sure why this is also needed - it feels redundant - but oh well.

Next, generate a migration! Not! This is super important to understand: this didn't cause any changes in the database: we just added some sugar to our Doctrine setup.

Add the ArrayCollection

Ok, one last detail: in Genus, add a __construct() method and initialize the notes property to a new ArrayCollection:

... lines 1 - 11
class Genus
{
... lines 14 - 50
public function __construct()
{
$this->notes = new ArrayCollection();
}
... lines 55 - 109
}

This object is like a PHP array on steroids. You can loop over it like an array, but it has other super powers we'll see soon. Doctrine always returns one of these for relationships instead of a normal PHP array.

Finally, go to the bottom of the class and add a getter for notes:

... lines 1 - 11
class Genus
{
... lines 14 - 105
public function getNotes()
{
return $this->notes;
}
}

Time to try it out! In getNotesAction() - just for now - loop over $genus->getNotes() as $note and dump($note):

... lines 1 - 12
class GenusController extends Controller
{
... lines 15 - 94
public function getNotesAction(Genus $genus)
{
foreach ($genus->getNotes() as $note) {
dump($note);
}
... lines 100 - 109
}
}

Head back and refresh! Let the AJAX call happen and then go to /_profiler to find the dump. Yes! A bunch of GenusNote objects.

Oh, and look at the Doctrine section: you can see the extra query that was made to fetch these. This query doesn't happen until you actually call $genus->getNotes(). Love it!

Owning and Inverse Sides

That was pretty easy: if you want this shortcut, just add a few lines to map the other side of the relationship.

But actually, you just learned the hardest thing in Doctrine. Whenever you have a relation: start by figuring out which entity should have the foreign key column and then add the ManyToOne relationship there first. This is the only side of the relationship that you must have - it's called the "owning" side.

Mapping the other side - the OneToMany inverse side - is always optional. I don't map it until I need to - either because I want a cute shortcut like $genus->getNotes() or because I want to join in a query from Genus to GenusNote - something we'll see in a few minutes.

Tip

ManyToMany relationships - the only other real type of relationship - also have an owning and inverse side, but you can choose which is which. We'll save that topic for later.

Now, there is one gotcha. Notice I did not add a setNotes() method to Genus. That's because you cannot set data on the inverse side: you can only set it on the owning side. In other words, $genusNote->setGenus() will work, but $genus->setNotes() would not work: Doctrine will ignore that when saving.

So when you setup the inverse side of a relation, do yourself a favor: do not generate the setter function.

Leave a comment!

51
Login or Register to join the conversation
Default user avatar
Default user avatar Mark Ernst | posted 4 years ago

I've been fiddling around with something that I can't quite get my head around to work with Doctrine. Simply put I have filters, accounts and transactions. I try to filter a transaction into the right category but to do that, I need to access the filters.

Now, iterating multiple transactions, I can fetch them via Transaction->getAccount()->getFilters() but that seems like a waste of resources. In plain old MySQL I would simply fetch it via a relation. Assuming my Transaction is linked to an Account and my Filter is linked to an Account, I can safely assume that they both have a corresponding account_id column. I'd fetch all filters via a JOIN.

E.g: (...) FROM transaction JOIN filter ON filter.account_id = transaction.account_id

However, doing this in PHP/Doctrine has me puzzled. It basically bores down to a Transaction having multiple Filters via Account, but I don't need the account at this time.

Solutions?

Reply

Hey Mark Ernst

If I understand you correctly you can get all the filters of a transaction without having an account, if that's correct, then, why you get the transaction's filters like this: $transaction->getAccount()->getFilters()? you should be able to do this $transaction->getFilters() right?
Anyways, what you can do is fetching the filters directly from the "FilterRepository" service by passing in a Transaction object

Cheers!

Reply
Default user avatar

Unfortunately no. ;) I've got a Filter and Transaction entity that are both linked to an Account. However, Filter and Transaction have no direct link to eachother. I want to fetch all the Filters from a specific Account via the Transaction. There is no mapping involved, no junction table whatsoever. As explained before, both could be fetched via old plain MySQL (thus using the QueryBuilder) but I want to know if it can be done via mapping on the entity itself.

Normally if you'd have a OneToMany, it'll fetch the Filter on each filter where filter.transaction_id equals that of the Entity. However, Filters have nothing to do with Transactions in my case. I use Filters to detect which shop/category I require and file them there.

Here is the gist with the files I was talking about:
https://gist.github.com/ReS...

Reply

Ohh I get it now. I'm afraid that's not possible using the ORM because your Transaction object doesn't know anything about Filters. You can add a shortcut in your Transaction class like this:


// src/Entity/Transaction.php

public function getAccountFilters()
{
     return $this->getAccount()->getFilters();
}

but it will keep executing the same amount of queries. You can always execute your own queries but it will require extra work to hydrate your query result into objects.

1 Reply
Default user avatar

Aw shucks. So nothing can be done with JoinColumn or JoinTable? I "hate" to fetch the Account when I actually need filters... Ah well, I guess if there is no midway here I'll just grab a version of your solution!

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

hi, i used all the setter and getters and the annotations mentioned on this tutorial, but for some reason the related entities on the owning side are not being retrieved to the inverse side,

when i use something like :
$direcciones = $usuario->getDirecciones();
foreach ($direcciones as $dir) { var_dump($dir); }

it says invalid argument supplied for foreach, that's cause the arrayCollection of the property its empty or null, it's weird, dont know why for a not understood reason, the property does not get filled, even if wasn't fetching in EAGER, but i do...the call to the arrayCollection getter is done.

is there anything else that im forgetting? , all the annotations are right and the names of the properties too.

this is the var_dump, as you can see, direcciones doesn't bring something inside, and it comes from a simple sentence
$usuario = $this->usuarioDAO->find(20); , here my DAO has the doctrine functions like find , findBy(etc).

someone help me please

object(Usuario)#109 (13) {
["rut":"Usuario":private]=>
string(10) "17738715-0"
["nombre":"Usuario":private]=>
NULL
["correo":"Usuario":private]=>
string(15) "hola1@gmail.com"
["contrasena":"Usuario":private]=>
string(6) "123123"
["perfil":"Usuario":private]=>
string(7) "usuario"
["telefono":"Usuario":private]=>
string(7) "7346534"
["id":"Usuario":private]=>
int(20)
["direcciones":"Usuario":private]=>
NULL
}

Reply

Hey sebastian

I believe that you forgot to initialize (to a new ArrayCollection) the "direcciones" property in the "Usuario" class constructor. If that's not the case, let me know!

Have a nice day

Reply
Default user avatar

already setted this way:

direcciones = new \Doctrine\Common\Collections\ArrayCollection();
}
...

also tried just :

$this->direcciones = new ArrayCollection();

That's why i'm asking, i've done and replyed a few tutorials by now, has taken me days of none achieve on retrieving associated data from the inverse side.

I'm in the point of, paying any tutorial that can tell me the final truth jaksjjkas, or just start to use another way to bring related data as DQL or anything on hands to achieve, you know people don't pay for things not getting done.

Thanks for your time, if you have anythign else on mind or if i can share you anything else of the code, just tell me.

Have a nice day too Diego

Reply
Default user avatar

i started using, it seems that on my config files specifically bootstrap.php file

i have something like in the next paragraph, i trusted that doctrine was paying attention to the php entities annotations, it seems that it is first paying attention to the XML metadata configuration , or the last change that i've done in the Usuario.dcm.xml was effectively affecting the result of the query or to the definition of the relation.

$config = Setup::createXMLMetadataConfiguration(array(__DIR__."/config/xml"), $isDevMode); ...so i went and put a definition for the relation on the XML file for Usuario.dcm,xml...this way:

<one-to-many field="direcciones" target-entity="Direccion" mapped-by="idUsuario" indexby="id"/>

now it seems to be trying to execute and what the sentence spit is something more reazonable but...still a fail, i show you.

'Doctrine\DBAL\Exception\SyntaxErrorException' with message 'An exception occurred while executing 'SELECT t0.Calle AS Calle1, t0.depto AS depto2, t0.block AS block3, t0.Numero AS Numero4, t0.Comuna AS Comuna5, t0.Codigo Postal AS CodigoPostal6, t0.id AS id7, t0.id_usuario AS id_usuario8 FROM direccion t0 WHERE t0.id_usuario = ?' with params [20]:

it seems to be trying to retrieve directions from the mdfk user who has id = 20.

now it is an syntax error, as i don't manage how the query is written... so now i'm a step closer, still have to figure out how the query could be "well defined", to not fail on the execution.

Reply

Ohh so you were using both ways to define your entity metadata. As you noticed, Symfony gives preference to XML, I recommend you to choose only one (is easier to maintain and to avoid those what tha heck moments).

After doing that, you need to update your database schema, but in this case I would recreate everything (you know, just in case)


php bin/console doctrine:database:drop 
php bin/console doctrine:database:create
php bin/console doctrine:schema:create 
php bin/console doctrine:schema:update  // I'm not sure if this step is needed

Let me know if this works!

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

thank you, yesterday in the afternoon, before doing my extra hours to resolve it jajaja,
i did found that same fact, i stopped trying with the annotations and yeah i realize that the changes made on the XML where truly affecting the querys to the DB, so for the maximun detail in how i resolved this, to help anyone who gets to this disqus. as you saw before i was getting an error in the execution of the query, well, that was happening cause i had a column in my DB called "Codigo Postal", so what occurs is that the query gets miswritten for this reason... you know, the space between the words... daaaaah

Thanks for reading MolloKhan

Reply

Wooow, I totally missed it!
Yeah, you have to pay attention to your column names, some times you can choose a name that is a reserved word in MySql, and the error message is not that helpful :)

I'm glad you could fix your problem, cheers!

Reply
Default user avatar
Default user avatar 3amprogrammer | posted 5 years ago

There is a very useful shortcut - which I use daily - fore -> tab. It creates foreach loop and generates as variable name automatically. You should definitely check this out!

Reply
Default user avatar
Default user avatar Terry Caliendo | posted 5 years ago

Nevermind... its the first thing you say in the video! Deleting this post!

Reply
Default user avatar
Default user avatar Terry Caliendo | posted 5 years ago

How do all of Genus' notes get assigned to the Genus? I don't see anything that seems to explicitly state that a lookup is happening. Is something magical happening in the __construct function with the ArrayCollection? Does Symfony know that if you assign a new ArrayCollection to a GenusNotes type (ie. $this->notes) that it should find all the associated GenusNotes?

Reply
Default user avatar

Seems to me the line should be more like:
$this->notes = new ArrayCollection($this->Genus);

Reply

Yo Terry!

Oh yea, it's super magic :). Here's how it works behind the scenes:

1) We query for a Genus.
2) Doctrine populates all of the normal (non-relationship) fields onto the Genus object
3) Since we have only queried for a Genus, we don't have the GenusNotes data yet. So, Doctrine assigns a PersistentCollection object to the notes property. This and the ArrayCollection object have all the same methods, so most people don't notice that sometimes notes is an ArrayCollection and sometimes it's a PersistentCollection. The point is, don't get too hung up on this point :).
4) Later (e.g. maybe in our templates) we try to access the data on our notes property - e.g. we call $genus->getNotes() to fetch that PersistentCollection object and we start looping over it. At that moment, the PersistentCollection object makes a query to the database for the GenusNotes and populates itself with that data. We don't realize this is happening, and we happily loop over those GenusNotes as if there were always there.

This is called "lazy loading" and when you look under the hood like this, it's pretty obvious why :). Also, it turns out that the __construct() we added, is not important at all for any of this to happen. I mean, if you remove the $this->notes = new ArrayCollection(), everything I just described will work perfectly. The only reason we do this, is so that - under all circumstances - the notes property is a "collection" object (either PersistentCollection or ArrayCollection - they implement the same interface). Without this code, if you query for a Genus notes will be a collection. But if you create a new Genus, then notes would actually be null. That might not be an issue... until you perhaps call getNotes() on a new Genus or try to add a GenusNote to the notes property. Suddenly, when you're expecting a collection (i.e. something that acts like an array), you're dealing with null.

Let me know if that helps! Probably it was more explanation than you wanted ;).

Cheers!

Reply
Default user avatar
Default user avatar Terry Caliendo | weaverryan | posted 5 years ago

No... I highly appreciate the detailed explanation. Thanks much!

Reply
mehdi Avatar

What a fantastic tutorial!
I want a clarification, you said that the owning side is the GenusNote Entity but the GenusNotes can't exist without a Genus already created, therefore the owning side must be Genus ?

Reply

Hey mehdi!

Wow, that's a really great point you made! :)

So, the idea of the "owning" side is completely a "Doctrine" concept. And the "owning" side is GenusNote, because it is the side where the foreign key column exists (genus_id). This simply means that we need to make sure that we call $genusNote->setGenus($genus) so that Doctrine is aware of the relationship (if we only set the GenusNote onto the Genus object, Doctrine would not "notice" the relationship).

But once Doctrine saves things, yes, it actually realizes that the Genus row needs to be inserted first and then the GenusNote row, because we need to know the value of genus_id in order to save the GenusNote. This is more of a "detail that we're not supposed to care about". All we do is make sure that the GenusNote.genus property is set, and Doctrine takes care of doing the INSERT queries in the correct order.

Does that make sense? Cheers!

1 Reply
mehdi Avatar

YES, I get it. thank you for your explanations.

Reply
Default user avatar
Default user avatar Yahya A. Erturan | posted 5 years ago | edited

Well, I have User and UserInformation entities. Here is the owning side will be UserInformation because it will have the foreign key - but I really need to make sure that I am doing in correct way with right annotations etc:


class User implements UserInterface
{
    /**
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     * @ORM\Column(type="integer")
     */
    private $id;

    /**
     * @ORM\Column(type="string")
     */
    private $firstName;

    /**
     * @ORM\OneToOne(targetEntity="App\Entity\UserInformation", mappedBy="user")
     */
    private $userInformation;

    .......
    /**
     * ONLY GETTER
     * @return UserInformation
     */
    public function getUserInformation()
    {
        return $this->userInformation;
    }
}


class UserInformation
{
    /**
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     * @ORM\Column(type="integer")
     */
    private $id;

    /**
     * @ORM\Column(type="string")
     */
    private $gender;

    /**
     * @ORM\OneToOne(targetEntity="App\Entity\User", inversedBy="user")
     * @ORM\JoinColumn(name="user_id", referencedColumnName="id", nullable=false)
     */
    private $user;

    ......
    /**
     * @return mixed
     */
    public function getUser()
    {
        return $this->user;
    }

    /**
     * @param mixed $user
     */
    public function setUser($user): void
    {
        $this->user = $user;
    }
}

Saving to relation :


       $em = $this->getDoctrine()->getManager();

        $user = new User();
        $user->setFirstName('Yahya');

        $userInformation = new UserInformation();
        $userInformation->setGender('M');
        $userInformation->setUser($user);

        $em->persist($user);
        $em->persist($userInformation);
        $em->flush();

As I said, it would be good to know if it is all right. Then how can we make a search through UserInformation gender property ? Sorry, I have a lot of questions :)

Reply

Hey Yahya,

Yes, your mapping looks valid for me, good you don't have setter on inverse side. And how you save entities are good too. Btw, to make sure you have a valid annotation which matches your DB schema - you can run the next command:
bin/console doctrine:schema:validate

What about your question, what is your problem? Do you want to search for User or UserInformation? It's easy to make this search with query builder. Create a custom repository for entity you want to search. In some queries where you search for a different from UserInformation entity you would need to join UserInformation first to be able to use gender in WHERE statements.

Also, I think it's good to reduce the length of gender column to 1 char that is enough as I see from your code, and add an index for this column if you need to filter by gender in some queries.

Cheers!

Reply
Richard Avatar
Richard Avatar Richard | posted 5 years ago

Surely the notes to genus is, at the end of the day, a unidirectional relationship? The notes refer to the Genus. Are we not complicating things by mangling it into "bidirectional" and thus meaning we now have to juggle mappedBy and inversedBy because of it being bidirectional all of a sudden. A quick google suggests there is a lot of confusion out there with regards to mappedBy and inversedBy.

Reply

Hey Richard ,

Yes, you always need to avoid bidirectional relationship, but you don't have to. Sometimes it could be useful in query builder, or when you need to fetch some data from the inverse side of the relationship. So the main idea is to use bidirectional relashinship only when you really need it. If you can do without it - then do not add it at all! But this feature is exist, and why don't use it if it helps you and make your life easier? :) And of course, you should have strong understanding of mappedBy/inversedBy concepts, otherwise you can do some mess in your code, but it's not the reason do not use it when it perfectly fit your business. ;)

Cheers!

1 Reply
Default user avatar
Default user avatar Ruben Bijker | posted 5 years ago

Hi,
I have 2 entities: Boat and Amenity. There needs to be a ManyToMany relationship between Boat and Amenity. I also need to be able to store the amount of Amenities a Boat has. Eg. A Boat can have 3 Kayaks. Therefor I have created to join table JoinBoatAmentity that stores: boat_id, amenity_id, amount.

My setup of entities looks like this:
Boat <onetomany>JoinBoatAmentity<manytoone>Amenity

I have created this relationship according to this article http://future500.nl/article...

Now I have created a form to edit a Boat. My problem is the following; How do I add a field/section in the boat form builder that displays all the Amenities that are stored in the Amenity table, and that will have a field next to it displaying the amount this boat has, displaying 0 if it doesn't have any.
eg.:
[3] Kayak
[0] Surfboard

Your help would be very much appreciated.
Thank you!
Ruben

Reply

Yo Ruben!

Ok - first thing: your relationship setup looks perfect. Because of the "amount" property on the "join table", this is not a true ManyToMany relationship - it's actually 2 ManyToOne relationships, and you nailed that :).

This form will be very tricky, especially because you want to display *all* of the amenities, even if the amount is 0. After thinking about this, I believe this is your best option:

1) Don't use the forms system :). You would just render these text boxes yourself, and perhaps save them via AJAX or fetch them off of the Request object manually on save. No shame in this - it's a fairly "simple" form - so you don't need a lot of help from Symfony. But the data modeling is complex, so it will be harder to fit this into Symfony forms. This is the cardinal rule of Symfony forms: use them when they help, don't use them when they don't help.

I *was* going to also give you an option (2) that uses the form framework, but it's quite advanced - honestly, something we could have a screencast on all by itself :). So, I'll save that for later.

Let me know if this helps clear things up!

Cheers!

Reply
Default user avatar

Hi Ryan,
Thank you for your answer. In the end I got an experienced Symfony programmer to build the form for me, and it ended up being a quite complex solution. The lesson I can draw from this is like you said: "use them when they help, don't use them when they don't help.". I think in many cases it is better to just create (parts of) a form custom rather then making it really complex with the Sf form builder. I look forward to more tutorials on forms for Sf3.
Cheers!

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

EDIT: I found my error! It was in the first line.. I forgot an * in the mapping setup. Should Be /** instead of /*. (Leaving this comment here in case someone else has the same error.)

Hello, thank you for these tutorials, they are a great help! However, I have encountered a mapping error: The association AppBundle\Entity\ProfileComment#profile refers to the inverse side field AppBundle\Entity\Profile#comments which does not exist.

I'm using Profile for Genus and Comments for Notes, but $profile->getComments() is returning null. Here's my code:

Profile.php


	/*
	 * @ORM\OneToMany(targetEntity="ProfileComment", mappedBy="profile")
	 */
	private $comments;

	public function __construct(){
		$this->comments = new ArrayCollection();
	}
	...
	public function getComments(){
		return $this->comments;
	}

ProfileComment.php


	/**
	 * @ORM\ManyToOne(targetEntity="Profile", inversedBy="comments")
	 * @ORM\JoinColumn(nullable=false)
	 */
	private $profile;

	public function setProfile(Profile $profile){
		$this->profile = $profile;
	}

ProfileController.php


	public function getCommentsAction(Profile $profile){
		$com = $profile->getComments();
		dump($com); //returns null??
	}

Thank you in advance, I hope you can help me figure out what's going on. I have searched online to no avail.

Reply

Hey Chris,

Thanks for sharing it! And glad you found the problem by yourself. Yes, to be able to parse annotations with PHP you need to write doc-block comments which should start with "/**".

Cheers!

Reply
Default user avatar
Default user avatar Roberto Briones Argüelles | posted 5 years ago | edited

I'm trying to setup something like this, but without success, just with restaurants instead of genus and with addresses instead of notes.

I have this:


public function listAction(): Response
{
    $restaurants = $this->entityManager->getRepository(Restaurant::class)->findAll();
    foreach ($restaurants as $restaurant){
    $address = $restaurant->getAddress();
    $id = $address->getId();
}

And it seems that the $address object is a PersistentCollection, not an RestaurantAddress object like my Entity, so I get an exception:
Call to undefined method Doctrine\ORM\PersistentCollection::getId().

I'm not using annotations, just yaml configurations, this for my RestaurantEntity:


AppBundle\Entity\Restaurant:
type: entity
table: null
repositoryClass: AppBundle\Repository\RestaurantRepository
id:
id:
type: integer
id: true
generator:
strategy: AUTO
fields:
name:
type: string
length: 255
oneToMany:
address:
targetEntity: AppBundle\Entity\RestaurantAddress
mappedBy: restaurant
lifecycleCallbacks: { }

and this for my RestaurantAddress entity:


AppBundle\Entity\RestaurantAddress:
type: entity
table: null
repositoryClass: AppBundle\Repository\RestaurantAddressRepository
id:
id:
type: integer
id: true
generator:
strategy: AUTO
fields:
street:
type: string
length: 255
number:
type: string
length: 255
postalCode:
type: string
length: '10'
column: postal_code
interiorNumber:
type: string
length: '10'
column: interior_number
neighborhood:
type: string
length: 255
city:
type: string
length: 255
manyToOne:
restaurant:
targetEntity: AppBundle\Entity\Restaurant
inversedBy: address
lifecycleCallbacks: { }

My DB is created by Symfony and the data is fake using fixtures. Any idea what could be wrong?

Reply

Hey Roberto,

Sorry for the delay, your message was in spam box. So as I see, the Restaurant entity relates to the RestaurantAddress as OneToMany, i.e. each Restaurant entity may have one or more address. If it's on purpose - then when you call $restaurant->getAddress() - you get ArrayCollection of RestaurantAddress entities, so you have to iterate over them.


public function listAction(): Response
{
$restaurants = $this->entityManager->getRepository(Restaurant::class)->findAll();
    foreach ($restaurants as $restaurant){
        // because $restaurant->getAddress() returns another ArrayCollection!
        foreach ($restaurant->getAddress() as $address)
            $id = $address->getId();
        }
    }
}

So you better to rename your Restaurant::$address property to Restaurant::$addresses, because it holds collection! But if you want that Restaurant may have one and only one address - use the OneToOne relationship instead.

P.S. You can always dump things like "dump($restaurant->getAddress()); die;" - it will help you to understand what the getAddress() method returns.

Cheers!

Reply
Default user avatar
Default user avatar Yehuda Am-Baruch | posted 5 years ago

Hi, i'm running symfony 3.2.4 with doctrine/orm 2.5.6
And this doesn't seems to work.. I get null for OneToMany field when $em->getRepository(TestEntity::class)->findAll();

It seems that the __construct() is not called at all.

Reply

Hey Yehuda Am-Baruch!

Very good detective work! When you query for an entity (doesn't matter if it's find(), findAll(), custom query, etc) - Doctrine does not call your object's __construct method. That can be weird at first, but it's by design. Doctrine's philosophy is that an object is really only instantiated one time - when you originally say new Genus. When you query for this later, Doctrine says that you are not really "re-creating" it. You are simply "waking it up" from sleeping (in the database). So, it skips the constructor.

So let's look at what happens with relationships. There are 2 different scenarios:

1) When we create a new Genus, the __construct() method is called and the genusNotes property is set to a new ArrayCollection.

2) When we query for a Genus, the __construct() method is not called. However, this does not mean that genusNotes is null. Obviously, when you query for a Genus, Doctrine puts all of the data from the database onto its properties - e.g. id, name, etc. It does the same thing for the genusNotes property - it sets this to a PersistentCollection object (side note: PersistentCollection and ArrayCollection implement the same Collection interface). This object holds your GenusNote objects. Actually, it's a bit more complex / interesting than that: the PersistentCollection is empty at first, but as soon as you reference the genusNotes property and try to loop over the items in it, PersistentCollection performs a (lazy) query for the GenusNote objects.

Phew! So, you should never see null as the value for the genusNotes OneToMany field. If you are, there must be some misconfiguration somewhere! If you are still having issues, you can definitely post some code here!

Cheers!

Reply
Default user avatar
Default user avatar Yehuda Am-Baruch | weaverryan | posted 5 years ago | edited

Hey weaverryan ,

Thank you for your informative answer.

If I understood correctly that means I need to generate a new Instance with "new" so the __construct() will set the genusNotes to ArrayCollection ? (Because basically I don't see a reason to create one if not, as I get the Instance from the Query).

Anyway, This also doesn't work, I failed to mention, that I've build another project under symfony 3.0.1, doctrine/orm 2.5.6 where everything works just fine..

The thing I did:
To be fair the doctrine did most of it when I import DB :)

At the child Entity I set:

/**
* @var \AppBundle\Entity\Genus
*
* @Serializer\Groups({"Default"})
* @Serializer\Expose()
* @ORM\ManyToOne(targetEntity="AppBundle\Entity\Genus", inversedBy="genusNotes")
* @ORM\JoinColumns({
* @ORM\JoinColumn(name="fk_genus_id", referencedColumnName="genus_id")
* })
*/
private $fkGenus;

At the parent Entity I set:
/**
* @var \AppBundle\Entity\genusNotes
*
* @Serializer\Groups({"Default"})
* @Serializer\Expose()
* @ORM\OneToMany(targetEntity="AppBundle\Entity\genusNotes",mappedBy="fkGenus")
*/
private $genusNotes;

public function __construct()
{
$this->genusNotes= new ArrayCollection();
}
public funtion getGenusNotes(){
$this->genusNotes;

}

Results are showing All Genus Data & Parent Data as well, but the child data is Null..

I really fail to see the problem (Could it be anything local after I've cleared cache?).
Will appreciate any feedbacks.

*Also, need to mention that DB relations was verified and the $em->getRepository(GenusNotes::class)->findBy(['fkGenus'=>$genus]);
(where $genus is result of last query) returns the notes with the Genus Data under fkGenus.

Regards,
Yehuda.

Reply

Hi Yehuda Am-Baruch!

Hmm. I didn't quite explain myself well :). The most important idea from my first message was this:

> When you query for a Genus, the genusNotes property will be a PersistentCollection, never null. So if you're seeing null, something is misconfigured!

So let's get down to debugging this! First, you mentioned that you did an "import DB". Are you referring to this: http://symfony.com/doc/curr... If so, make sure that you have deleted the orm.xml files that were created as part of this process! If you don't delete these, then Doctrine continues to read the metadata from those XML files, and ignores your annotations. This could explain why Doctrine is not seeing your OneToMany relationship!

Other than that, I don't see any issues. So, let me know if the XML files are the problem... and if not, we'll think of something else to debug!

Cheers!

Reply
Default user avatar
Default user avatar Yehuda Am-Baruch | weaverryan | posted 5 years ago | edited

Hi weaverryan ,

Again, thank you for your informative feedback.

There is no doubt that a logical conclusion would be that the annotation is not applied.

I've also applied a repository for that entity that its methods are not working -which makes that thought stronger.

Yet it is only for Symfony, the phpStorm refers to the new repository methods (and autocomplete).

The import was done precisely according to that manual you referred.
The xml files removed before contacting, and in the config.yml, under doctrine.orm I put:

auto_mapping: false
mappings:
AppBundle:
type: annotation

I also tried:
-updating symfony to 3.2.5
-purge all composer cache and reinstall everything (after cloning the project to a brand new directory).
-Of course, purging cache

Again, the phpStorm recognize the repository reference for that entity but I get error on symfony.

I still convinced that, for some reason, Symfony ignores the annotation -could you think of a reason?

Thanks for your help :)

Reply

Hey Yehuda Am-Baruch!

Hmm, yes, I agree with your assessment! I also think that Symfony/Doctrine is still not reading the annotations! Your configuration in config.yml should be ok, but let's change it back to how it looked originally: https://github.com/symfony/symfony-standard/blob/3d0af0691582c826a8fa2d44749fe832423c9100/app/config/config.yml#L59-L62. You should not have needed to change this config, and while your new config should work, I want to eliminate this as a possible cause.

Also, one way to know for SURE whether or not the annotations are being ignored, is to add a new field + @ORM\Column to your entity. Then, run bin/console doctrine:schema:update --dump-sql. If you see the SQL to add the new column, then your annotations ARE being read correctly. If you do no, then they are NOT being read.

Let's determine whether or not the annotations are being read for sure first! If they are NOT being read, I believe there are only 2 causes: (A) There are .orm.xml or .orm.yml files in your bundle, and Doctrine is reading these inead or (B) your configuration in config.yml is incorrect (which we will definitely have fixed by changing the settings back to the default).

I'm sure we're close! Cheers!

Reply
Default user avatar
Default user avatar Yehuda Am-Baruch | weaverryan | posted 5 years ago | edited

Eureka!

weaverryan thank you very much for your help!
The configuration was ok, yet annotaion were ignored due to cache of Redis (https://github.com/snc/SncR...

I did flush the DB but I guess i had a connection problem to the redis-server or something so it didn't actually flushed.

Everything is seems to work just fine now, and that event was written in the Book-Of-Days of our company :)

Again, thank you very much for the wonderful and informative answers (and tips!)

Have a lovely weekend! :)

Reply

Ah, Eureka indeed! Of course it would be caching ;). Good debugging and have a great week yourself!

Reply
Trafficmanagertech Avatar
Trafficmanagertech Avatar Trafficmanagertech | posted 5 years ago

Hello,
Is it normal that the return type of the getter function is sometimes PersistentCollection instead of ArrayCollection?

I followed your instructions, plus I added a php7 return type to the getter function:
public function getCategories() : ArrayCollection
{
return $this->categories;
}

But I get the type error (Return value of getCategories() must be an instance of ArrayCollection, instance of PersistentCollection returned) when in twig I do:
{{ mainCategory.categories.count() }}

PS Since they both implement Collection, I changed the return type to Collection to solve the error

Reply

Hey there!

Hm, you did it right when typehinting to the Collection interface to fix it. Anyways, you will get a collection and you can iterate over it.

Did you update you entity constructor to set an ArrayCollection by default on entity creation?


public function __construct()
{
    $this->categories = new ArrayCollection();
}

P.S. You can use length filter instead in Twig templates:
{{ mainCategory.categories|length }}. This code will work with both ArrayCollection and PersistentCollection and even with simple PHP arrays.

1 Reply

BTW, please, do the "bin/console doctrine:schema:validate" command to ensure your mapping is correct. You should get 2x "OK" on Mapping and Database checks

Reply
Default user avatar
Default user avatar Larry Lu | posted 5 years ago

Hi I have a fairly complicated relationships that I need to do. I have a bunch of images that contains characters of an anime, and I identify each character as character model, and for each model I want to identify the color used for every part of his body, for example, "hair => black", "skin => red". For each image, I wanted to be able to add the character models, then custom define the body parts that appeared on the image and then set a color to each. And then all of these would be saved to the database.

I started with 5 entities, Image, CharacterModel, Character, ModelPart and PartName. The Character and PartName already have data declared in the db. I have defined all relationships between the entities, they are Image<=OneToMany=>CharacterModel, CharacterModel<=ManyToOne=>Character, CharacterModel<=OneToMany=>ModelPart, ModelPart<=ManyToOne=>PartName. And the ModelPart entity will hold the color property.

As you can see, the CharacterModel entity has three different relationships with three other entities. How do I even set this up in the controller for adding a new character model? Is it even possible to do it in one go?

Reply

Hey Larry Lu!

Haha, yes! This *is* complicated! Overall, it seems like you've given a lot of thought to your setup. Here is some advise, which I hope will help:

A) If possible, I would remove PartName and just make this a string field on ModelPart. This may not help simplify a TON, but it will help simplify a bit. The only reason to *keep* this entity is if you need to be able to dynamically add new (via an admin interface) body parts to the system quite often. Do whatever you need for your app :). Even if it is a string field, you can still query by the body part, as it's just a simple string.

B) Another possible simplification would be to use the json_array field type to remove some relationships. If it's *really* as simple as just storing "hair => black", then, in theory, you could add a modelColors field to CharacterModel and store all that information on that one json_array field (and remove ModelPart and PartName). This would make doing "search" queries more difficult - that's the big downside.

But even if you keep this full setup, you should be able to setup the model without too much trouble in a controller. But, just *don't* try to do it via the form system. What I mean is, even if you do want to use the form system, you should:

1) Create a form class that models (i.e. "looks like") what you want your form to look like on the page. Do *not* bind this to any entity (or, create a new "form" model if you want - this is not an entity - and bind the form to that model class)

2) In the controller, take the data directly off of the form (or directly from the "form" model) and manually create all the objects and setup all of the relationships. This is the most important part: creating these objects & relationships in normal PHP shouldn't be too hard. But trying to make a form system that will do it automatically for you would be crazy, and not worth the complexity.

Let me know if that helps!

Cheers!

Reply
Default user avatar
Default user avatar Larry Lu | weaverryan | posted 5 years ago | edited

Hi, my plan is to make it so that all the parts and color can be customary build and make them all searchable, so it couldn't be simplify anymore. I entered some random data into the database for testing and it works.

1) Yeah that had been what I heard too from the tech talk videos about no passing post data directly into the entity object with the form. Instead use a middle-man form object to take the post data, validate with the form first, before passing. Something such as this:
https://blog.martinhujer.cz/symfony-forms-with-request-objects/

But what I don't understand is how do you get the data out after the "submit & validated"?


$em = $this->getDoctrine()->getManager();
 
$addModelObject = new addModel();
$form = $this->createForm(addModelFormType::class, $addModelObject);

$form->handleRequest($request);

if ($form->isSubmitted() && $form->isValid()) {
      ????
      $em->persist(???);
      $em->flush();      
}

In the Symfony documents, everything was done implicitly and it confuses me. For example, where do I get the data from after the validation? From the $form or $addModelObject? And how do I get them out?

Reply

Hey Larry,

After you handle the request with your form - you can do $form->getData() which will return to you actual data whether it's an entity, data object or array (depend on the data_class value in your form type). But you even don't need to call this getData() method if you pass object as the second argument to the createForm() (I see you do in your example). So, if you pass your object as the 2nd argument as $this->createForm(addModelFormType::class, $addModelObject), then later, when you call form->handleRequest($request), this object will contain all the sent data. Try to dump() this object to see the difference before and after handleRequest().

Cheers!

Reply
Default user avatar

Hi Victor,

I just found out that middle man object is call data transfer object. After thinking on the problem for a few days, I don't even think the form can handles it.

To add a model, I only need an image id from the database. In the view I'll have a drop down selection for choosing a character, then a block for model-part to select a part-name and enter a color value. Since the number of model-parts depends on the image, I can't pre-populate the blocks from the form. I would need to write some javascript and ajax call to add model-part blocks on requests.

This leads me to question what is the position of the form in this situation. The form is expecting the number of fields it gets from the post and number of fields it populate to the view to be the same. But in my case there're definitely not going to be the same.

Reply

Hey Larry,

Answering your question, the form expects the same number of fields to be submitted as it was rendered. Otherwise you'll get "Extra fields added..." error. So if you modified the form with JS, I mean added/removed some fields to it - the form will be invalid. You can allow adding/removing fields for collections, see allow_add/allow_remove options: https://symfony.com/doc/cur... .

I hope this helps.

Cheers!

Reply
Default user avatar

Hi, what if you needed to add a new column to Genus, and therefore run a new migration.
Would it then create also the Notes column?

Reply
Cat in space

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

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": ">=5.5.9",
        "symfony/symfony": "3.1.*", // v3.1.4
        "doctrine/orm": "^2.5", // v2.7.2
        "doctrine/doctrine-bundle": "^1.6", // 1.6.4
        "doctrine/doctrine-cache-bundle": "^1.2", // 1.3.0
        "symfony/swiftmailer-bundle": "^2.3", // v2.3.11
        "symfony/monolog-bundle": "^2.8", // 2.11.1
        "symfony/polyfill-apcu": "^1.0", // v1.2.0
        "sensio/distribution-bundle": "^5.0", // v5.0.22
        "sensio/framework-extra-bundle": "^3.0.2", // v3.0.16
        "incenteev/composer-parameter-handler": "^2.0", // v2.1.2
        "composer/package-versions-deprecated": "^1.11", // 1.11.99
        "knplabs/knp-markdown-bundle": "^1.4", // 1.4.2
        "doctrine/doctrine-migrations-bundle": "^1.1" // 1.1.1
    },
    "require-dev": {
        "sensio/generator-bundle": "^3.0", // v3.0.7
        "symfony/phpunit-bridge": "^3.0", // v3.1.3
        "nelmio/alice": "^2.1", // 2.1.4
        "doctrine/doctrine-fixtures-bundle": "^2.3" // 2.3.0
    }
}
userVoice