Chapters
-
Course Code
Subscribe to download the code!Compatible PHP versions: >=5.3.3
Subscribe to download the code!Compatible PHP versions: >=5.3.3
-
This Video
Subscribe to download the video!
Subscribe to download the video!
-
Course Script
Subscribe to download the script!
Subscribe to download the script!
ManyToOne Doctrine Relationships
Scroll down to the script below, click on any sentence (including terminal blocks) to jump to that spot in the video!
ManyToOne Doctrine Relationships¶
Right now, if I creat an Event, there’s no database link back to my user. We don’t know which user created each Event.
To fix this, we need to create a OneToMany relationship from User to Event. In the database, this will mean a user_id foreign key column on the yoda_event table.
In Doctrine, relationships are handled by creating links between objects. Start by creating an owner property inside Event:
// src/Yoda/EventBundle/Entity/Event.php
// ...
class Event
{
// ...
protected $owner;
}
For normal fields, we use the @ORM\Column annotation. But for relationships, we use @ORM\ManyToOne, @ORM\ManyToMany or @ORM\OneToMany. This is a ManyToOne relationship because many events may have the same one User. I’ll talk about the other 2 relationships later (OneToMany, ManyToMany).
Add the @ORM\ManyToOne relationship and pass in the entity that forms the other side:
// src/Yoda/EventBundle/Entity/Event.php
// ...
/**
* @ORM\ManyToOne(targetEntity="Yoda\UserBundle\Entity\User")
*/
protected $owner;
Next, create the getter and setter for the the new property:
// src/Yoda/EventBundle/Entity/Event.php
// ...
use Yoda\UserBundle\Entity\User;
class Event
{
// ...
public function getOwner()
{
return $this->owner;
}
public function setOwner(User $owner)
{
$this->owner = $owner;
}
}
Notice that when we call setOwner, we’ll pass it an actual User object, not the id of a user. But when you save an Event, Doctrine will use the owner’s id value to populate an owner_id column on the yoda_event table. So we link objects to objects in PHP, and Doctrine takes care of setting the foreign key id value for us. If you’re newer to an ORM, this is one of the toughest things to understand about Doctrine.
Updating the Database¶
How can we update our database with the new column and foreign key? Why, with the doctrine:schema:update command of course! I’ll dump the SQL to the terminal first to see it:
php app/console doctrine:schema:update --dump-sql
php app/console doctrine:schema:update --force
As expected, the SQL that’s generated will add a new owner_id field to yoda_event along with the foreign key constraint.
ManyToOne Options¶
Since I’m feeling fancy, let’s configure a few things. Whenever you have a ManyToOne annotation, you can optionally add an @ORM\JoinColumn annotation to control some database options.
JoinColumn onDelete¶
To add a database-level “ON DELETE” cascade behavior, add the onDelete option:
// src/Yoda/EventBundle/Entity/Event.php
// ...
/**
* @ORM\ManyToOne(targetEntity="Yoda\UserBundle\Entity\User")
* @ORM\JoinColumn(onDelete="CASCADE")
*/
protected $owner;
Now, let’s run the doctrine:schema:update command again:
php app/console doctrine:schema:update --dump-sql
php app/console doctrine:schema:update --force
The SQL tells us that this actually re-creates the foreign key with the “on delete” behavior. So if we delete a User, the database will automatically delete all rows in the yoda_event table that link to that user and ship them off into hyper space.
The cascade Option¶
Another common option is cascade on the actual ManyToOne part:
// src/Yoda/EventBundle/Entity/Event.php
// ...
/**
* @ORM\ManyToOne(targetEntity="Yoda\UserBundle\Entity\User", cascade={"remove"})
* @ORM\JoinColumn(onDelete="CASCADE")
*/
protected $owner;
This is like onDelete, but in the opposite direction. With this, if we delete an Event, it will cascade the remove onto the owner. In other words, If I delete an Event, it will also delete the User who is the owner.
Run doctrine:schema:update again:
php app/console doctrine:schema:update --dump-sql
Now, it doesn’t want to change our database at all. Unlike onDelete, this behavior is enforced entirely by Doctrine in PHP, not in the database layer.
Tip
You can also cascade persist, which is useful at times with ManyToMany relationship where you’re creating new items in the relationship.
Remove the cascade option because it’s dangerous in our situation:
// src/Yoda/EventBundle/Entity/Event.php
// ...
/**
* @ORM\ManyToOne(targetEntity="Yoda\UserBundle\Entity\User")
* @ORM\JoinColumn(onDelete="CASCADE")
*/
protected $owner;
If we delete an Event, we definitely don’t want that to delete the Event’s owner. Darth would be so angry.
Linking an Event to its owner on creation¶
Time to put our shiny relationship to the test. When a new Event object is created, let’s associate it with the User object for whoever is logged in:
// src/Yoda/EventBundle/Controller/EventController.php
// ...
public function createAction(Request $request)
{
// ...
if ($form->isValid()) {
$user = $this->getUser();
// ...
}
}
To complete the link, just call setOwner on the Event and pass in the whole User object:
// src/Yoda/EventBundle/Controller/EventController.php
// ...
public function createAction(Request $request)
{
// ...
if ($form->isValid()) {
$user = $this->getUser();
$entity->setOwner($user);
// ... the existing save logic
}
}
Yep, that’s it. When we save the Event, Doctrine will automatically grab the id of the User object and place it on the owner_id field.
Time to test! Login as Wayne. Remember, he has ROLE_ADMIN, which also means he has ROLE_EVENT_CREATE because of the role_hierarchy section in security.yml.
Now, fill in some basic data and submit it. To see the result, use the query tool to list the events:
php app/console doctrine:query:sql "SELECT * FROM yoda_event"
Sure enough, our newest event is linked back to our user! #Winning
22 Comments
Hi Dung L.!
Oh, boo - I think those links stopped working for some reason :/ - I'm particularly interested in what all the queries are that you did not expect? If I remember correctly, setting onDelete="CASCADE" to a relation property will require a few queries - it needs to drop the foreign key then re-add it with the constraint. But if you're seeing a lot more queries, that could be something weird.
Cheers!
Good Morning weaverryan I have updated new links, can you look at it again?
Best regards,
Dung L. I think something is not perfectly right with my database designing, I am digging will update, it is truly weird as you said :)
Still I can not find out why I get all these unnecessary MYSQL CHANGE queries https://uofc-my.sharepoint.... the only thing I can think of is because I built the database first then generated entities using existing databasev https://symfony.com/doc/4.4... but since I took this course and learnt and adopted from these tutorials I started to create the database from entities. Hence problem?
Without making any changes to the entities when I run "php bin/console make:migration" I always get these CHANGE statements in a newd migration version and after manually executed these statements within the database, I still did not see what changes were made :).
Hey Dung L. !
Thanks for re-posting the links! I'm still having trouble loading them (it just loads forever?) but I think I might know what's going on here. And, you are definitely not doing anything wrong :). Exactly what database are you using? And what version - e.g. MySQL 5.7? Maria DB 10.2.21?
If you look in your config/packages/doctrine.yaml
file, you should see a server_version
config key - like https://symfonycasts.com/screencast/symfony-doctrine/install#codeblock-7f348c163d - this is meant to be set to your server version - e.g. 5.7 or something like mariadb-10.2.12
if you're using Mariadb. If you do not set this correctly, probably everything will work ok... but maybe not ;). Doctrine uses this information to know which features your database engine supports or doesn't support. It then generates different SQL queries based on it. In some cases, setting the wrong server_version can lead to the exact issue you're describing. Doctrine thinks the database engine is out of date, but it's not. Check this, and let me know.
If I'm wrong, try posting the photos to something like imgur - I use them a lot for random screenshot stuff :).
Cheers!
Good news weaverryan ,
You are right :), the change can be written in 2 value formats
server_version: 'mariadb-10.3.16'
or
server_version: '10.3.16-mariadb'
Doctrine understands both (clever).
In addition, I also had to fix / change
FROM:
/**
* @var int|null
*
* @ORM\Column(name="num", type="integer", nullable=true, options={"default"="NULL"})
*/
private $num = NULL;
TO:
/**
* @var int|null
*
* @ORM\Column(name="num", type="integer", nullable=true, options={"default"=NULL})
*/
private $num = NULL;
Notice that: "NULL" in double quotes is wrong, there should not be quotes. I think I got this auto generated syntax from generating entities from existing database.
All is good now :), In the future, I will not use generate entities from database and I will not use MariaDB. Thank you for your support!
For the very first time ever I got this feeling good message
The database schema and the application mapping information are already in sync.
Hey Dung L.
I'm glad to know that you could fix your problem, and about auto generated entities, I don't think there is anything wrong using them but you should check them to assure they got generated correctly
Cheers!
Absolutely agree, they are great when you started out with an existing database rather than having to write entities - which can be time consuming especially for a big database, instead we can just use this tool as in my case. But we should say that this Reverse Engineering tool is not perfect that we need to clean up such as "NUL" or NULL without double quotes - this job however is very minimal comparing to writing all entities but one must be aware and clean up syntax a bit after the entities are auto generated. Thanks for your opinion MolloKhan
NP! my pleasure :)
Hi @weaverryan I reposted the links sorry, correct! that onDelete="CASCADE" will require 2 - drop and add queries as seen in tutorial. I believe this is the case of something being weird (I built the database first when started out project but now I have learnt and used doctrine annotations in entities to modify the database) is this what causes weird extra no harm queries now?
the database still always works using make migration and migrate, it is just extra queries always there that do no harm that bother me to ask for your advice?
Thank you for your time.
Hi Ryan,
I am working with oneToMany bidirectional Entity and I am having problem saving the data
I have an entity called survey and I have an entity called Creditcards
Creditcard is linked with survey using manyToOne
survey is linked with Creditcard using oneToMany
The survey has bunch of financial related questions which gets saved in survey table and it also asks user for their creditcard related details which then gets saved in Creditcard table, since user can have more than 1 card so they can add multiple card details and this is where the manyToOne andoneToMany comes into play.
There are two errors that I run into and I just cant seem to get past them, its probably my lack of Symfony knowledge,
The first error I see is
A new entity was found through the relationship 'ExampleBundle\Entity\survey#creditcards' that was not configured to cascade persist operations for entity: ExampleBundle\Entity\Creditcards@000000001bbd76da000000008bbf1e28. 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 'ExampleBundle\Entity\Creditcards#__toString()' to get a clue.
500 Internal Server Error - ORMInvalidArgumentException
So while trying to fix this I then run into
The class 'Doctrine\ORM\PersistentCollection' was not found in the chain configured namespaces ExampleBundle\Entity, UserBundle\Entity
At this stage I am pretty cluelss what to do and I am hoping if you can help me figure this out.
This the code is too long, rather than pasting it here I have created a gist.
This is my Creditcard.orm.yml
https://gist.github.com/shairyar/48e0b76a5c590342a288#file-creditcards-orm-yml
This is my This is my Creditcard Entity
https://gist.github.com/shairyar/48e0b76a5c590342a288#file-creditcards-php
This is my survey.orm.yml
https://gist.github.com/shairyar/48e0b76a5c590342a288#file-survey-orm-yml
This is my survey Entity
https://gist.github.com/shairyar/48e0b76a5c590342a288#file-survey-php
The Form that gets called in controller and passed to the twig
https://gist.github.com/shairyar/48e0b76a5c590342a288#file-debttype-php
Finally my debtAction that processes the form and tries to save the data
https://gist.github.com/shairyar/48e0b76a5c590342a288#file-defaultcontroller-php
I have gone through the following step by step article but I just cant get the save to work
http://symfony.com/doc/current/cookbook/form/form_collections.html
I will really appreciate if you can help me understand what am i doing wrong?
Hey Shairyar!
Hmm, I can see a few things initially. First, what's the purpose of all the manual data setting here? https://gist.github.com/shairyar/48e0b76a5c590342a288#file-defaultcontroller-php-L19. I think this is at the root of your problem, specifically the line where you set the creditCards field: https://gist.github.com/shairyar/48e0b76a5c590342a288#file-defaultcontroller-php-L23.
When you have a collectionType, it looks for an addCreditCard method and calls that for all the new credit cards added. You do have this method, and so it's being called. But then you're immediately re-setting this whole property in the controller, but you're now skipping the addCreditCard method by saying getCreditCards()->add(...);. That may or may not be part if the problem, but I'd get rid of that: you do want to use your adder* function :).
The real problem is that the "owning" side (a concept I talk about about here https://knpuniversity.com/screencast/symfony2-ep3/doctrine-inverse-relation#owning-versus-inverse-side - because it is tricky) if your relationship is never set, which directly leads to your first error. Your "adder" function would need to look like this:
public function addCreditCard(CreditCard $card)
{
if (!$this->creditcards->contains($card)) {
$this->creditcards->add($card);
}
// THIS IS THE KEY PART
$card->setSurvey($this);
}
That sets the owning side, and is likely the key to your issue. This stuff is hard: only setting the "owning" side (e.g. CreditCard::setSurvey) does anything for saving to Doctrine.
Good luck!
Wow, that did fix the problem, I did not know when collectionType is used we do not need to worry about getting the linked data saved, thats great to know.
I have couple of queries to get the concept straight
1)
The relationship I am using here between Survey and Creditcard is OneToMany bidirectional, is this is correct relationship? A survey can have multiple cards linked to it, but the cards cannot have multiple surveys they can be linked with only one survey. I dont think ManyToMany relationship goes here, correct?
2)
How will the delete work here if I need to delete one card? will that be linked with an action with get parameter card id and based on the id run the delete query or Symfony has a shortcut to that?
3)
My survey.orm.yml where I have defined OneToMany relationship, I had to use cascade: ["persist"], what is the purpose of this?
You asked me the reason for manual data setting, its actually an extremely long survey which is broken down is different forms, i was doing the manual data setting as i did not want one section to overwrite the data of other section while saving and now i realize i do not need to do that as symfony will do it for me automatically and all I need to do is $em->flush(); i have no idea why i did that :) may be i was going crazy making the relationship work and save data :)
Coming across this website is such a blessing :)
Hey again!
1) Yep, I agree with the OneToMany because cards cannot have multiple surveys.
2) The easiest way to delete a card is to do it just like you said: have an action, with the {id} of the card in the URL, and delete it that way. The "collection" form type also has an "allow_delete" option, but it can be a bit tricky, as you need to make sure you "unlink" the relationship in the removeXX method and also probably need to use the orphanRemoval option so that Doctrine fully deletes the removed Creditcards (not just unlink them from the Survey). Your way is much easier.
3) We talk about this a little bit here: http://knpuniversity.com/sc... - I rarely use the cascade option. Basically, it says "If I save a Survey, automatically call $em->persist() on all of the related Creditcards entities linked to it". In your case, in your controller, you're never looping over $survey->getCreditcards() and calling $em->persist() on each, so without the cascade, Doctrine says "You told me to persist this Survey, and this survey is related to these 2 Creditcards, but you did not tell me to persist them. What the heck?". I usually call persist() manually on all of my entities, to avoid any wtf issues later. But, this is probably the best situation for cascade persist. I would still probably remove it and manually loop over the linked creditCards in your controller and persist them.
Thanks for the nice comment - I love it!
Cheers!
Thanks Ryan, Really appreciate the help.....
When I try this tutorial, I get an error about "A new entity was found through the relationship". I finally tracked down the cause: the $user-object from $this->getUser() is reconstructed from the security.context (unserialized from the token). The entity manager does not know about this object and thus fails.
I tried various ways (e.g. $em->merge() instead of $em->persist, or adding cascade=PERSIST to the relationship), but none worked, partly because (a) as part of a former episode we don't serialize the complete $user-object - and even if we did (I tried it) - we would create another user of the same name & email address.
Conclusio: The entity manager does not know about the $user-object which is unserialized from the token. Instead I modified the code like this:
$user = $this->getUser(); // user reconstructed from token
$em = $this->getDoctrine()->getManager();
$user = $em->getRepository('UserBundle:User')->find($user->getId());
This way we get the complete user object (e.g. including isActive & email address). The entity manager knows about it and persisting the event object finally works.
Versions:
doctrine/common: v2.4.2
symfony/symfony: v2.5.4
Doh! Had an error in the UserRepository:refreshUser() function: I was returning the old $user not the new $refreshedUser obect.
Well, at least I learned something about the inner workings of Symfony & Doctrine while debugging this :o) With this fix my "solution" from the previous comment is no longer necessary (basically it is what refreshUser() does).
How did you automatically create the getter and setter in your text editor? Thanks!
In PhpStorm, goto the "Code" menu on top and then select "Generate". I'm using the Mac shortcut for this - which is cmd+n.
Yea, having this is HUGE for productivity. We're releasing a screencast all about being super fast in PhpStorm - http://knpuniversity.com/sc...
Cheers!
Hey Ryan, anything to consider when trying to set a relation on the same table (like a Person has a mother and father in the same Person table... that would mean a field User with ManyToOne and a field User with OneToMany. Is it just that or is it a little bit more difficult?
Hey Hermen!
If you will have a *Person Class* with two fields, Father and Mother, you will need a *ManyToOne* association in both fields, because a father or a mother might have more than one son
You can read more information about relationships here:
http://docs.doctrine-projec...
Have a nice day!
"Houston: no signs of life"
Start the conversation!
What PHP libraries does this tutorial use?
// composer.json
{
"require": {
"php": ">=5.3.3",
"symfony/symfony": "~2.4", // v2.4.2
"doctrine/orm": "~2.2,>=2.2.3", // v2.4.2
"doctrine/doctrine-bundle": "~1.2", // v1.2.0
"twig/extensions": "~1.0", // v1.0.1
"symfony/assetic-bundle": "~2.3", // v2.3.0
"symfony/swiftmailer-bundle": "~2.3", // v2.3.5
"symfony/monolog-bundle": "~2.4", // v2.5.0
"sensio/distribution-bundle": "~2.3", // v2.3.4
"sensio/framework-extra-bundle": "~3.0", // v3.0.0
"sensio/generator-bundle": "~2.3", // v2.3.4
"incenteev/composer-parameter-handler": "~2.0", // v2.1.0
"doctrine/doctrine-fixtures-bundle": "~2.2.0", // v2.2.0
"ircmaxell/password-compat": "~1.0.3", // 1.0.3
"phpunit/phpunit": "~4.1", // 4.1.0
"stof/doctrine-extensions-bundle": "~1.1.0" // v1.1.0
}
}
Hello SymfonyCasts,
***Re-posted links only***
I am following this tutorial chapter 02 as seen here https://uofc-my.sharepoint.... , and add
to a property as seen here https://uofc-my.sharepoint.... however when I ran the command
I will get a lot more queries that I think I should NOT get and all of these queries are
as seen here https://uofc-my.sharepoint....
Could you please help point out why this is? Thank you all!