Rendering a Template
How a controller renders a Twig template, where templates live, and why Twig is compiled rather than interpreted.
protected function render(string $view, array $parameters = [], ?Response $response = null): ResponseA template is the file that produces the HTML a browser receives. Keeping that markup inside a controller mixes two jobs. Moving it to a template separates them. Symfony renders templates with Twig, a template language with its own syntax.
Twig is not PHP wearing a costume. Each template is compiled once into a PHP class under var/cache, and that generated class is what runs on later requests. A template therefore cannot call arbitrary PHP; it can use only what Twig exposes. That constraint is the point, because it keeps logic out of the view.
Where templates live
Templates live in templates/ at the project root. That directory is the twig.default_path setting, whose default value is %kernel.project_dir%/templates. Names are relative to it, so article/index.html.twig means templates/article/index.html.twig. Two extensions is the convention: the first names the output format, the second is always twig.
Rendering from a controller
render() comes from AbstractController. Its first argument is the template name. Its second is an array whose keys become variable names inside the template, so 'articles' => $articles makes {{ articles }} available. It returns a Response, which a controller returns directly. In the example below, GET /articles returns 200 with the rendered page.
The short form and the long form are the same thing. return $this->render($name, $vars); is return new Response($this->renderView($name, $vars));. renderView() hands back the string, which is what you want for an email body or a fragment you intend to store.
Example
// src/Controller/ArticleController.php
declare(strict_types=1);
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
final class ArticleController extends AbstractController
{
// GET /articles returns 200 with the rendered HTML of
// templates/article/index.html.twig
#[Route('/articles', name: 'app_article_index', methods: ['GET'])]
public function index(): Response
{
// Plain PHP values for now. Nothing here reads a database.
$articles = [
['title' => 'Routing', 'minutes' => 6],
['title' => 'Templates', 'minutes' => 9],
];
// First argument: the file templates/article/index.html.twig
// Second argument: every key becomes a variable in the template
return $this->render('article/index.html.twig', [
'page_title' => 'Articles',
'articles' => $articles,
]);
}
}When to use it
- Return an HTML page from a controller instead of building strings with sprintf.
- Render a fragment to a string for the body of a notification email.
- Share one set of variables between a page and the partial it includes.
More examples
The template that receives those variables
The two keys passed from the controller are now ordinary variables. The {% else %} branch of a for loop runs when the collection is empty, which saves wrapping the loop in a condition. Reach for this layout as soon as a controller produces more than a line of markup.
{# templates/article/index.html.twig #}
<h1>{{ page_title }}</h1>
<ul>
{% for article in articles %}
<li>{{ article.title }} - {{ article.minutes }} min</li>
{% else %}
<li>Nothing published yet.</li>
{% endfor %}
</ul>renderView() when you need the string, not a response
GET /articles/routing/card returns 200 with the card fragment alone and no surrounding page. The same method is what you call when the rendered HTML is going into an email, a stored document or a response you build by hand rather than straight down the wire.
// src/Controller/ArticleController.php (a second action on the same class)
#[Route('/articles/{slug}/card', name: 'app_article_card', methods: ['GET'])]
public function card(string $slug): Response
{
// renderView() returns the rendered template as a string.
$html = $this->renderView('article/_card.html.twig', [
'slug' => $slug,
]);
// A controller must return a Response, so build one around the string.
return new Response($html, Response::HTTP_OK, [
'Content-Type' => 'text/html; charset=UTF-8',
]);
}The configuration behind the defaults
You rarely change default_path, but knowing it exists explains why template names have no leading slash and no templates/ prefix. Setting strict_variables to true in every environment is a reasonable choice if you would rather find a typo as an error than as a blank space on a production page.
# config/packages/twig.yaml
twig:
# Where template names are resolved from. This is the default value.
default_path: '%kernel.project_dir%/templates'
# Unknown variables raise an error instead of rendering nothing.
# The default is %kernel.debug%, so it is on in dev and off in prod.
strict_variables: '%kernel.debug%'
Discussion