Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Symfony2 KnpMenuBundle: set active a menu item even when its not on that menu

I created my menu builder and it works. One of my route is

/database

But this has a child route:

database/view/{id}

I don't want to put the view route into the menu items because without the ID it won't work.

But I want the database route to be active when the user is on the view.

How can I do this?

like image 519
NaGeL182 Avatar asked Feb 03 '13 16:02

NaGeL182


4 Answers

Managed to solve it with this little hack:

in the menuBuider after you add all the Children but before you return the menu i added

$request = $this->container->get('request');
        $routeName = $request->get('_route');
        switch ($routeName)
        {
            case 'battlemamono_database_view_by_name':
            case 'battlemamono_database_view_by_id':
                $database->setCurrent(true);
                break;
        }

this checks the routes and sets the needed menu active.

like image 135
NaGeL182 Avatar answered Nov 04 '22 02:11

NaGeL182


It's not documented, but the correct way of doing this is now (ex):

        $menu->addChild('Category', [
            'route' => 'category_show',
            'routeParameters' => ['slug' => $category->getSlug()],
            'extras' => [
                'routes' => [
                    [
                        'route' => 'thread_show',
                        'parameters' => ['categorySlug' => $category->getSlug()]
                    ],
                ],
            ],
        ]);

You can skip the parameters if you don't need them.

like image 26
Nicholas Ruunu Avatar answered Nov 04 '22 00:11

Nicholas Ruunu


When $menu->addChild('Item name', ['route' => 'item_route']), the value ['extras']['routes'][0] = 'item_route' is added to the current child.

So you need to add routes to the ['extra']['routes'] = [] array to child.

It's simple. There are three ways to do this:

  1. I recommend this way. The 'extras' array will be merge (no need to duplicate the current 'route' in 'routes'):

    $menu->addChild('Item name', [
        'route' => 'item_route', // The item link in the menu
        'extras' => [
            // Set current (active) if route matches one of the set (including route from key 'route')
            'routes' => [ 
                'item_route_inner_1',
                'item_route_inner_2',
                'item_route_outside_1',
            ],
        ],
    ]);
    
  2. ->setExtra(key, value) In the 'extras' array, the entire value will be replaced by the key (it can be useful when for some reason you do not need to get to routes path specified in route):

    $menu->addChild('Point name', [
        'route' => 'point_route', // route point (on click)
    ])
    // In the 'extras' array, the entire value will be replaced by the 'routes' key
    ->setExtra(
        'routes', [ // Set current (active) if route matches one of the set
            'point_route', // Can be removed, if necessary
            'point_route_inner_1',
            'point_route_inner_2',
            'point_route_outside_1',
        ]
    );
    
  3. ->setExtras(['routes'] => value) The 'extras' array will be completely replaced. Use extremely carefully!

    $menu->addChild('Point name', [
        'route' => 'point_route', // route point (on click)
    ])
    // The 'extras' array will be completely replaced
    ->setExtras([
        'routes' => [
            'point_route', // Can be removed, if necessary
            'point_route_inner_1',
            'point_route_inner_2',
            'point_route_outside_1',
        ]
    ]);
    
like image 22
Maxim Mandrik Avatar answered Nov 04 '22 02:11

Maxim Mandrik


I've been racking my brain on this problem for a couple of days and I think I came up with the cleanest solution to solve this problem by using my routing configuration and a custom Voter. Since this is about the Symfony Routing component I'm assuming you're using the Symfony framework or have enough knowledge of Symfony to make it work for yourself.

Create a custom Voter

<?php
# src/AppBundle/Menu/RouteKeyVoter.php

namespace AppBundle\Menu;

use Knp\Menu\ItemInterface;
use Knp\Menu\Matcher\Voter\VoterInterface;
use Symfony\Component\HttpFoundation\Request;

/**
 * Voter based on routing configuration
 */
class RouteKeyVoter implements VoterInterface
{
    /**
     * @var Request
     */
    private $request;

    public function __construct(Request $request = null)
    {
        $this->request = $request;
    }

    public function setRequest(Request $request)
    {
        $this->request = $request;
    }

    public function matchItem(ItemInterface $item)
    {
        if (null === $this->request) {
            return null;
        }

        $routeKeys = explode('|', $this->request->attributes->get('_menu_key'));

        foreach ($routeKeys as $key) {
            if (!is_string($key) || $key == '') {
                continue;
            }

            // Compare the key(s) defined in the routing configuration to the name of the menu item
            if ($key == $item->getName()) {
                return true;
            }
        }

        return null;
    }
}

Register the voter in the container

# app/config/services.yml

services:
    # your other services...

    app.menu.route_key_voter:
        class: AppBundle\Menu\RouteKeyVoter
        scope: request
        tags:
            - { name: knp_menu.voter }

<!-- xml -->

<service id="app.menu.route_key_voter" class="AppBundle\Menu\RouteKeyVoter">
    <tag name="knp_menu.voter" request="true"/>
</service>

Note that these haven't been fully tested. Leave a comment if I should improve things.

Configuring the menu keys

Once you've created the custom Voter and added it to the configured Matcher, you can configure the current menu items by adding the _menu_key default to a route. Keys can be seperated by using |.

/**
 * @Route("/articles/{id}/comments",
 *     defaults={
 *         "_menu_key": "article|other_menu_item"
 *     }
 * )
 */
public function commentsAction($id)
{
}

article_comments:
    path:      /articles/{id}/comments
    defaults:
        _controller: AppBundle:Default:comments
        _menu_key:   article|other_menu_item

<route id="article_comments" path="/articles/{id}/comments">
    <default key="_controller">AppBundle:Default:comments</default>
    <default key="_menu_key">article|other_menu_item</default>
</route>

The above configurations would match the following menu item:

$menu->addChild('article', [ // <-- matched name of the menu item
    'label' => 'Articles',
    'route' => 'article_index',
]);
like image 26
Coded Monkey Avatar answered Nov 04 '22 00:11

Coded Monkey