KnpLabs / KnpMenu

Menu Library for PHP

Home Page:https://knplabs.com

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Render KnpMenu with custom template through Twig

Kwbmm opened this issue · comments

I'm having issues in rendering a KnpMenu in a twig template according to a custom template.
The situation is the following: I have create three menus, 2 which should be rendered according to the default template provided, and the third (mobile menu) must be rendered according to my custom template. All the three menus should be rendered inside my navbar.twig template through knp_menu_render() function.

So what I do, inside navbar.twig is:

{{knp_menu_render('desktop',{'depth':2})}} {#The first menu#}
{{knp_menu_render('rightDesktopSection')}} {#The second menu#}

And the mobile menu rendered as:

{{knp_menu_render('mobile',{'depth':2,'template':'knp-menu/knp_menu.html.twig'})}}

That is what I understand I can do after reading the documentation.
However, no matter what I write as value for template key, mobile is always rendered using the default template.

This is the folder structure of my project:

|
|-lib/
    |-routes/ /*My silex routes*/
        |-... /*Routes php files*/
    |-... /*Other files*/
|-templates/
    |-knp-menu/
        |-knp_menu.html.twig /*My customized template*/
        |-knp_menu_base.html.twig
    |-navbar.twig /*Where I render the KnpMenus*/
|-vendor/
    |-knplabs/
        |-knp-menu/
            |- /*All the KnpMenu source code*/
|-web/
    |-index.php /*My public endpoint*/

Provided it is possible to do rendering according to a custom template, I believe the issue is that the template path is wrong and the rendering, in this case, falls back to the default one. If so, which point of the folder structure the search starts from?

If you pass a wrong template name, Twig will throw an exception, it will not fallback to another template.

Are you sure that your custom template is never called ?

I tested it and I can even specify a non existant file, no error shows up and rendering is fine becuase menu is rendered with default template

So, let me clear things up a bit, maybe I did some mistake during initialization of the library.

This is the folder structure with the relevant files:

|
|-lib/
    |-includes/ /*PHP Classes for general reuse*/
        |- utilClass.php /*Class containing utility methods*/
        |- ...
    |-routes/ /*My silex routes*/
        |-... /*Routes php files*/
    |- boot.php /*Bootstrap silex, twig and KnpM*/
    |-... /*Other files*/
|-templates/
    |-knp-menu/
        |-knp_menu.html.twig /*My customized template*/
        |-knp_menu_base.html.twig
    |-navbar.twig /*Where I render the KnpMenus*/
        |-index.twig
|-vendor/
    |-knplabs/
        |-knp-menu/
            |- /*All the KnpMenu source code*/

And this is the main logic run for rendering the menus:

  1. boot.php:

    require_once __DIR__.'/../vendor/autoload.php';
    $app = new Silex\Application();
    //Other stuff
    $app['debug'] = true;
    //Some stuff
    $app->register(new Silex\Provider\TwigServiceProvider(), array('twig.path' => __DIR__.'/../templates'));
    $app->register(new SilexGuzzle\GuzzleServiceProvider());
    $app->register(new \Knp\Menu\Integration\Silex\KnpMenuServiceProvider());
    
    //Some more code
    
  2. utilClass.php:

    class Utils {
        //Only the main functions are shown
    
        public function initNavbar(&$app){
            $app['desktopMenu']=$this->generateDesktopNavbar($app);
            if(isset($_SESSION['user']))
                $app['rightSectionMenu']=$this->generateRightSectionNavbar($app);
            $app['mobileMenu']=$this->generateMobileNavbar($app);
            $app['knp_menu.menus'] = array(
                    'desktop'=>'desktopMenu',
                    'rightDesktopSection'=>'rightSectionMenu',
                    'mobile'=>'mobileMenu');
        }
    
        private function generateDesktopNavbar($app){
            $menu = $app['knp_menu.factory']->createItem('DesktopMenu');
            $menu->setChildrenAttribute('class','left');
            $menu->addChild('Home',array('uri' => '/home'));
            //And it goes on like this until I build the whole tree..
            return $menu;
        }
    
        private function generateMobileNavbar($app){
            $menu = $app['knp_menu.factory']->createItem('MobileMenu');
            $menu->setChildrenAttribute('class','off-canvas-list');
            $menu->addChild('Home',array('uri' => '/home'));
            //Same as before
            return $menu;
        }
    
        private function generateRightSectionNavbar($app){
            $menu = $app['knp_menu.factory']->createItem('RightSection');
            //Same as before            
            return $menu;
        }
    }
    
  3. home.php (or any other route):

    require_once __DIR__."/../includes/utilClass.php";
    $app->get('/home',function() use($app){
        $cmn = new Utils;
        $cmn->initNavbar($app);
        //Plus some extra parameters are loaded into $twigParameters
        return $app['twig']->render('index.twig',$twigParameters);
    });
    
  4. index.twig (nothing special, just some includes of other twig templates, and navbar.twig).

  5. navbar.twig:

    <!-- Some HTML -->
    <section class="top-bar-section">
        {{knp_menu_render('rightDesktopSection')}}                  
        {{knp_menu_render('desktop',{'depth':2})}}
    </section>
    <!-- Some HTML -->
    <aside class="left-off-canvas-menu" aria-hidden="true">
        {{knp_menu_render('mobile',{'depth':2,'template':'knp-menu/knp_menu.html.twig'})}}
                    {#as value for 'template' key I can write whatever I want! Even non-existent path!#}
    </aside>
    <a class="exit-off-canvas"></a>
    

That's pretty much it.. For completeness, I'm using (copying from comopser.json):

  • Silex: ~1.3
  • Twig: ~1.0
  • knplabs/knp-menu: ^2.1

I did some digging and either I'm not seeing something or there is something very strange in this code going on..

So, to clear up what I'm trying to achieve, I want that li elements that are not links, are rendered as <li><label>list item label</label></li> .
Now, since I was not able to achieve this my setting a custom template in knp_menu_render, I thought to manually change the original template found under vendor/knplabs/knp-menu/src/Knp/Menu/Resources/views: this is the place where the default templates are taken, if I understood it right.

Guess what? If I edit the original knp_menu.html.twig and change:

{% block spanElement %}{% import _self as knp_menu %}<span{{ knp_menu.attributes(item.labelAttributes) }}>{{ block('label') }}</span>{% endblock %}

To:

{% block spanElement %}{% import _self as knp_menu %}<label{{ knp_menu.attributes(item.labelAttributes) }}>{{ block('label') }}</label>{% endblock %}

Menu is still rendered as <li><span>..</span></li>

Tried with a fresh setup, same issue. It doesn't sound right to me..

EDIT: Could the culprit be vendor/knplabs/knp-menu/src/Knp/Menu/Renderer/ListRenderer.php line 205 and on:

protected function renderSpanElement(ItemInterface $item, array $options)
{
    return sprintf('<span%s>%s</span>', $this->renderHtmlAttributes($item->getLabelAttributes()), $this->renderLabel($item, $options));
}

looks like this is by design then. i guess its done to make sure that label attributes can be applied somewhere. is it okay if we close this issue?

Well, I don't think it's a design choice, and if it is, it doesn't seem smart: users may want to change the inner element and it should be possible throught template overriding. Which is the point of having a twig template (knp_menu.html.twig) under vendor/knplabs/knp-menu/src/Knp/Menu/Resources/views to override things if it's not taken into account?

In my case I just needed to apply a set of CSS rules (which were coded on a element), so I created a new CSS class and applied it to the span element and that fixed the issue.. But there may be cases in which the fix is not that simple.

I guess class inherintance and method override would work but I think it's easier said than done. Best would be allow template override.. I tried to modify the Knp menu code but I didn't have enough project and php knowledge at that time to provide a fix.

I thought maybe renderSpanElement method could be modified to take an extra parameter which contains the tag to use (instead of hardcoding it).

if this needs refactoring, then i would go for a template and not for passing the tag name to use. i just noticed the class is called ListRenderer - which suggests you could make knp menu use your custom renderer.

i improve the documentation in #245. if you use the ListRenderer, its quite simple to extend the renderer and overwrite just the renderSpanElement method. if you use the TwigRenderer, you need to look at knp_menu.html.twig. you can extend that template and overwrite {% block spanElement %} with what you need.

the two renderers (twig and list) are completely separate and only one of the two will be used.

your code looks like you are using silex. and the silex loader does this: https://github.com/KnpLabs/KnpMenu/blob/master/src/Knp/Menu/Integration/Silex/KnpMenuServiceProvider.php#L43 so its using the ListRenderer (probably because twig is optional in silex). i don't know silex much, but i would hope there is a way to change what renderer class is used, by overwriting knp_menu.renderer.list to use your custom class? a twig renderer is also created if twig is defined: https://github.com/KnpLabs/KnpMenu/blob/master/src/Knp/Menu/Integration/Silex/KnpMenuServiceProvider.php#L81 - and your code indicates you load twig before loading knp menu. not sure why its picking the list in that case. maybe the order in https://github.com/KnpLabs/KnpMenu/blob/master/src/Knp/Menu/Integration/Silex/KnpMenuServiceProvider.php#L54 could be wrong? should twig come before the list renderer there if we want to ever use the twig renderer in silex? or maybe there is some other way to force the twig renderer.