Buy

Render another Controller in Twig

When a user sees our 404 page, I’d love it if we could show them a list of upcoming events. Hmm, but that’s not possible. Normally, I’d query for some events and then pass them into my template. But we don’t have access to Symfony’s core controller that’s rendering error404.html.twig.

Whenever you’re in a template and don’t have access to something you need, there’s a sure-fire solution: use the Twig render function. This lets you call any controller function you want and prints the results.

Create an Embedded Controller

Start by adding a new controller function that queries for upcoming events, and renders a template. So far, this feels like any other controller, except it doesn’t have a route:

// src/Yoda/EventBundle/Controller/EventController.php
// ...

public function _upcomingEventsAction()
{
    $em = $this->getDoctrine()->getManager();

    $events = $em->getRepository('EventBundle:Event')
        ->getUpcomingEvents()
    ;

    return $this->render('EventBundle:Event:_upcomingEvents.html.twig', array(
        'events' => $events,
    ));
}

Create the template and grab the event-rendering code from index.html.twig. But hey, don’t extend the base layout. This controller is meant to just render “part” of a page, not the entire HTML body. I also need to rename entities to events, since that’s how I called the variable in the controller:

{# src/Yoda/EventBundle/Resources/views/Event/_upcomingEvents.html.twig #}
{% for event in events %}
    <article>
        <header class="map-container">
            <img src="http://maps.googleapis.com/maps/api/staticmap?center={{ event.location | url_encode }}&markers=color:red%7Ccolor:red%7C{{ event.location | url_encode }}&zoom=14&size=150x150&maptype=roadmap&sensor=false" />
        </header>
        <section>
            <h3>
                <a href="{{ path('event_show', {'slug': event.slug}) }}">{{ event.name }}</a>
            </h3>

            <dl>
                <dt>where:</dt>
                <dd>{{ event.location }}</dd>

                <dt>when:</dt>
                <dd>{{ event.time | date('g:ia / l M j, Y') }}</dd>

                <dt>who:</dt>
                <dd>Todo # of people</dd>
            </dl>
        </section>
    </article>
{% endfor %}

The new controller prints just a list of events, without a layout. We didn’t give it a route, but we don’t need to: we’re going to call it straight from Twig.

Oh, and what’s up with the underscore in front of the name? That’s just a standard I follow for controllers that render partial pages.

Getting render-happy in Twig

Ok, now I’ll show you the power behind this render weapon. Remove the query in indexAction and pass nothing into the template:

// src/Yoda/EventBundle/Controller/EventController.php
// ...

/**
 * @Template()
 * @Route("/", name="event")
 */
public function indexAction()
{
    return array();
}

Next, remove the big entities for loop that we just copied from index.html.twig and replace it with the render function:

{% extends 'EventBundle::layout.html.twig' %}

{% block body %}
    <section class="events">
        {# same <header> stuff as before #}
        {# ... #}

        {{ render(controller('EventBundle:Event:_upcomingEvents')) }}
    </section>
{% endblock %}

Try out the homepage in the dev environment. Hey, it looks just like before! render calls our controller, we build a partial HTML page, and then it gets printed. This handy function is great for re-using page chunks and is also key to using Symfony’s Caching.

Tip

If you just want to re-use a Twig template, use the include function.

Using render in the Error Template

Our goal was to list upcoming events on the 404 page. Well, that’s pretty easy now:

{# app/Resources/TwigBundle/views/Exception/error404.html.twig #}
{# ... #}

{% block body %}
    {# existing <section> ... #}

    <section class="events">
        {{ render(controller('EventBundle:Event:_upcomingEvents')) }}
    </section>
{% endblock %}

Move to an imaginary page in your prod environment. In other words, put the app.php back in the URL:

Ah, but don’t forget to clear your cache!

php app/console cache:clear --env=prod

Controller Arguments

Great! Now what if we wanted to show a different number of upcoming events on the homepage versus the error page? No problem: render let’s us pass arguments to the controller function. Pass a max argument of 1 from the error template:

{# app/Resources/TwigBundle/views/Exception/error404.html.twig #}
{# ... #}

<section class="events">
    {{ render(controller('EventBundle:Event:_upcomingEvents', {
        'max': 1
    })) }}
</section>

Next, add a $max argument to _upcomingEventsAction and give it a default value so that we don’t have to pass it in. Send this variable into the getUpcomingEvents() function:

// src/Yoda/EventBundle/Controller/EventController.php
// ...

public function _upcomingEventsAction($max = null)
{
    $em = $this->getDoctrine()->getManager();

    $events = $em->getRepository('EventBundle:Event')
        ->getUpcomingEvents($max)
    ;

    return $this->render('EventBundle:Event:_upcomingEvents.html.twig', array(
        'events' => $events,
    ));
}

In EventRepository, give the function a $max argument. Instead of returning immediately, set the query builder to a variable and then return it later. If $max is set, limit the number of results that will be returned:

// src/Yoda/EventBundle/Entity/EventRepository.php
// ...

public function getUpcomingEvents($max = null)
{
    $qb = $this
        ->createQueryBuilder('e')
        ->addOrderBy('e.time', 'ASC')
        ->andWhere('e.time > :now')
        ->setParameter('now', new \DateTime());

    if ($max) {
        $qb->setMaxResults($max);
    }

    return $qb
        ->getQuery()
        ->execute()
    ;
}

Clear your cache and then try it out. Hey, only 1 event! Not only can render call a controller, but we can control its arguments. Now you’re unstoppable.

Leave a comment!

  • 2016-04-13 weaverryan

    Hey, making mistakes is good for learning! Good debugging :)

  • 2016-04-05 Oscar

    Hi Ryan,

    Got it, getUpcommingEvents() was indeed returning an empty array. Because i missed the "Creating a Custom ordrBy" part, i copied the method a little bit to fast from the video without noticing that the method of course returns an empty array because the event dates are in the passed.. "upCommingEvents".. Stupid me.
    Thanks!

  • 2016-04-05 weaverryan

    Hey Oscar!

    Hmm, when you go to the homepage where the events should be listed, do you see an error? Did you add the {{ render(controller(...)) }} to that template (index.html.twig)? Basically, if something isn't right, you *should* be getting an error from Symfony - right in the middle of the page where you're trying to render the embedded controller. Also, make sure that the getUpcomingEvents() is not returning an empty array (and make sure you still have some events in the database). In _upcomingEventsAction(), I would add var_dump($events);die; temporarily to see what this is returning.

    Cheers!

  • 2016-04-05 Oscar

    I found the part that i missen (Creating a Custom orderBy..), but unfortunately i still cant make it work.

  • 2016-04-05 Oscar

    Help, not working..
    for some reason it looks like i skipped the getUpcomingEvents method but after copying it from the video it is still not doing what it should do. Is there any way to let symfony tell me what could be wrong?
    Both the production as the dev environment dont show any events anymore.
    Thanks!