I’m facing a weird behavior from Symfony 2.5.5 (PHP 5.6.1), more specifically Twig. Here is a fragment of my template layout:
<nav>
{% render controller('SGLotteryGameBundle:Home:lastDraw') %}
<ol class="breadcrumb">
<li>{{ 'SuperWinner'|trans }}</li>
{% block bc %}{% endblock %}
</ol>
</nav>
This template worked fine until I added the render call. After that, Symfony reported:
An exception has been thrown during the rendering of a template
("Unable to generate a URL for the named route "sg_lottery_home" as such route does not exist.")
in /home/kevin/Prog/PHP/SG2/src/SG/Lottery/GameBundle/Resources/views/layout.html.twig at line 70.
Of course, the sg_lottery_home is defined and works well without the render block. If I comment the path generation of this route, the immediate next one fails. Routes before the tag are rendered without any issue.
Here is the SGLotteryGameBundle:Home controller:
<?php
namespace SG\Lottery\GameBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
use Symfony\Component\HttpFoundation\Response;
class HomeController extends Controller
{
/**
* #Template
*/
public function indexAction()
{
return [];
}
public function lastDrawAction()
{
return new Response('Dummy');
}
}
I tried replacing {% render ... %} by {{ render(...) }}, without any change.
Important note: it only happens when I’m logged in.
Apparently, it was caused by JMSI18nRoutingBundle generating an error while retrieving the user's locale: the available locales were en and fr and the user's locale fr_FR. I have no idea how the {{ render(...) }} call interacted with that.
Related
Is there a standard/preferred way in Symfony to pass common variables to the base template?
There are things that are on every page, like a username in the menu that I obviously don't want to have to remember to add for every controller.
My thought was to create a controller for the template and return the data. But wondering if there is something more built in to handle this.
return $this->render(
'#secured/account/profile.html.twig',
array('userForm' => $form->createView(),
'base' => call_base_layout_controller()
);
{# templates/account/profile.html.twig #}
{% extends '#secured/base.html.twig' %}
{% block body %}
{% endblock %}
I cannot find it in the current version docs but, as far as I know you can still access a lot directly from twig through the app twig variable defined automatically by the framework for every request https://symfony.com/doc/3.4/templating/app_variable.html
For example, you can get the current user's username as follows:
{{ app.user.username }}
app holds user, request, session, environment, and debug so you can put other values needed into the session or environment variables, and fetch/render those values directly in the template.
If the data that you want is related to the authenticated user, I would recommend what Arleigh Hix said
Otherwise you can create a class that extends AbstractExtension and fill it with your logic
Anything on this class can be accessed from all your twig pages
// src/Twig/AppExtension.php
namespace App\Twig;
use Twig\Extension\AbstractExtension;
use Twig\TwigFunction;
class AppExtension extends AbstractExtension
{
public function getFunctions()
{
return [
new TwigFunction('myGlobalData', [$this, 'myGlobalData']),
];
}
public function myGlobalData()
{
return "what ever you want ( ex: a query resualt..)";
}
}
Than on your twig template just call it like this
{{ myGlobalData() }}
Even if it retuns an array, u can access it.
I recently started using Symfony 4 and I am creating my first website with this wonderful framework right now.
I have a sidebar that should be displayed in about half of my routes and the content of the sidebar should be filled with some data from a database.
Currently I use DI in all this routes and pass the result of the injected repository to the template (which includes my sidebar.html.twig) for the route.
public function chalupaBatman(FancyRepository $repository)
{
$sidebarObjects = $repository->getSidebarObjects();
$this->render('controllername/chalupabatman.html.twig', [
'sidebarObjects' => $sidebarObjects,
]);
}
I am wondering if there is a way to avoid this for every route I define in my controllers.
So far I found this topic on stackoverflow.
The User Mvin described my problem in a perfect way and also provided some solutions.
However there is still no answer to "what is the best practice" part also the topic is from 2017; therefor, the way to solve this may have changed in Symfony 4.
I ended up with a TwigExtension solution. I'll describe how to achieve it and it would be great if you guys could provide some feedback.
Let me know if I produce massive overhead or miss something essential ;-)
Alright, first of all I created a TwigExtension via command-line
php bin/console make:twig-extension AppExtension
And then I modified the class to look like this:
<?php
namespace App\Twig;
use App\Repository\ArticleRepository;
use Psr\Container\ContainerInterface;
use Symfony\Contracts\Service\ServiceSubscriberInterface;
use Twig\Extension\AbstractExtension;
use Twig\TwigFunction;
class AppExtension extends AbstractExtension implements ServiceSubscriberInterface
{
private $container;
public function __construct(ContainerInterface $container)
{
$this->container = $container;
}
public function getFunctions(): array
{
return [
new TwigFunction('article_sidebar', [$this, 'getArticleSidebar'], ['needs_environment' => true, 'is_safe' => ['html']]),
];
}
public function getArticleSidebar(\Twig_Environment $twig)
{
$articleRepository = $this->container->get(ArticleRepository::class);
$archive = $articleRepository->myAwesomeLogic('omegalul');
return $twig->render('/article/sidebar.html.twig', [
'archive' => $archive,
]);
}
public static function getSubscribedServices()
{
return [
ArticleRepository::class,
];
}
}
In order to activate Lazy Performance so our Repository and the additional Twig_Environment doesn't get instantiated everytime when we use Twig
we implement the ServiceSubscriberInterface and add the getSubscribedServices-method.
Therefor, our Repo and Twig_Environment only gets instantiated when we actually call {{ article_sidebar() }} inside a template.
{# example-template article_base.html.twig #}
{% extends 'base.html.twig' %}
{% block body %}
<div class="row">
<div class="col-10">
{% block article_body %}{% endblock %}
</div>
<div class="col-2">
{{ article_sidebar() }}
</div>
</div>
{% endblock %}
Now I am able to define my templates for the article-routes like this:
{# example-template /article/show.html.twig #}
{% extends 'article_base.html.twig' %}
{% block article_body %}
{# display the article here #}
{% endblock %}
I have a Project entity, which has a controller defining many routes:
projects/1
projects/1/foo
projects/1/bar
I need a service to provide the current project. The use case is that I have dependencies in my base twig templates which need to know the current project. i.e. a dropdown project selector that is outside the context of the template the current controller is serving.
I've tried creating a service getting route info with $container->get('router.request_context');, however, that only provides a path. I don't want to have to parse the path string if I don't have to.
What is the most proper approach?
If I understood you correctly the solution for your problem is rendering/embedding controller. Of course it's simplest, yet somehow acceptable solution for rendering parts of html with some custom logic apart from current template.
You can read about rendering/embedding controllers.
Some snippets...
Define controller:action (obviously the logic in my example is pretty straight forward):
/**
* Generate select input field.
*
* #Route("/widget", name="project_widget")
* #Method("GET")
*/
public function widgetAction()
{
$repo = $this->getDoctrine()
->getManager()
->getRepository('AppBundle:Project');
// #NOTICE: Wee need master request info, because our widget
// is rendered and called as subrequest.
$masterRequest = $this->get('request_stack')->getMasterRequest();
// #NOTICE: You need second parameter as default value in case there is no id in masterRequest route.
$currentProjectId = $masterRequest->get('id', 0);
$currentProject = $repo->find($currentProjectId);
$projects = $repo->findAll();
return $this->render('project/widget.html.twig', [
'currentProject' => $currentProject,
'projects' => $projects,
]);
}
Then you need to create the template project/widget.html.twig for it:
<div class="widget_project_selection">
{% if projects is not empty %}
<select name="widget_project_selection" id="widget_project_selection">
<option value="">None</option>
{% for project in projects %}
<option value="{{ project.id }}"
{# #NOTICE: If this is current project, select it #}
{{- currentProject and project.id == currentProject.id
? 'selected="selected"'
: '' -}}
>
{{- project.title -}}
</option>
{% endfor %}
</select>
{% else %}
<span>{{ 'Sadly, no projects yet :('|trans }}</span>
{% endif %}
</div>
and at last (but not least) render it somewhere like in base.html.twig:
{{- render(controller('AppBundle:Project:widget')) -}}
I've created for you a Github repo as reference. It's a small Symfony app with similar setup. You can even run it if you like, don't forget about dependencies and database update thou. Entry point is /app_dev.php/project/
Take a look at widgetAction, widget template and example usage in base.html.twig.
EDIT: But that's not everything. You said you need a service. If for some reason rendering/embedding controller is not an option for you or you really whant to use a Service (as in Dependency Container) you can extend Twig and use the full power of services.
I've also implemented a Twig Filter as example to show you the real power of Twig Extensions in here and here (usage in templates).
Check out Twig Extension and Extending Twig for more info about Twig Extensions.
Also check out service.yml for service and extension definitions - if you are not using Symfony3.3+, there will be some additional work to do - defining service and extension directly.
In your controller, you can use type hinting to load the "current" entity via the route.
For example:
#myrouter.yml
current_project:
path: /projects/{project}
Separately, your controller...
//mycontroller.php
public function myControllerAction(Request $request, Project $project)
{
//$project is entity (assuming valid) loaded via the route above
return $this->render('mytemplate.twig', ['project' => $project]);
}
So, I'm still fairly new to Symfony and Twig. I was wondering how to best include/create a snippet of reusable code in the templates. Say, for example, that you have a sidebar that you want to show on every page.
{% extends 'AppBundle::base.html.twig' %}
{% block body %}
<div id="wrapper">
<div id="content-container">
{# Main content... #}
</div>
<div id="sidebar">
{% include 'sidebar.html.twig' %}
</div>
</div>
{% endblock %}
And that in that sidebar are a couple of widgets that all do their own logic. How you do go about creating/including those widgets?
So far, I've come across several solutions.
As a controller
The first was to embed the widget as a controller(s) in Twig.
class WidgetController extends Controller
{
public function recentArticlesWidgetAction()
{
// some logic to generate to required widget data
// ...
// Render custom widget template with data
return $this->render('widgets/recentArticles.html.twig', array('data' => $data)
);
}
public function subscribeButtonWidgetAction()
{
// ...
return $this->render('widgets/subscribeButton.html.twig', array('data' => $data)
}
// Many more widgets
// ...
}
And include that in 'sidebar.html.twig' like so
<div id="sidebar">
{# Recent Articles widget #}
{{ render(controller('AppBundle:Widget:recentArticlesWidget' )) }}
{# Subscribe-Button widget #}
{{ render(controller('AppBundle:Widget:subscribeButtonWidget' )) }}
{# and so on #}
</div>
As a service
I've also seen some people register widgets as services (that can be used in Twig directly). With the widget main class
// src/AppBundle/Service/RecentArticlesWidget.php
namespace AppBundle\Service;
use Symfony\Component\DependencyInjection\ContainerInterface;
class RecentArticlesWidget
{
protected $container;
public function __construct(ContainerInterface $container)
{
$this->container = $container;
}
public function getRecentArticles()
{
// do some logic (use container for doctrine etc.)
}
}
that is then registered as a service,
# src/AppBundle/Resources/config/services.yml
services:
recentArticlesWidget:
class: AppBundle\Service\RecentArticlesWidget
arguments: ["#service_container"]
passed to the template in the controller,
namespace AppBundle\Controller;
class SidebarController {
public function showAction($request) {
// Get the widget(s)
$recentArticlesWidget = $this->get('recentArticlesWidget');
// Pass it (them) along
return $this->render('sidebar.html.twig', array('recentArticlesWidget' => $recentArticlesWidget));
}
}
so it can simply be used like this in Twig
{# sidebar.html.twig #}
{{ recentArticlesWidget.getRecentArticles()|raw }}
Alternatively, you can also add your service to the Twig global variables directly by adding it to the Twig config. This way, it won't need to be passed into the view by the controller.
#app/config/config.yml
twig:
globals:
# twig_var_name: symfony_service
recentArticlesWidget: "#recentArticlesWidget"
As a Twig Extension
This one is very similar to using a service above (see the documentation). You create an a twig extension class that is almost identical to the service shown previously
// src/AppBundle/Twig/RecentArticlesWidgetExtension.php
namespace AppBundle\Twig;
class RecentArticlesWidgetExtension extends \Twig_Extension
{
protected $container;
public function __construct(ContainerInterface $container)
{
$this->container = $container;
}
public function getFunctions()
{
return array(
"getRecentArticles" => new Twig_Function_Method($this, "getRecentArticles")
// register more functions
);
}
public function getRecentArticles()
{
// do some logic (use container for doctrine etc.)
}
// Some more functions...
public function getName()
{
return 'WidgetExtension';
}
}
Register that as a service with an added tag
# src/AppBundle/Resources/config/services.yml
services:
recentArticlesWidget:
class: AppBundle\Twig\RecentArticlesWidgetExtension
arguments: [#service_container]
tags:
- { name: twig.extension }
and simply use it like a global function in Twig
{# sidebar.html.twig #}
{{ getRecentArticles() }}
Thoughts
One thing I noticed is that with the last two methods is that the logic and the view don't seem to be seperated at all anymore. You basically write a widget function and have that function output the complete html for the widget. This seems to go against the modularity and patterns Symfony tries to enforce.
On the other hand, calling a distinct controller or controller action (with their own twig renders) for every single widget seems like it could take more processing than might be needed. I'm not sure if it actually slows anything down, but I do wonder if its excessive.
Long story short, is there a best practice for using reusable widgets in Symfony? I'm sure some of these methods can also be mixed, so I was just wondering how to best go about this.
Twig extension and Twig macro should point you in the right direction.
Use the macro for the view and extension for the business logic.
On a side note in your Twig extension example, it's probably a good idea to only pass in services that you are using instead of the whole service container.
I would rather use blocks and a parent template. Simply put, insert the side bar in the main layout and have all other templates that require the side bar
inherit from it.
Something like this:
layout.html.twig will be something like this:
{% block title}
// title goes here
{%endblock%}
<div id="wrapper">
<div id="content-container">
{% block pageContent %}
{% endblock %}
</div>
<div id="sidebar">
// Side bar html goes here
</div>
</div>
Now all pages will inherit from this layout.html.twig. Say for example a page called home.html.twig will be:
home.html.twig
{% extends 'AppBundle::layout.html.twig' %}
{% block title%}
// this page title goes here
{% endblock %}
{% block pageContent %}
//This page content goes here
{% endblock %}
You can add as many blocks as needed, for example css and js blocks for each page.
Hope this helps!
I think the simplest way is defining a block in a template and then extending that template to render blocks like so:
#reusable.html.twig
{% block reusable_code %}
...
{% endblock %}
And
#reused.html.twig
{% extends 'reusable.html.twig' %}
{{ block('reusable_code') }}
If you want more reusability than that or your block contains business logic or model calls a twig extension is the way to go
I am using controllers as services and try to embed those controllers in the twig template using the following syntax:
{% render 'my_controller:thisAction' %}
{% render 'my_controller2:this2Action' %}
{% render 'my_controller3:this3Action' %}
The problem is that instead of getting parsed correctly, only the first render statement is able to render the template and the following ones are not.
Any suggestions why this problem is occuring ?
Just make sure the naming convention is respected. And you do not need your controller to be services. Controllers are meant to grab a Request and return a Response.
Imagine you have a Controller called Default.
namespace Renoir\SiteBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
class DefaultController extends Controller
{
// ...
public function randomNameRenderAction()
{
// Do some logic
}
}
In the view you could call by using
{% render 'RenoirSiteBundle:Default:randomNameRender' %}