Search code examples
phpsymfonydependency-injectionknpmenubundleknpmenu

Access Container or securityContext or EntityManager from MenuBuilder through RequestVoter


I found this piece of code shared in a Gist (somewhere I lost the link) and I needed something like that so I started to use in my application but I have not yet fully understood and therefore I am having some problems.

I'm trying to create dynamic menus with KnpMenuBundle and dynamic means, at some point I must verify access permissions via database and would be ideal if I could read the routes from controllers but this is another task, perhaps creating an annotation I can do it but I will open another topic when that time comes.

Right now I need to access the SecurityContext to check if the user is logged or not but not know how.

I'm render the menu though RequestVoter (I think) and this is the code:

namespace PlantillaBundle\Menu;

use Knp\Menu\ItemInterface;
use Knp\Menu\Matcher\Voter\VoterInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\Security\Core\SecurityContextInterface; 

class RequestVoter implements VoterInterface {

    private $container;

    private $securityContext; 

    public function __construct(ContainerInterface $container, SecurityContextInterface $securityContext)
    {
        $this->container = $container;
        $this->securityContext = $securityContext;
    }

    public function matchItem(ItemInterface $item)
    {
        if ($item->getUri() === $this->container->get('request')->getRequestUri())
        {
            // URL's completely match
            return true;
        }
        else if ($item->getUri() !== $this->container->get('request')->getBaseUrl() . '/' && (substr($this->container->get('request')->getRequestUri(), 0, strlen($item->getUri())) === $item->getUri()))
        {
            // URL isn't just "/" and the first part of the URL match
            return true;
        }
        return null;
    }

}

All the code related to securityContext was added by me in a attempt to work with it from the menuBuilder. Now this is the code where I'm making the menu:

namespace PlantillaBundle\Menu;

use Knp\Menu\FactoryInterface;
use Symfony\Component\DependencyInjection\ContainerAware;

class MenuBuilder extends ContainerAware {

    public function mainMenu(FactoryInterface $factory, array $options)
    {
        // and here is where I need to access securityContext 
        // and in the near future EntityManger

        $user = $this->securityContext->getToken()->getUser();
        $logged_in = $this->securityContext->isGranted('IS_AUTHENTICATED_FULLY');

        $menu = $factory->createItem('root');
        $menu->setChildrenAttribute('class', 'nav');

        if ($logged_in)
        {
            $menu->addChild('Home', array('route' => 'home'))->setAttribute('icon', 'fa fa-list');
        }
        else
        {
            $menu->addChild('Some Menu');
        }

        return $menu;
    }     

}

But this is complete wrong since I'm not passing securityContext to the method and I don't know how to and I'm getting this error:

An exception has been thrown during the rendering of a template ("Notice: Undefined property: PlantillaBundle\Menu\MenuBuilder::$securityContext in /var/www/html/src/PlantillaBundle/Menu/MenuBuilder.php line 12") in /var/www/html/src/PlantillaBundle/Resources/views/menu.html.twig at line 2.

The voter is defined in services.yml as follow:

plantilla.menu.voter.request:
    class: PlantillaBundle\Menu\RequestVoter
    arguments:
        - @service_container
        - @security.context
    tags:
        - { name: knp_menu.voter }

So, how I inject securityContext (I'll not ask for EntityManager since I asume will be the same procedure) and access it from the menuBuilder?

Update: refactorizing code

So, following @Cerad suggestion I made this changes:

services.yml

services:
    plantilla.menu_builder:
        class: PlantillaBundle\Menu\MenuBuilder
        arguments: ["@knp_menu.factory", "@security.context"]

    plantilla.frontend_menu_builder:
        class: Knp\Menu\MenuItem # the service definition requires setting the class
        factory_service: plantilla.menu_builder
        factory_method: createMainMenu
        arguments: ["@request_stack"]
        tags:
            - { name: knp_menu.menu, alias: frontend_menu } 

MenuBuilder.php

namespace PlantillaBundle\Menu;

use Knp\Menu\FactoryInterface;
use Symfony\Component\HttpFoundation\RequestStack;

class MenuBuilder {

    /**
     * @var Symfony\Component\Form\FormFactory $factory
     */
    private $factory;

    /**
     * @var Symfony\Component\Security\Core\SecurityContext $securityContext
     */
    private $securityContext;

    /**
     * @param FactoryInterface $factory
     */
    public function __construct(FactoryInterface $factory, $securityContext)
    {
        $this->factory = $factory;
        $this->securityContext = $securityContext;
    }

    public function createMainMenu(RequestStack $request)
    {
        $user = $this->securityContext->getToken()->getUser();
        $logged_in = $this->securityContext->isGranted('IS_AUTHENTICATED_FULLY');

        $menu = $this->factory->createItem('root');
        $menu->setChildrenAttribute('class', 'nav');

        if ($logged_in)
        {
            $menu->addChild('Home', array('route' => 'home'))->setAttribute('icon', 'fa fa-list');
        }
        else
        {
            $menu->addChild('Some Menu');
        }

        return $menu;
    }

}

Abd ib my template just render the menu {{ knp_menu_render('frontend_menu') }} but now I loose the FontAwesome part and before it works, why?


Solution

  • Per your comment request, here is what your menu builder might look like:

    namespace PlantillaBundle\Menu;
    
    use Knp\Menu\FactoryInterface;
    
    class MenuBuilder {
    
        protected $securityContext;
    
        public function __construct($securityContext)
        {
            $this->securityContext = $securityContext;
        }
        public function mainMenu(FactoryInterface $factory, array $options)
        {
            // and here is where I need to access securityContext 
            // and in the near future EntityManger
    
            $user = $this->securityContext->getToken()->getUser();
            ...
    
    // services.yml
    plantilla.menu.builder:
        class: PlantillaBundle\Menu\MenuBuilder
        arguments:
            - '@security.context'
    
    // controller
    $menuBuilder = $this->container->get('plantilla.menu.builder');
    

    Notice that there is no need to make the builder container aware since you only need the security context service. You can of course inject the entity manager as well.

    ================================

    With respect to the voter stuff, right now you are only checking to see if a user is logged in. So no real need for voters. But suppose that certain users (administrators etc) had access to additional menu items. You can move all the security checking logic to the voter. Your menu builder code might then look like:

    if ($this->securityContext->isGranted('view','homeMenuItem')
    {
        $menu->addChild('Home', array('route' ...
    

    In other words, you can get finer controller over who gets what menu item.

    But get your MenuBuilder working first then add the voter stuff if needed.