Symfony2 Mark menu as active layout.html.twig - symfony

I created a small website with 4 pages, means 4 navigation points. There is always one of these navigation points set as active in css:
class="active"
All these 4 pages inherit the layout.html.twig file where the navigation is defined.
Which is the easiest way to change the class ("active") class depending on the navigation point choosen?
Thanks in advance for your help!

As I don't have that many navigation points I solved the problem with a if tag of twig.
<a href="{{ path('dbe_underConstruction') }}" title="Home"
{% if app.request.get('_route') == 'dbe_underConstruction' %} class="active"{% endif %}>Home </a>
This might be not that a clear solution, but it works fine :-)

You can use the KnpMenuBundle. For example in src/Foo/BarBundle/Menu/MenuBuilder.php :
<?php
namespace Foo\BarBundle\Menu;
use Knp\Menu\FactoryInterface;
use Symfony\Component\HttpFoundation\Request;
class MenuBuilder
{
private $factory;
public function __construct(FactoryInterface $factory)
{
$this->factory = $factory;
}
public function createMyMenu(Request $request)
{
$menu = $this->factory->createItem('root');
$menu->setChildrenAttributes(array('class' => 'my-menu'));
// Always visible
$menu->addChild('Home', array('route' => 'foo_bar_homepage'));
// According to the current route
switch($request->get('_route')) {
case 'item_list':
$menu->addChild('Items')
->setCurrent(true);
break;
case 'item_show':
$menu->addChild('Items', array('route' => 'item_list'));
$menu->addChild('Show')
->setCurrent(true);
break;
// ...
}
}
The active class will be available automatically when calling setCurrent() method.
The documentation is available here, and you can find another tutorial here.

You could define the current menu entry:
{% set current_menu = "home" %}
And then check what the current menu entry is:
<li class="nav-item{% if current_menu is same as("home") %} active{% endif %}">

Related

Symfony twig controller route

I am begginer and I have little problem.
I have:
I have one primary menu with links to Controller main actions (index) and one secondary menu with links to other actions like create / edit / delete.
My problem is:
I don't know how to tell twig, that links in primary menu are active when any action in that controller is called.
In secondary menu I have:
{% set route_name = app.request.attributes.get('_route') %}
<a class="{% if route_name == item.route %} active {% endif %}">Link</a>
I have tried:
I tried to do it with prefixes on Controller, but it didn't worked.
Any hints or tips please? Thank you!
If your menu has only 2 nested levels, you can set a #Route annotation in the whole controller:
/**
* #Route(path="admin/pegass", name="admin_pegass_")
*/
class PegassController extends BaseController
{
For every action in your controller, you just need to set the end of the name and path:
/**
* #Route(name="list_users", path="/list-users")
*/
public function userList()
{
Finally, when rendering the first level of your menu, you can use:
{% set route_name = app.request.attributes.get('_route') %}
<a class="{% if route_name[0:13] == 'admin_pegass_' %} active {% endif %}">Link</a>
For more complex menus, breadcrumbs are not trivial, it will be easier to create an object representation of it in order to traverse it and perform operations in a more generic way (you can use a bundle like KnpMenu).

reusable dynamic sidebar in Symfony 4 (Twig)?

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 %}

How to include a reusable widget in Symfony (Twig)?

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

Checking class scope ACLs in Twig templates

I'm building a small-scale symfony project, as much for my own edification as anything else.
I have a Network class which extends Entity in a Doctrine ORM setup, and a bunch of users (also entities in a Doctrine setup). I've given some users the CREATE permission on the Network class, and that seems to be working. At least the exception is thrown when I expect it:
$securityContext = $this->get('security.context');
$objectId = new ObjectIdentity('class', 'Acme\\AcmeBundle\\Entity\\Network');
if(false === $securityContext->isGranted('CREATE', $objectId)) {
throw new AccessDeniedException("You do not have permission.");
}
But I'd like to check the permission in a twig template, something like this:
{% if is_granted('CREATE', 'Acme\\AcmeBundle\\Entity\\Network') %}
<li>
<a href="{{ path('network_new') }}">
Create a new entry
</a>
</li>
{% endif %}
My goal here is to only show the link if the user has permission to create a new network. But the is_granted() call seems to be returning true for all the users, not just the ones that I've explicitly granted CREATE to, or at least link is always appearing, even for users that have no ACL/ACE entries for the Network class.
It turns out that is_granted() expects an object as the second parameter. This is a Twig Extension that provides a classId() function to return an ObjectIdentifier object.
class ClassIdExtension extends \Twig_Extension
{
public function getFunctions()
{
return array(
'classId' => new \Twig_SimpleFunction('classId', array($this, 'classId'))
);
}
public function classId($class)
{
return new ObjectIdentity('class', $class);
}
public function getName()
{
return 'acme_classidextension';
}
}
Once it's registered as a service in Symfony, you can use it in Twig templates like so:
{% if is_granted('CREATE', classId('Acme\\AcmeBundle\\Entity\\Network')) %}
<li>
<a href="{{ path('network_new') }}">
Create a new entry
</a>
</li>
{% endif %}
And it works as expected.

Make controller only give a value to template, not render it - Symfony2

I have a twig template with the navbar and all other templates (the pages) include this template. I have a value in it which should be equal to all pages. How to set this value?
I tries something like this in a controller:
public function setNotificationsAction() {
$this->setNotifications();
return $this->render('AcmeMyBundle::navbar.html.twig', array(
'debts' => $this->notifications,
));
}
and then this in the template:
<span class="badge badge-important">
{% render(controller('AcmeMyBundle:DebtsLoansController:setNotifications')) %}
{{ debts }}
</span>
The result I want it like this:
<span class="badge badge-important">
3
</span>
but the number should be different and the controller should tell it.
I also tried to create a function which returns the value and to call it in the way like above.
I also tried this syntax
{{ render(controller('AcmeMyBundle:DebtsLoansController:setNotifications')) }}
but it isn't working, too.
I get the following mistake:
The function "controller" does not exist in AcmeMyBundle::navbar.html.twig at line 6
Do you have any idea how to achive this and not to have to edit each controller and each template :S Thanks very much in advance!
Well, I would suggest creating your own Twig extension. Something around the lines of:
<span class="badge">
{{ acme_notifications() }}
</span>
namespace Acme\DemoBundle\Twig\AcmeDemoExtension
class AcmeDemoExtension extends \Twig_Extension
{
public function getFunctions()
{
return array(
'acme_notifications' => new \Twig_Function_Method($this, 'getNotifications');
);
}
public function getNotifications()
{
$notifications = ...;
return $notifications;
}
}
Read more about creating your own Twig extension in the Symfony2 documentation.
You don't need the controller part :
{% render "AcmeBundle:MyController:MyAction" %}
Be aware however, that a render is a completely new request going through the whole Symfony lifecycle and thus can impact performance if you abuse it.
Edit : And as #Wouter J has pointed out : prior to Symfony 2.2 use above notation. After Symfony 2.2 the following has to be used :
{{ render(controller('AcmeArticleBundle:Article:recentArticles', { 'max': 3 })) }}

Resources