Symfony2 KnpMenuBundle children with dynamic parameters - symfony

Let's say I have a routing setup like that :
users_list:
pattern: /users
defaults: { _controller: AcmeBundle:User:list }
user_edit:
pattern: /user-edit/{id}
defaults: { _controller: AcmeBundle:User:edit }
I would like to setup a menu like this
Home
--Users list
----User edition
But I'm not sure how to handle my route 'user_edit' with dynamic params "id". Actually I don't want to display the link (which id ?), but I would like the 'Users list' parent node to be active if I edit an user.
I tried something like this
$userNode = $rootNode->addChild('Users list', array(
'route' => 'users_list',
));
$userNode->addChild('User edition', array(
'route' => 'user_edit',
));
Symfony complains about the missing parameter :(
Thanks !

This setup should work if you want to show the links:
$userNode->addChild('User edition', array(
'route' => 'user_edit',
'routeParameters' => array('id' => $someParameter)
));
For setting the parrent node as active you could use a custom renderer, override the menubundle's template or just add active class to the menu item based on this condition:
{% if app.request.attributes.get('_route') == 'user_edit' %}
{# activate parrent node #}
{% endif %}

Found something usefull in my case, you're able to solve this using the request in your menu definition class :
$userNode->addChild('User edition', array(
'route' => 'user_edit',
'routeParameters' => array('id' => $this->getRequest()->get('id'))
));

Related

Drupal 8 first MVC

I'm trying to reach my first controller and view on a custom module with drupal 8. I'm new and this is my first custom module.
my_module.info.yaml located in /modules/custom/my_module
name: My Own Custom Module
description: A silly example module
type: module
core: 8.x
With this, I've been able to go to my extension to activate the module.
So, the module is installed.
Now, I want to try to hit my first twig template
In order to do so
my_module.routings.yaml located in /modules/custom/my_module
my_module.article_list:
path: 'my_module/articles'
defaults:
_controller: '\Drupal\my_module\Controller\ArticleController::page'
_title: "Title routing"
requirements:
_permissions: 'access content'
My controller located in /modules/custom/my_module/src/Controller
<?php
namespace Drupal\my_module\Controller;
class ArticleController{
public function page(){
$items = array(
array('name' => 'Article one'),
array('name' => 'Article 2')
array('name' => 'Article 3')
);
return array('#theme' => 'article_list',
'#items' => $items,
'#title' => 'Liste d\'article');
}
}
?>
article-list.html.twig located in /modules/custom/my_module/src/templates
<h4>{{ title }}</h4>
<ul>
{% for article in items %}
<li>{{article.title}}</li>
{% endfor %}
</ul>
my_module.module located in /modules/custom/my_module
<?php
function my_module_theme($existing, $type, $theme, $path){
return array('article_list' => array('variables' => array('items' => array(), 'title' => '')));
}
?>
But then, when I try to reach my template I hit a page not found
http://localhost:9000/drupal-8.7.8/index.php/my_module/articles
Thanks for your help.
It's not my_module.routings.yaml
but my_module.routing.yml
Also I think that path parameter must start with slash sign:
path: '/my_module/articles'

Symfony 3 SonataAdmin show a "Sonata_type_collection" field a readonly in Form edit

I have a "Sonata_type_collection" field that only the owner can edit , and i want that the admin can only read this attribute ( he can edit other attributes).
I couldn't find anything but this :
$formMapper->add('commandeElements', 'sonata_type_collection', array('required'=> true,'by_reference' => false,'attr' => array(
'readonly' => true,
'disabled' => true
)), array(
'edit' => 'inline',
'inline' => 'table',
'sortable' => 'position',
));
it works somehow , the attribute can't be edited(when the form is submitted an error message is shown) but the button "add" and the checkbox "delete" still and a dropdown can be edited at least in the view .
is there a way to do this ?
you can hide the buttons using btn_add = false in the options array
https://sonata-project.org/bundles/admin/3-x/doc/reference/form_types.html#sonata-type-collection
but I will probably try to check in the frontend with twig, check if the user has certain role {% if is_granted('ROLE_ADMIN') %} ... {% endif %} and enable or disable the form component.
I will probably do...
{% set disabled = !is_granted('ROLE_YOU_WANT_TO_ALLOW') %} // in your case ROLE_OWNER
and then when rendering try something like...
{{ form_row(yourForm.yourCollectionName, {
'disabled': disabled
}) }}
Take as an example the twig template reference
http://symfony.com/doc/current/reference/forms/twig_reference.html#form-variables-reference
That's an idea that may let you do what you want

How to loop through form fields in Sensio Generator Bundle

I have overridden the Sensio Generator Bundle for CRUD in order to better suit my needs.
What I would like to do is to be able to loop through the entity fields.
It is done by default in show.html.twig but not in new and edit views.
When I implement the same logic in new.html.twig.twig it doesn't work though it does for edit.html.twig.twig.
{#app/Resources/SensioGeneratorBundle/skeleton/crud/views/new.html.twig.twig#}
{% for field, metadata in fields %}
{% if field not in 'id' %}
{{ '{{ form_row(edit_form.' ~ field ~ ')}}' }}
{% endif %}
{% endfor %}
When running the generator, the error is: Variable "fields" does not exist in "crud/views/new.html.twig.twig" at line 9
Ok, in fact it is an issue in Sensio Generator Bundle.
In the file: sensio\generator-bundle\Sensio\Bundle\GeneratorBundle\Generator\DoctrineCrudGenerator.php the generateNewView function is missing a paramter. It is not passing the fields as opposed to generateShowView.
Here is the comparison:
protected function generateNewView($dir)
{
$this->renderFile('crud/views/new.html.twig.twig', $dir.'/new.html.twig', array(
'bundle' => $this->bundle->getName(),
'entity' => $this->entity,
'route_prefix' => $this->routePrefix,
'route_name_prefix' => $this->routeNamePrefix,
'actions' => $this->actions,
));
}
versus
protected function generateShowView($dir)
{
$this->renderFile('crud/views/show.html.twig.twig', $dir.'/show.html.twig', array(
'bundle' => $this->bundle->getName(),
'entity' => $this->entity,
'fields' => $this->metadata->fieldMappings,
'actions' => $this->actions,
'route_prefix' => $this->routePrefix,
'route_name_prefix' => $this->routeNamePrefix,
));
}
I'll try to post this as an improvement.

Nested menu with parameters in Symfony and KNPmenu

I am struggling with Symfony2 and Knpmenu to build a menu that handles:
breadcrumbs
routing with dynamic parameters
rendering of separate menus starting with different children
My Menu/Builder.php file looks like this (the extra bits like navbar, pull-nav etc are part of the mopa_bootstrap extension that handles the rendering using bootstrap classes):
namespace My\AppBundle\Menu;
use Knp\Menu\FactoryInterface;
class Builder
{
public function mainMenu(FactoryInterface $factory, array $options)
{
$menu = $factory->createItem(
'root', array(
'navbar' => true,
'pull-right' => true,
)
);
// Main Menu -> Config
// no link here, it's just a placeholder
$dropdown = $menu->addChild(
'Config', array(
'dropdown' => true,
'caret' => true,
)
);
// Menu -> Config -> User
$dropdown2 = $dropdown ->addChild(
'User', array(
'route' => 'user',
)
);
// Secondary Menu -> Edit (but child of Menu -> Config -> User)
$dropdown2->addChild(
'Edit',
array(
'route' => 'user_edit',
'routeParameters' => array('name' => $options['id']),
)
);
The idea is to have a main menu that prints the first two levels only, and a separate menu that gets rendered somewhere else to allow users moving between the edit/delete/whatever views of that specific element being viewed.
What I am trying to achieve, is to have a single structure thus to handle the parenting structure, not only to have sign parents as active in the menu, but also to be able to handle a working breadcrumb structure.
InResources/views/base.html.twig I am calling the main menu like this:
{{ mopa_bootstrap_menu('MyAppBundle:Builder:mainMenu', {depth: 2}) }}
And ideally the sub menu like this:
{% set id = app.request.attributes.get('id') %}
{% if app.request.attributes.get('_route') starts with 'user_' %}
{% set menu = knp_menu_get('MyAppBundle:Builder:mainMenu', ['User'], {'id': id }) %}
{{ knp_menu_render(menu) }}
{% endif %}
However:
knpmenu returns error when rendering the main menu, as $options['id'] isn't defined
I still can't render the secondary menu (therefore by passing the parameter 'User') - the page just returns a black output in that block
Is this approach correct?
I'm using "knplabs/knp-menu": "2.0.*#dev" and "symfony/symfony": "2.5.*"
As it turns out, using this method allowed to access effectively $request and add the conditional in the MenuBuilder.php file to prompt the sub-sub-items only under certain conditions.
The final code looks like this:
// Menu/MenuBuilder.php
namespace My\AppBundle\Menu;
use Knp\Menu\FactoryInterface;
use Symfony\Component\HttpFoundation\Request;
class MenuBuilder
{
private $factory;
/**
* #param FactoryInterface $factory
*/
public function __construct(FactoryInterface $factory)
{
$this->factory = $factory;
}
public function createMainMenu(Request $request)
{
$menu = $this->factory->createItem(
'root', array(
'navbar' => true,
'pull-right' => true,
)
);
// Main Menu -> Config
// no link here, it's just a placeholder
$dropdown = $menu->addChild(
'Config', array(
'dropdown' => true,
'caret' => true,
)
);
// Menu -> Config -> User
$dropdown2 = $dropdown ->addChild(
'User', array(
'route' => 'user',
)
);
// Secondary Menu -> Edit (but child of Menu -> Config -> User)
// Skip this part if we are not in the relevant page, thus to avoid errors
if (false !== strpos($request->get('_route'), 'user_') && $request->get('id')):
$dropdown2->addChild(
'Edit',
array(
'route' => 'user_edit',
'routeParameters' => array('id' => $request->get('id')),
)
);
endif;
And the services configuration file:
// Resources/config/services.yml
services:
my_app.menu_builder:
class: My\AppBundle\Menu\MenuBuilder
arguments: ["#knp_menu.factory"]
my_app.menu.main:
class: Knp\Menu\MenuItem
factory_service: my_app.menu_builder
factory_method: createMainMenu
arguments: ["#request"]
scope: request
tags:
- { name: knp_menu.menu, alias: main }
And then the Resources/views/base.html.twig template:
// Main menu
{% set menu = knp_menu_get('main', ['Config', 'Users'], {'id': id }) %}
{{ knp_menu_render(menu) }}
// Secondary menu
{% if app.request.attributes.get('_route') starts with 'user_' %}
{% set menu = knp_menu_get('main', ['Config', 'Users'], {'id': id }) %}
{{ mopa_bootstrap_menu(menu, {'automenu': 'navbar'}) }}
{% endif %}
The solution seems to work, but if you have a better approach please do let me know!

How to disable HTML escaping of labels in KnpMenuBundle

I want to render an HTML label like:
$menu->addChild('Dashboard', array(
'route' => 'dashboard',
'label' => '<i class="fa-icon-bar-chart"></i><span class="hidden-tablet"> Dashboard</span></a>',
'extra' => array('safe_label' => true)
)
);
And I've pass the proper option while rendering:
{{ knp_menu_render('WshCmsHtmlBundle:Builder:mainMenu', {'allow_safe_labels': true} ) }}
But my label is still being escaped. What am I doing wrong?
Ok, the answer is!
You set up extra items on menu item not by 'extra' key but by 'extras' key.
So when you setup the item like this:
$menu->addChild('Dashboard', array(
'route' => 'dashboard',
'label' => '<i class="fa-icon-bar-chart"></i><span class="hidden-tablet"> Dashboard</span></a>',
'extras' => array('safe_label' => true)
)
);
it works fine!
There's two steps to achieve this.
1. MenuBuilder
You have to set safe_label to true in extras. Note that you can now write HTML in your label.
$menu->addChild('Home<i><b></b></i>', array(
'route' => 'homepage',
'extras' => array(
'safe_label' => true
),
));
2. Twig
You have to filter the output of knp_menu_render() so that it prints raw HTML (see documentation).
{{ knp_menu_render('main', {'allow_safe_labels': true}) | raw }}
Warning
Please be aware that this may be dangerous. From the documentation:
Use it with caution as it can create some XSS holes in your application if the label is coming from the user.
I used FyodorX's method to add a strong tag. It works like a charm but I must say that the raw filter is not necessary

Resources