Buy

Order By with a OneToMany

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

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

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

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

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

  • 2016-11-23 Victor Bocharsky

    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!

  • 2016-11-23 zied haj salah

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

  • 2016-11-23 Victor Bocharsky

    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!

  • 2016-11-22 zied haj salah

    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?

  • 2016-11-18 Victor Bocharsky

    Hey somecallmetim27 ,

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

    Cheers!

  • 2016-11-17 somecallmetim27

    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.

  • 2016-11-07 weaverryan

    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!

  • 2016-11-07 Terry Caliendo

    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).

  • 2016-11-05 weaverryan

    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, *that*s 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!

  • 2016-11-05 Terry Caliendo

    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).

  • 2016-11-04 weaverryan

    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/scre... 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!

  • 2016-11-04 Terry Caliendo

    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).

  • 2016-10-31 Terry Caliendo

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

  • 2016-10-30 weaverryan

    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/Dee.... 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!

  • 2016-10-29 Terry Caliendo

    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();

  • 2016-07-25 weaverryan

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

  • 2016-07-25 Aistis

    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'

  • 2016-07-16 weaverryan

    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!

  • 2016-07-15 Neal Ostrander

    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?

  • 2016-05-14 Ing Alex Vitari

    Maybe because i'm using intelliJ and not PHPStorm.

    /** @var Genus $genus */

    Works Great!
    Many Thanks

  • 2016-05-13 weaverryan

    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!

  • 2016-05-12 Ing Alex Vitari

    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

  • 2016-03-29 weaverryan

    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!

  • 2016-03-25 Andrew Grudin

    /**
    * @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?