Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Order By with a OneToMany

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

Let's finish this! Ultimately, we need to create the same $notes structure, but with the real data. Above the foreach add a new $notes variable. Inside, add a new entry to that and start populating it with id => $note->getId():

... lines 1 - 12
class GenusController extends Controller
{
... lines 15 - 94
public function getNotesAction(Genus $genus)
{
$notes = [];
foreach ($genus->getNotes() as $note) {
$notes[] = [
'id' => $note->getId(),
];
}
... lines 104 - 114
}
}

Hey! Where's my autocompletion on that method!? Check out the getNotes() method in Genus. Ah, there's no @return - so PhpStorm has no idea what that returns. Sorry PhpStorm - my bad. Add some PhpDoc with @return ArrayCollection|GenusNote[]:

... lines 1 - 11
class Genus
{
... lines 14 - 105
/**
* @return ArrayCollection|GenusNote[]
*/
public function getNotes()
{
return $this->notes;
}
}

This will autocomplete any methods from ArrayCollection and auto-complete from GenusNote if we loop over these results.

Now we get autocompletion for getId(). Next, add username => $note->getUsername() and I'll paste in the other fields: avatarUri, note and createdAt. Ok, delete that hardcoded stuff!

... lines 1 - 12
class GenusController extends Controller
{
... lines 15 - 94
public function getNotesAction(Genus $genus)
{
$notes = [];
foreach ($genus->getNotes() as $note) {
$notes[] = [
'id' => $note->getId(),
'username' => $note->getUsername(),
'avatarUri' => '/images/'.$note->getUserAvatarFilename(),
'note' => $note->getNote(),
'date' => $note->getCreatedAt()->format('M d, Y')
];
}
$data = [
'notes' => $notes
];
return new JsonResponse($data);
}
}

Deep breath: moment of truth. Refresh! Ha! There are the 15 beautiful, random notes, courtesy of the AJAX request, Alice and Faker.

Ordering the OneToMany

But wait - the order of the notes is weird: these should really be ordered from newest to oldest. That's the downside of using the $genus->getNotes() shortcut: you can't customize the query - it just happens magically in the background.

Well ok, I'm lying a little bit: you can control the order. Open up Genus and find the $notes property. Add another annotation: @ORM\OrderBy with {"createdAt"="DESC"}:

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

I know, the curly-braces and quotes look a little crazy here: just Google this if you can't remember the syntax. I do!

Ok, refresh! Hey! Newest ones on top, oldest ones on the bottom. So we do have some control. But if you need to go further - like only returning the GenusNotes that are less than 30 days old - you'll need to do a little bit more work.

Leave a comment!

42
Login or Register to join the conversation
Default user avatar
Default user avatar Artemio Vegas | posted 5 years ago

Hi guys, I found litle bug in your code (maybe). When you call getNotesAction, And fill the array $notes
$notes[] = [
'id' => $note->getId(), // HERE
......
];

As far as I remember, we did not create a getter for property 'id' in GenusNote Entity.
Maybe I missed something.
Cheers.

Reply

Hey Artemio!

Nice catch! we definitely forgot about adding that getter, but that's an easy fix :)

Thanks for letting us know, have a nice day!

Reply
Default user avatar
Default user avatar Artemio Vegas | MolloKhan | posted 5 years ago

Heh, Thank you for this useful and interesting course =)

Reply

Hey Artemio,

We'll fix this bug in our code soon, thanks for letting us know about it! ;)

Cheers!

Reply
Default user avatar
Default user avatar zied haj salah | posted 5 years ago

Hi every one, it seems that the notes are not ordered by date even after i applied the orderBy annotation.
how can i fix this issue?

Reply

Hey Zied,

Please, double check you have "@ORM\" prefix for this annotation, it should be "@ORM\OrderBy({"createdAt" = "DESC"})". Then ensure you has exactly "createdAt" property on GenusNote class. You also need to clear cache for prod environment, please, clear it with "./bin/console cache:clear --env=prod" and just in case do it for dev env too. Note that ordering works only for that entities which you get with "$genus->getNotes()". If you load notes from repository - you should manually order them because they won't be ordered by default.

Cheers!

Reply
Default user avatar

Thank you! Two days after I finally made this work!

Reply
Default user avatar
Default user avatar zied haj salah | Victor | posted 5 years ago

Thank you Victor,
It worked for me after clearing the cache for dev env.

Reply

Great! Actually, it's not necessary to clear the dev cache manually in most cases, but looks like it was an exclusion. Sometimes it happens even with dev environment. I always try to clear cache first in any unexpected behavior - it's the first-aid :)

Cheers!

Reply
Default user avatar

Thanks Victor. I had the same problem, clearing dev cache fixed it right up.

Reply

Hey Robdig!

Happy to know that 3 years old comment still helps :)

Cheers!

Reply
Neal O. Avatar
Neal O. Avatar Neal O. | posted 5 years ago

When I switch from the hard codes notes to using the getNotes the images no longer load. Inspecting shows the same path. I have tried clearing the cache but still not images. Inspecting the images show a resource not found error in the console any idea as to why the switch causes the images to not display?

Reply

Hey Neal!

Hmm - I have a few ideas, but let's see :). If you open your network tools in your browser and then fresh to trigger the note loading, you should see the 404 links showing up there. What do the URL's look like? It's definitely interesting that it was working with the hardcoded notes, but not with the new dynamic ones. I'm curious what the difference is between the image paths in both situations (they should be the same, but apparently not!)

Cheers!

Reply
Default user avatar

Hey, the problem was with links. In fixtures.yml you set :
userAvatarFilename: '50%? leanna.jpeg : ryan.jpeg'
But you need to set images folder.
So it should be like this:
userAvatarFilename: '50%? /images/leanna.jpeg : /images/ryan.jpeg'

Reply

Interesting! This shouldn't be the problem - I intentionally *only* store the filename in the database. Then, in getNotesAction(), when we return the avatarUri there, we prefix the filename with '/images/' (check the code-blocks on this page). Both are valid ways to handle this - you just need to make sure you have the "/images" part either in the database or somewhere else :).

Unless there was some other issue that I'm not seeing! Let me know :)

Reply

I ran into this same issue and ended up discovering that I had .jpg instead of .jpeg. I had to fix the typo AND reload the fixtures not to mention make sure I had a genus url that actually still existed to fix it.

Sounds simple, and it is, but it took me a little tinkering to figure out and then fix the problem.

Reply

Hey somecallmetim ,

Thank you for sharing your case! I think it will help someone. Simple errors are always the most dangerous ones ;)

Cheers!

Reply
Richard Avatar
Richard Avatar Richard | posted 5 years ago

Why are we having to map the collection returned by getNotes to another NEW array? We really can not just render or pass the GenusNote/ArrayCollection[] ? OK, if we cant we cant, but why cant we? The dump() shows that the field names are already the indices for each note. What am I missing?

Reply

Hey Richard ,

This is an API, and as you can notice, we transform some object into the plain text (as we do for date object - $note->getCreatedAt()->format('M d, Y'), we want to sent already formatted date) and also do some extra modification (tweaking user avatar path as '/images/'.$note->getUserAvatarFilename()). That's main reason, and actually it's a good practice to do so in API - probably, you don't want to send to your API user a heavy object which you can add to the Note entity in the future as a new relationship.

Cheers!

1 Reply
Default user avatar
Default user avatar Boran Alsaleh | posted 5 years ago

Hello Victor ,

Sorry I know this Question is not related to this tutorial , but i would like to ask it , what is the description of Junior Symfony Developer ?

Thanks .

Reply

Hey Boran,

I don't know either :) Actually, different companies may imply different responsibilities and skills, but as I see, Junior developer should be able to solve simple tasks by himself, also has a mentor who has more experience in development and can help with more complex tasks. If we're talking about Symfony, well junior dev should understand what's Composer, what's a bundle, how to install and enable it, and know what's Symfony and how to work with it without going into detail to deep. But that's just a title, so better read the full description of the job offer to know what tasks you'll need to solve and what exactly the company expects from you.

I hope this helps.

Cheers!

Reply
Default user avatar
Default user avatar Nobuyuki Fujioka | posted 5 years ago

Hi,

in my parent entity called Food entity (same as Genus entity), my annotation is like this.
/**
* @ORM\OneToMany(targetEntity="FoodNote", mappedBy="food")
*/
private $notes;

As soon as I add * @ORM\OrderBy({"CreatedAt"="DESC"}) like this,
/**
* @ORM\OneToMany(targetEntity="FoodNote", mappedBy="food")
* @ORM\OrderBy({"CreatedAt"="DESC"})
*/
private $notes;

It breaks. The error message says Unrecognized field: CreatedAt
CreatedAt property and getter and setter for it are all there in foodNote entity (same as GenusNote). So, I don't understand why it is say unrecognized field...
Am I missing something?

Here is my github for your reference.
https://github.com/nfabacus...

Your help is highly appreciated.

Kind regards

Reply

Hey Nobuyuki!

That problem can be caused by not updating your schema "bin/console doctrine:schema:update" remember to check the SQL first with --dump-sql

After a deeper look, I think your problem is in the name of the field, you have it with a capital C, it should be exactly as you have it in your class "createdAt", try making that change and let me know about it :)

Have a nice day!

Reply
Default user avatar
Default user avatar Ing Alex Vitari | posted 5 years ago

Hi Ryan,

as you do for the ArrayCollection in the function getNotes(), there is a way to do it with variables?

for example:

$genus = $em->getRepository('AppBundle:Genus')->findOneBy(['name' => $genusName]);

how can i say that the variable $genus is a Genus?

(just for autocompletion)

Many Thanks

Reply

Hey there!

Good question - because I'm obsessed with auto-completion! A few answers for you:

1) In theory, the Symfony plugin should detect that this should return a Genus object, since you're going to the GenusRepository. But, I can't actually remember if it works that way :).

2) In this situation, because you don't own the findOneBy method, you can't add phpdoc to it. But, you can add inline docs:


/** @var Genus $genus */
$genus = // ...

3) What I often do is actually create a custom method in my repository - eg. findOneByName($genusName). I do this in part to keep even more of my query logic in the repository... but also because then I can add phpdoc :).

Hope that helps!

Reply
Default user avatar
Default user avatar Ing Alex Vitari | weaverryan | posted 5 years ago

Maybe because i'm using intelliJ and not PHPStorm.

/** @var Genus $genus */

Works Great!
Many Thanks

Reply
Default user avatar

Hello!

I have noticed that my comments were already ordered from newest to oldest, even before adding the @ORM\OrderBy({"createdAt"="DESC"}) .

Has this been updated to be Default setting perhaps?

Reply
MolloKhan Avatar MolloKhan | SFCASTS | Tom | posted 5 years ago | edited

Hey Tom

Ohh, usually when you fetch results from a DB table that it's initial order has not been changed, they come out as they were inserted

Cheers!

Reply
Default user avatar
Default user avatar Yani Oz | posted 5 years ago | edited

Hi all,

I had a problem with the array collection, in the example you gave, in GenusController's getNotesAction you initialize $notes[ ]='';
In my project it does set the first value of the array to "". I had to remove $notes[ ]='';


    public function getNotesAPIAction(Genus $genus){

        foreach ($genus->getNotes() as $note) {

                $notes[] = [
                    'id' => $note->getId(),
                    'username' => $note->getUsername(),
                    'avatarUri' => '/images/' . $note->getUserAvatarFilename(),
                    'note' => $note->getNote(),
                    'date' => $note->getCreatedAt()->format('M d, Y')
                ];

        }
        
        $data = [
            'notes' => $notes
        ];
        
        return new JsonResponse($data);
Reply

Hi Yani Oz!

This may have been caused by a small typo in your code :). In the tutorial, we have $notes = [], which initializes it into an empty array. Make sure you have that instead of $notes[] = '', which would absolutely add an empty item at the beginning :). Your updated code obviously works too, but the $notes variable is never initialized (doesn't break anything, but is considered a bad practice).

Cheers!

Reply
Default user avatar

Oooooh yeah, i didn't notice ! Thanks for the help !

Cheers !

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

Say you wanted to copy a Genus and all its GenusNotes to a new set of rows in the tables. Is there an easy way to do it?

This code only copies Genus and doesn't make copies of all the subsequent GenusNotes


$NewGenus = clone $Genus;
$em->persist($NewGenus);
$em->flush();

I had to do a foreach similar to below, but was hoping Symfony would have something built-in that's quicker/easier:


        $em = $this->getDoctrine()->getManager();
        $Genus = $em->getRepository('AppBundle:Genus')->findOne(...);

        $NewGenus = clone $Genus;
        $em->persist($NewGenus);
        $em->flush();

        foreach ($Genus->getGenusNotes() as $GenusNote) {
            $NewGenusNote = clone $GenusNote;
            $NewGenusNote->setGenus($NewGenus);
            $em->persist($NewGenusNote);
            $em->flush();
        }

        $em->persist($NewGenus);
        $em->flush();
-1 Reply

Hey Terry!

There's not a good way that I know of to do this :/. Your method is probably about the right approach. Btw, it does touch on something cool about Doctrine: when you clone the Genus object, the new Genus object has the same idea. But when you save the new Genus object, Doctrine is smart enough to realize that this is *not* the same Genus object, and so does an INSERT, instead of confusing it with the original Genus. Total side thing, but it's cool :).

Oh, also, while Symfony doesn't have anything for this "deep cloning", I wondered if there was a random PHP library that might do this. And, I found this https://github.com/myclabs/.... I've never used it, but it has decent stars on GitHub and the docs look great. If you're doing a lot of this, give it a shot and let me know!

Cheers!

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

Thanks. I found that too but saw some comments that it was buggy.

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

Does Doctrine do any caching of ArrayCollections by default? I'm having some issues after doing my clone process above.

After doing the clone above, I return the "id" of the $NewGenus. When I later call the Entity Manager to retrieve that new Genus by the "id", I get the correct Genus. But when I do a foreach loop on that objects children, I get the GenusNotes that are attached to the original Genus not the new one.

Thus, I'm guessing Doctrine is somehow caching that lookup of the ArrayCollection that gets the GenusNotes when I make that 2nd call.

If I do the clone process, end the script, and then start the script again by calling the URL another time with a parameter to skip the cloning process and use that id created by the cloning process, the correct GenusNotes are returned. So there's got to be a caching issue.

Is there any way to force Doctrine to clear its cache from within the script? (I know I can do /bin/console stuff but that's not possible at runtime and probably not part of this script-side caching issue).

Reply

Hey Terry!

Hmm. So, the genusNotes property on Genus is an ArrayCollection object. In the code above, you're cloning the Genus and then cloning the GenusNote objects and setting the Genus on them. This is perfect to have Doctrine save everything correctly. But, $newGenus will still have a reference to the original ArrayCollection: when you clone, embedded objects aren't cloned, the new object just points to the original reference.

No biggie! Try this:

1) After you clone $newGenus, set a fresh ArrayCollection onto it:


$newGenus->setGenusNotes(new ArrayCollection());

2) After cloning each note, set it onto the genusNotes property, so that this inverse side of the relation is also set:


$newGenus->getGenusNotes()->add($newGenusNote);

In our upcoming (like next week) tutorial about Doctrine collections (https://knpuniversity.com/screencast/collections) we talk a lot about the two sides of each relationship and how you can add some cool code to keep them in "sync". And that's what you're doing here: making sure that when you call $newGenusNote->setGenus($newGenus), that you also make sure that the $newGenus knows that this now belongs to it. That second part isn't needed for Doctrine persistence, it's only needed if you expect to access the $genusNotes property later in this request and expect it to have your new stuff.

Let me know if that puts you in a good spot!

Cheers!

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

I did what you said and it works. Thanks much!

However, what's really interesting to me is that I'm not passing the $NewGenus back. I'm doing the cloning process in a "Clone" function and returning the "id" of the $NewGenus after the flush() call.


private function Clone($Genus){
                 .... cloning process...
                 $this->em->flush();
                  return $NewGenus->getId();
}

Then, in the function that called "Clone", I take the returned "id" and do a database query for the Entity:


$ReturnedGenusID = $this->Clone($NewGenus);
$NewGenus = $this->em->getRepository('AppBundle:Genus')->findOneBy([
            'id' => $ReturnedGenusID
   ]);

foreach ($NewGenus->getNotes() as $GenusNote){
   dump($GenusNote); // referred to the wrong $Genus before doing your recommendations above
}

So the getRepository was somehow cheating when it came to getting the ArrayCollection Entities attached to $Genus when I went to retrieve them. Your fix did fix this issue... but I really don't understand why in this case (If I passed back the $NewGenus object created in "Clone", I would understand why. But this was a brand new query outside the creating function, you'd think doctrine would start the process from scratch).

Reply

Hey Terry!

I can answer this one as well :). Doctrine internally has something called an "identity map". Basically, it keeps track of all of the entities that were either queried for or persisted during this request. In fact, when you call persist() and flush() on an entity, this is how it knows whether or not to execute an INSERT or UPDATE: it asks "Is this object in my identity map because I queried for it? Or is it not in my identity map, so it must be new and i should insert?". It doesn't, as you might expect, check if the "id" is blank to determine and INSERT/UPDATE.

Anyways, this is at the core of Doctrine and by design. If I remember correctly, the identity map is mapped by id/primary key, which means that if you try to query for a Genus with id 5 times in one request, it will query for it just once. The other 4 times, it'll say "Terry! Man, we already queried for the Genus - so I'll just give that object back to you to save time, and also to make sure we don't have 5 duplicate objects flying around". So, thats the reason :). Before my "fix", you persisted a $NewGenus that had the ArrayCollection problem that we talked about. When you re-queried for it, Doctrine just gave you the same object back that you just gave to it :).

In rare cases, this can be a problem - e.g. you query for a Genus, but then later (on the same request) you know that something else has likely changed that data directly in the database (maybe a direct SQL query) and you need to "refresh" the entity. You can do this:


$em->refresh($NewGenus);

Honestly, this only ever happens when I'm writing functional tests (e.g. I create a Genus in PHP, save it, make an HTTP request to my app, which changed that data, then I refresh() so I can make sure that the data did in fact change). This is way deeper in Doctrine than most people ever need to get, but, since you're asking and curious, awesome! More power to you to know this stuff :).

Cheers!

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

Thanks for the detailed answer. I enjoy learning all the details so your answer is highly appreciated!

So I'm concluding from your response that instead of setting an Empty ArrayCollection on each Entity and also setting the subsequent inverse side of the relation, I could have waited until the end and called the refresh on the Entity:


private function Clone($Genus){
  $em = $this->getDoctrine()->getManager();
  $Genus = $em->getRepository('AppBundle:Genus')->findOne(...);

  $NewGenus = clone $Genus;
  $em->persist($NewGenus);
  $em->flush();

  foreach ($Genus->getGenusNotes() as $GenusNote) {
      $NewGenusNote = clone $GenusNote;
      $NewGenusNote->setGenus($NewGenus);
      $em->persist($NewGenusNote);
      $em->flush();
  }

  $this->em->flush();
  // ** Refresh instead of rebuilding ArrayCollections **
  $em->refresh($NewGenus);
  return $NewGenus->getId();
}

This actually works better for me, as in my case I have a chain of sub entities (ie. for comparison $GenusNotes would have a child Entity, maybe $GenusRatings, with ManyToOne relation on each $GenusNote. And in my actual code that child $GenusRatings also has children). So it would simplify my cloning code to just call a refresh at the end.

Granted that brings up the question as to whether or not I need to refresh each child Entity. Or if just refreshing the top level will clear all the saved lookups on the descendant Entities all the way down the chain (I'm guessing not).

Reply

Hey Terry!

You're definitely diving into a deep area here with all the cloning. I *believe* you would need to refresh each child entity, but I'm actually not sure.... and it only really matters in these cases where you have a "dangling" object reference like ArrayCollection. Specifically, if you *don't* refresh the GenusNote, I can't think of what problem that would be caused: you're already explicitly setting its genus property to the new Genus. If *it* were *also* related to some other entity (e.g. GenusNote is OneToMany with GenusNotePhoto or something), then you would have the situation again (i.e. cloning would actually mean that the new GenusNote would be related to the same GenusNotePhoto objects).

So, just try it out and see what works / doesn't work. In the collection tutorial we're about to release, we spell out really clearly the importance (or non-importance) of the owning and inverse sides of a relationship... which might at least clear your thinking on this :).

Cheers!

Reply
Default user avatar
Default user avatar Andrew Grudin | posted 5 years ago

/**
* @ORM\Column(type="date")
*/
private $createdAt; in my table genus_note. created_at after alice\faker looks just like:

2015-11-13

if i code:
'date' => $note -> getCreatedAt()

and go, let's say, to http://localhost:8000/genus/Balaena/notes , i get json on date like this:

-date: {date: "2016-03-08 00:00:00.000000",
timezone_type: 3,
timezone: "Mariana Trench/Challenger Deep"
}

Could you tell me, please , who adds this 'timezone_type' and 'timezone' ?
May be 'Return new JsonResponse($data)' does?

-1 Reply

Hey Andrew!

Ah, cool! And yea, you're 100% right, this comes from JsonResponse. But more specifically, all JsonResponse does internally is call json_encode() on the $data variable that you pass it. And it turns out, if you json_encode() a DateTime object, it uses this representation. You can see it with this code:


$data = [
    'data' => new \DateTime()
];
echo json_encode($data);
// prints {"data":{"date":"2016-03-29 13:49:24.000000","timezone_type":3,"timezone":"America\/Detroit"}}

To fix that, I manually format this string before returning it in the controller - check out the 3rd code-block on this page - it sets the 'date' key in the array to `$note->getCreatedAt()->format('M d, Y') - this makes it return a string instead of a DateTime object.

I hope that helps!

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