Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Fetching Items from a ManyToMany Collection

Keep on Learning!

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

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

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

Login Subscribe

New mission! On the genus show page, I want to list all of the users that are studying this Genus. If you think about the database - which I told you NOT to do, but ignore me for a second - then we want to query for all users that appear in the genus_scientist join table for this Genus.

Well, it turns out this query happens automagically, and the matching users are set into the $genusScientists property. Yea, Doctrine just does it! All we need to do is expose this private property with a getter: public function, getGenusScientists(), then return $this->genusScientists:

... lines 1 - 14
class Genus
{
... lines 17 - 183
public function getGenusScientists()
{
return $this->genusScientists;
}
}

Now, open up the show.html.twig genus template and go straight to the bottom of the list. Let's add a header called Lead Scientists. Next, add a list-group, then start looping over the related users. What I mean is: for genusScientist in genus.genusScientists, then endfor:

... lines 1 - 4
{% block body %}
... lines 6 - 7
<div class="sea-creature-container">
<div class="genus-photo"></div>
<div class="genus-details">
<dl class="genus-details-list">
... lines 12 - 20
<dt>Lead Scientists</dt>
<dd>
<ul class="list-group">
{% for genusScientist in genus.genusScientists %}
... lines 25 - 31
{% endfor %}
</ul>
</dd>
</dl>
</div>
</div>
<div id="js-notes-wrapper"></div>
{% endblock %}
... lines 40 - 57

The genusScientist variable will be a User object, because genusScientists is an array of users. In fact, let's advertise that above the getGenusScientists() method by adding @return ArrayCollection|User[]:

... lines 1 - 14
class Genus
{
... lines 17 - 183
/**
* @return ArrayCollection|User[]
*/
public function getGenusScientists()
{
return $this->genusScientists;
}
}

We know this technically returns an ArrayCollection, but we also know that if we loop over this, each item will be a User object. By adding the |User[], our editor will give us auto-completion when looping. And that, is pretty awesome.

Inside the loop, add an li with some styling:

... lines 1 - 4
{% block body %}
... lines 6 - 7
<div class="sea-creature-container">
<div class="genus-photo"></div>
<div class="genus-details">
<dl class="genus-details-list">
... lines 12 - 20
<dt>Lead Scientists</dt>
<dd>
<ul class="list-group">
{% for genusScientist in genus.genusScientists %}
<li class="list-group-item">
... lines 26 - 30
</li>
{% endfor %}
</ul>
</dd>
</dl>
</div>
</div>
<div id="js-notes-wrapper"></div>
{% endblock %}
... lines 40 - 57

Then add a link. Why a link? Because before this course, I created a handy-dandy user show page:

... lines 1 - 4
use AppBundle\Entity\User;
... lines 6 - 11
class UserController extends Controller
{
... lines 14 - 44
/**
* @Route("/users/{id}", name="user_show")
*/
public function showAction(User $user)
{
return $this->render('user/show.html.twig', array(
'user' => $user
));
}
... lines 54 - 79
}

Copy the user_show route name, then use path(), paste the route, and pass it an id set to genusScientist.id, which we know is a User object. Then, genusScientist.fullName:

... lines 1 - 4
{% block body %}
... lines 6 - 7
<div class="sea-creature-container">
<div class="genus-photo"></div>
<div class="genus-details">
<dl class="genus-details-list">
... lines 12 - 20
<dt>Lead Scientists</dt>
<dd>
<ul class="list-group">
{% for genusScientist in genus.genusScientists %}
<li class="list-group-item">
<a href="{{ path('user_show', {
'id': genusScientist.id
}) }}">
{{ genusScientist.fullName }}
</a>
</li>
{% endfor %}
</ul>
</dd>
</dl>
</div>
</div>
<div id="js-notes-wrapper"></div>
{% endblock %}
... lines 40 - 57

Why fullName? If you look in the User class, I added a method called getFullName(), which puts the firstName and lastName together:

... lines 1 - 15
class User implements UserInterface
{
... lines 18 - 197
public function getFullName()
{
return trim($this->getFirstName().' '.$this->getLastName());
}
}

It's really not that fancy.

Time for a test drive! When we refresh, we get the header, but this Genus doesn't have any scientists. Go back to /genus/new to create a more interesting Genus. Click the link to view it. Boom! How many queries did we need to write to make this work? None! That's right - we are keeping lazy.

But now, click to go check out the user show page. What if we want to do the same thing here? How can we list all of the genuses that are studied by this User? Time to setup the inverse side of this relationship!

Leave a comment!

24
Login or Register to join the conversation
Default user avatar
Default user avatar Marek Burda | posted 5 years ago

Hello. i Need a little help. I have really long loading of my symfony webpage on localhost. FIrewall probably taking the most as u can see on screenshort. Is there someone who probably know how to fix it ? I tryind all thinks from internet but nothing works its taking like 6 seconds to load 1 page and thats bad if u want to test somehing new :) http://imgur.com/a/8X0YO
My controller : https://pastebin.com/1xcSeLjw

Please help me :) Thanks

1 Reply

Hey Marek Burda

Does it happens all the time ? When you clear the cache, the first time you load a page it takes longer because Symfony needs to setup a few things, but after that it should be fast

Reply
Default user avatar

Yes it happening all the time. I tryid a lot of thigs.. restarting server, changim some things in services or parameters a lot of things from tutorials and forums on internet. But nothing change. Still loading page 6-10 seconds.

Reply

Hmm, it is a weird behaviour, in which enviroment are you running, windows, mac, linux ? Try to monitor your resources (htop command if you are on Linux), maybe something is leaking memory

Reply
Default user avatar

Iam on windows :/ Every other pages are working normaly. Btw i think a projekt which i make before this one, working normaly and i also used same codes and db connections as here...

Reply
Default user avatar

Okey a fixed it. Problem was in loading javascript and foundation scripts from nonexists files. (I have 1 where is exists and 1 who dont exist) so cose was looking for files who dont exist. But thanks :)

1 Reply

I'm glad to hear you could fix your problem :)

Reply
Default user avatar
Default user avatar carles bañuls | posted 2 years ago

Hey, I've been looking what you did here, but I applied to my own project and didn't work.. I don't know what I am doing wrong.... I have posted the question in stackOverflow but seems that nobody is answering, I need help :( https://stackoverflow.com/q...

Reply

Hey Carles,

I see your question on SO was answered correctly, I'm glad you were able to solve this.

Cheers!

Reply
Default user avatar
Default user avatar Abesse Smahi | posted 5 years ago

Hi,
The download button on this page https://knpuniversity.com/s... is serving the wrong video file, it's serving the EP07 instead of EP06

Reply

Hey Abesse,

Oh, I see what you mean here. Actually, if you look closer - it's the same correct video about "Fetching Items from a ManyToMany Collection", but it just has a wrong numbering :)

We'll fix numbering soon, thanks for this report!

Cheers!

Reply
Default user avatar
Default user avatar Abesse Smahi | Victor | posted 5 years ago

Thank you.

Reply
Default user avatar
Default user avatar Dan Costinel | posted 5 years ago | edited

Hi.
I have the following ManyToMany relationship:

Posts - Tags.



# AppBundle/Entity/Post
    /**
     * @ORM\ManyToMany(targetEntity="Tag", inversedBy="posts")
     * @ORM\JoinTable(name="post_tag")
     */
    private $tags;

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

    public function addTags(Tag $tag)
    {
    if($this->tags->contains($tag)){ return; }
    $this->tags[] = $tag;
    }

    /**
     * @return ArrayCollection|Tag[]
     */
     public function getTags() { return $this->tags; }

     /**
      * @param ArrayCollection $tags
      * @return Post
      */
      public function setTags($tags)
      {
    $this->tags = $tags;
    return $this;
      }

# AppBundle/Entity/Tag

/**

 * @ORM\ManyToMany(targetEntity="Post", mappedBy="tags")
 * @ORM\JoinTable(name="post_tag")
 */
private $posts;

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

public function addPosts(Post $post)

{
    if($this->posts->contains($post)){
        return;
    }
    $this->posts[] = $post;
}

/**

 * @return ArrayCollection|Post[]
 */
 public function getPosts() { return $this->posts; }

 /**
  * @param ArrayCollection $posts
  */
 public function setPosts($posts)
 {
$this->posts = $posts;
return $this;
 }



I have a CRUD controller for my Post entity, generated with $ php bin/console doctrine:generate:crud

I have a PostType form, generated with $ php bin/console doctrine:generate:form AppBundle:Post

All I need is to be able to attache, for each new post that I create, a list of corresponding tags. I want to add the tags, by using select2 jquery plugin. ( https://select2.github.io/ ).

My problem is that I can't figure out a way to get all the tags available in the database, and put them in a select html element, in the /post/new template. Or better I should say that I implemented a way, but I have the feeling it's not the right way to do it, as when I click the "Create" button, to create a new post, my browser freezes (firefox), chrome is behaving almost the same, when I try to print_r($form->getData()).



# AppBundle/Form/TagType

$builder->add('name');

# AppBundle/Form/PostType

$builder->add(

$builder->create('tags', FormType::class, ['by_reference'=>false]) ->add(

'tag',EntityType::class,[

                    'class' => 'AppBundle:Tag',
                    'placeholder' => ' ',
                    'query_builder' => function (EntityRepository $er) {
                        return $er->createQueryBuilder('t')
                            ->orderBy('t.name', 'ASC');
                    },
                    'choice_label' => 'name',
                ])
        );

# app/Resources/views/post/new.html.twig

<div class="form-group">

            {{ form_label(form.tags.tag) }}
            {{ form_errors(form.tags.tag) }}
            {{ form_widget(form.tags.tag, { attr: { class: 'form-control select2-multi', title:'Choose a Tag', multiple:'multiple' } }) }}
        </div>

# AppBundle/Controller/PostController

/**

 * Creates a new Post entity.
 *
 * @Route("/new", name="post_new")
 * @Method({"GET", "POST"})
 */
public function newAction(Request $request)
{

    $post = new Post();
    $form = $this->createForm('AppBundle\Form\PostType', $post);
    $form->handleRequest($request);
    if ($form->isSubmitted() && $form->isValid()) {
        $tag = $post->getTags();
        echo var_dump($tag);die;

}
}




In `PostController` I need to get all the tags sent through the /post/new form, explode them, and then add them to the $post object, then save the $post object to the database.

Right now, as I already said, if I run this in the browser, I get a HUGE representation of the form

What I'm doing wrong?

Here's a part of the output: http://imgur.com/t6TXo9Q

Sorry for this long post, but I have no more solutions ,and thanks for any tip.
Reply

Hey Dan!

Ok! A few things:

1) If you can, install XDebug. Or, instead of using print_r, use dump(). What's happening is that when you print_r the tags, you're expecting an array of Tag objects. But in fact, it's an ArrayCollection of Tag objects... and also that ArrayCollection contains a reference to the database connection... and a ton of other stuff. The tl;dr is that if you try to print_r certain objects, they will print forever and ever... and kill your browser :). If you install XDebug or (better) use Symfony's dump(), it avoids this.

2) Basically, you're building your "select" element correctly, by using the EntityType for AppBundle:Tag Though, you can simplify your builder code (I don't know if this is the problem, but it looks nicer):


# AppBundle/Form/PostType
$builder->add( 'tags', EntityType::class, [
    'class' => 'AppBundle:Tag',
    'placeholder' => ' ',
    'query_builder' => function (EntityRepository $er) {
        return $er->createQueryBuilder('t')
            ->orderBy('t.name', 'ASC');
    },
    'choice_label' => 'name',
    // these are important: multiple true is needed to make this a multi-select box, and allow the user
    // to select multiple tags (important, even if you replace the UI with select 2
    'multiple' => true,
    // this says to render as a select, not checkboxes
    'expanded' => false,
]);

NOTICE that I added multiple => true and expanded => false options!

Then, you should simply be printing {{ form_widget(form.tags) }}.

3) Because your PostType is bound to a Post object, and because your PostType has a tags field that uses the EntityType (and because this corresponds with the tags property that you ultimately want to update)... you basically shouldn't need to do anything in your controller. What I mean is:


/**
 * Creates a new Post entity.
 *
 * @Route("/new", name="post_new")
 * @Method({"GET", "POST"})
 */
public function newAction(Request $request)
{

    $post = new Post();
    $form = $this->createForm('AppBundle\Form\PostType', $post);
    $form->handleRequest($request);
    if ($form->isSubmitted() && $form->isValid()) {
        // $post->getTags() will already, at this point, contain the correct
        // array of Tag objects that was selected
        // you can just save the Post to Doctrine now
        $em = $this->getDoctrine()->getManager();
        $em->persist($post);
        $em->flush();
        
        // redirect
    }
}

Let me know if this helps! I would try it first without select2 (but using that shouldn't make any difference). What you have here is a pretty traditional ManyToMany form setup - it should end up looking similar to what we have here: http://knpuniversity.com/screencast/collections/entity-type-checkboxes

Cheers!

Reply
Default user avatar

Wo hoooo! You're a life saviour! It worked! Many thanks!

Reply
Default user avatar
Default user avatar jian su | posted 5 years ago | edited

Hi Guys:

how does the view know genusScientist.id is User ID not genus ID


{% for genusScientist in genus.genusScientists %}
                            <li class="list-group-item">
                                <a href="{{ path('user_show', {
                                    id : genusScientist.id
                                }) }}">
                                    {{ genusScientist.fullName }}
                                </a>
                            </li>
                        {% endfor %}

Is it because of this?


/**
     * @return ArrayCollection|User[]
     */
    public function getGenusScientists()
    {
        return $this->genusScientists;
    }

If this is the reason, then I ArrayCollection| Genus [], it would give me genus ID instead. or does it works?

Reply

Hey Jian,

If you're talking about PhpStorm autocompletion in templates - yes, it's due to that annotation: "@return ArrayCollection|User[]" for getGenusScientists(). And yes, if you change it to "@return ArrayCollection|Genus[]" (what is not a good idea because getGenusScientists returns collection of *User* not Genus) - you'll get autocompletion for Genus entity instead of User in the template. But it will be just an autocompletion, you still will have *User* object which is autocompleted as Genus object, because there's no magic, getGenusScientists() return collection of genusScientists, i.e. users according to the Doctrine mapping for Genus::$genusScientists property.

P.S. if you want to get Genus ID in that for loop, you can call it on genus variable: {{ genus.id }}.

Cheers!

Reply
Default user avatar

Thank you Victor! you are always been helpful :)

Reply
Default user avatar
Default user avatar jian su | posted 5 years ago | edited

Hi guys:

I tried to dump the user object. And I could not find any ArrayCollection inside studiedGenuses.
as you can see -elements: [] is empty
How does symfony do the magic and accessing genusStudied.name?


array:2 [▼
  "user" => User {#353 ▼
    -id: 74
    -email: "aquanaut4@example.org"
    -password: "$2y$13$XuPa.HYwBc23Dwa6FlaN3Op4f3J4kpMcdGbMb5/PELlIrgxb9elxq"
    -plainPassword: null
    -roles: []
    -isScientist: true
    -firstName: "Hector"
    -lastName: "Wisozk"
    -avatarUri: "http://lorempixel.com/100/100/abstract/?37466"
    -universityName: "Blanda-O'Conner University"
    -studiedGenuses: PersistentCollection {#393 ▼
      -snapshot: []
      -owner: User {#353}
      -association: array:17 [ …17]
      -em: EntityManager {#282 …11}
      -backRefFieldName: "genusScientists"
      -typeClass: ClassMetadata {#355 …}
      -isDirty: false
      #collection: ArrayCollection {#394 ▼
        -elements: []
      }
      #initialized: false
    }
  }
  "app" => AppVariable {#413 ▶}
]
Reply

Hey Jian!

When you haven't hydrated a relationship of an object (in this case User - StudiedGenuses), Doctrine creates and sets a "Proxy" into that property via "Reflection" by default, so when you use that property, it magically fetches and hydrates it on the fly with the proper entity object, and then, executes the method you called.
That's why you can see all those extra fields like "initialized"

I hope it helps you ;)

Have a nice day!

Reply
Default user avatar

Thank you so much, I understand it now Cheer!

Reply
Default user avatar

Hey folks,

thanks for this awesome site & your style.

Greetings from Germany

Reply

Hey Ronny,

Thanks for your warm words!

Cheers!

Reply

Haha, cheers! You rock for saying so! Cheers from Michigan, USA!

Reply
Cat in space

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

This course is built on Symfony 3, but most of the concepts apply just fine to newer versions of Symfony.

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": "^7.1.3",
        "symfony/symfony": "3.4.*", // v3.4.49
        "doctrine/orm": "^2.5", // 2.7.5
        "doctrine/doctrine-bundle": "^1.6", // 1.12.13
        "doctrine/doctrine-cache-bundle": "^1.2", // 1.4.0
        "symfony/swiftmailer-bundle": "^2.3", // v2.6.7
        "symfony/monolog-bundle": "^2.8", // v2.12.1
        "symfony/polyfill-apcu": "^1.0", // v1.23.0
        "sensio/distribution-bundle": "^5.0", // v5.0.25
        "sensio/framework-extra-bundle": "^3.0.2", // v3.0.29
        "incenteev/composer-parameter-handler": "^2.0", // v2.1.4
        "composer/package-versions-deprecated": "^1.11", // 1.11.99.4
        "knplabs/knp-markdown-bundle": "^1.4", // 1.9.0
        "doctrine/doctrine-migrations-bundle": "^1.1", // v1.3.2
        "stof/doctrine-extensions-bundle": "^1.2" // v1.3.0
    },
    "require-dev": {
        "sensio/generator-bundle": "^3.0", // v3.1.7
        "symfony/phpunit-bridge": "^3.0", // v3.4.47
        "nelmio/alice": "^2.1", // v2.3.6
        "doctrine/doctrine-fixtures-bundle": "^2.3" // v2.4.1
    }
}
userVoice