0

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?

ReynierPM
  • 17,594
  • 53
  • 193
  • 363
  • Merely making MenuBuilder ContainerAware does not automatically cause the container to be in injected. You need to do: $menuBuilder->setContainer($container) after instantiating. Better yet, make MenuBuilder a service and add "calls: [['setContainer', ['@service_container']]]" to the service definition. Finally, if possible avoid injecting the container and just inject the services you need such as the security context. – Cerad Oct 02 '14 at 13:35
  • @Cerad can you please leave me an example? I don't know how to achieve what you said before! And yes, I can avoid to inject the whole Service Container instead I should inject only `@security.context` and EntityManager later but will be nice if I leave this part ready from here – ReynierPM Oct 02 '14 at 14:02

2 Answers2

2

Your menu builder is ContainerAware, so I guess that in it you should access the SecurityContext via $this->getContainer()->get('security.context').

And you haven't supplied any use cases for the voter class, so I'm guessing you're not using the matchItem method.

You should definitely try to restructure your services so that the dependencies are obvious.

kix
  • 3,290
  • 27
  • 39
  • Is not working `Attempted to call method "getContainer" on class "PlantillaBundle\Menu\MenuBuilder" in /var/www/html/src/PlantillaBundle/Menu/MenuBuilder.php line 12. Did you mean to call: "setContainer"? ` and how you suggest me to restructure the dependencies? – ReynierPM Oct 02 '14 at 11:40
  • Since I'm still learning and I'm trying to understand somethings on Symfony I'm not clear at all around Voters so in others words I don't know watch `matchItem` method does, could you help me to understand this part with some explanation? – ReynierPM Oct 02 '14 at 11:42
  • 1
    Well, you've implemented the `ContainerAware` interface, thus you should know which property or method returns a container that was injected. On security voters. The doc is really rather clear, you should check it out: http://symfony.com/doc/current/cookbook/security/voters_data_permission.html Basically, a voter is `SecurityContext`'s slave class, it tells what should `$securityContext->isGranted()` return when given an object that the voter knows. – kix Oct 02 '14 at 11:44
  • Also the `matchItem` method you've implemented has nothing to do with `VoterInterface`. Note the interface has only three methods: https://github.com/symfony/symfony/blob/master/src/Symfony/Component/Security/Core/Authorization/Voter/VoterInterface.php – kix Oct 02 '14 at 11:59
  • I think you're talking about something complete different than what I'm talking. If you take a closer look to the `RequestVoter` class you should see it extends from [Knp\Menu\Matcher\Voter\VoterInterface](https://github.com/KnpLabs/KnpMenu/blob/master/src/Knp/Menu/Matcher/Voter/VoterInterface.php) and not from this [Symfony\Component\Security\Core\Authorization\Voter\VoterInterface](http://api.symfony.com/2.5/Symfony/Component/Security/Core/Authorization/Voter/VoterInterface.html) so yes, the KnpMenu Voter – ReynierPM Oct 02 '14 at 14:01
  • @ReynierPM, ouch, my bad, sure. – kix Oct 02 '14 at 15:44
1

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.

Cerad
  • 48,157
  • 8
  • 90
  • 92