Chapters
-
Course Code
Subscribe to download the code!Compatible PHP versions: >=5.5.9
Subscribe to download the code!Compatible PHP versions: >=5.5.9
-
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!
OneToMany: Inverse Side of the Relation
Scroll down to the script below, click on any sentence (including terminal blocks) to jump to that spot in the video!
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.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeWe 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:
Show Lines
|
// ... lines 1 - 11 |
class Genus | |
{ | |
Show Lines
|
// ... lines 14 - 48 |
private $notes; | |
Show Lines
|
// ... 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:
Show Lines
|
// ... lines 1 - 11 |
class Genus | |
{ | |
Show Lines
|
// ... lines 14 - 45 |
/** | |
* @ORM\OneToMany(targetEntity="GenusNote", mappedBy="genus") | |
*/ | |
private $notes; | |
Show Lines
|
// ... 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:
Show Lines
|
// ... lines 1 - 10 |
class GenusNote | |
{ | |
Show Lines
|
// ... lines 13 - 39 |
/** | |
* @ORM\ManyToOne(targetEntity="Genus", inversedBy="notes") | |
* @ORM\JoinColumn(nullable=false) | |
*/ | |
private $genus; | |
Show Lines
|
// ... 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
:
Show Lines
|
// ... lines 1 - 11 |
class Genus | |
{ | |
Show Lines
|
// ... lines 14 - 50 |
public function __construct() | |
{ | |
$this->notes = new ArrayCollection(); | |
} | |
Show Lines
|
// ... 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
:
Show Lines
|
// ... lines 1 - 11 |
class Genus | |
{ | |
Show Lines
|
// ... 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)
:
Show Lines
|
// ... lines 1 - 12 |
class GenusController extends Controller | |
{ | |
Show Lines
|
// ... lines 15 - 94 |
public function getNotesAction(Genus $genus) | |
{ | |
foreach ($genus->getNotes() as $note) { | |
dump($note); | |
} | |
Show Lines
|
// ... 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.
51 Comments
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!
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...
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.
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!
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
}
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
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
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.
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!
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
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!
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!
Nevermind... its the first thing you say in the video! Deleting this post!
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?
Seems to me the line should be more like:
$this->notes = new ArrayCollection($this->Genus);
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!
No... I highly appreciate the detailed explanation. Thanks much!
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 ?
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!
YES, I get it. thank you for your explanations.
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 :)
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!
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.
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!
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
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!
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!
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.
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!
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?
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!
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.
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!
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.
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!
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 :)
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!
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! :)
Ah, Eureka indeed! Of course it would be caching ;). Good debugging and have a great week yourself!
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
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.
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
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?
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!
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?
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!
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.
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!
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?
"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
}
}
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?