Symfony Twig Extension breaks other service - Is templating done before security? - php

I am working on a Symfony 2.7 WebApp. One of the bundles I created includes a service that offer some user related stuff, e.g. userHasPurchases().
Problem is, that including a Twig Extesion breaks another service:
AppShopService
namespace AppShopBundle\Service;
use AppBundle\Entity\User;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
...
class AppShopService {
protected $user;
public function __construct(TokenStorageInterface $tokenStorage, ...) {
$this->user = $tokenStorage->getToken() ? $tokenStorage->getToken()->getUser() : null;
...
}
public function userHasPurchases(User $user) {
$user = $user ? $user : $this->user;
$result = $user...
return result;
}
}
AppShopBundle\Resources\config\services.yml
services:
app_shop.service:
class: AppShopBundle\Service\AppShopService
arguments:
- "#security.token_storage"
- ...
So far everything works fine: The AppShopServices is created with the current user and userHasPurchases() work as expected.
Now I have add a Twig Extension to be able to use userHasPurchases() within my templates:
Twig Extension
namespace AppShopBundle\Twig;
use AppShopBundle\Service\AppShopService;
class AppShopExtension extends \Twig_Extension {
private $shopService;
public function __construct(AppShopService $shopService) {
$this->shopService = $shopService;
}
public function getName() {
return 'app_shop_bundle_extension';
}
public function getFunctions() {
$functions = array();
$functions[] = new \Twig_SimpleFunction('userHasPurchases', array(
$this,
'userHasPurchases'
));
return $functions;
}
public function userHasPurchases($user) {
return $this->shopService->userHasPurchases($user);
}
}
Including Extension in AppShopBundle\Resources\config\services.yml
services:
app_shop.service:
class: AppShopBundle\Service\AppShopService
arguments:
- "#security.token_storage"
- ...
app_shop.twig_extension:
class: AppShopBundle\Twig\AppShopExtension
arguments:
- "#app_shop.service"
tags:
- { name: twig.extension }
After icluding the Twig Extension, AppShopService and its method userHasPurchases does not work any more. Problem is, that the constructor of AppShopService does not set user anymore since $tokenStorage->getToken() now returns null.
How is this possible? I have changed nothing except including the Twig Extension. As soon as I remove the Twig Extension from services.yml everything works correctly again.
My only guess is, that the creation fo the Twig Extension is done before any security. But why?
Any idea what might be wrong here?

don't interact with the tokenStorage in the constructor but only in the userHasPurchases method.
namespace AppShopBundle\Service;
use AppBundle\Entity\User;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
...
class AppShopService {
protected $tokenStorage;
public function __construct(TokenStorageInterface $tokenStorage, ...) {
$this->tokenStorage = $tokenStorage;
}
public function userHasPurchases(User $user) {
$user = $this->tokenStorage->getToken() ? $this->tokenStorage->getToken()->getUser() : null;
$result = $user...
return result;
}
}
Hope this help

Related

Symfony 4 TwigFunction does not get registered

I am writing a Twig function in Symfony 4 but I cannot get it to work...
The extension class
<?php
namespace App\Twig;
use App\Utils\XXX;
use Twig\Extension\AbstractExtension;
use Twig\TwigFunction;
class XXXExtension extends AbstractExtension
{
/**
* #return array|TwigFunction|TwigFunction[]
*/
public function getFunctions()
{
return new TwigFunction('showControllerName', [$this, 'showControllerName']);
}
public function showControllerName($sControllerPath)
{
return XXX::getControllerName($sControllerPath);
}
}
I have autowire set to true in services.yaml but just in case i tried with this also:
App\Twig\XXXExtension:
public: true
tags:
- { name: twig.extension }
usage in html.twig
{% set controllerName = showControllerName(app.request.get('_controller')) %}
and the response i get after this is:
HTTP 500 Internal Server Error
Unknown "showControllerName" function.
You need to return an array of functions, you are only returning one.
...
public function getFunctions()
{
return [
new TwigFunction('showControllerName', [$this, 'showControllerName']),
];
}
...

Custom config with service/di references

For my project I want to specify some custom configuration. I have a bunch of 'mappers' that have some properties and will refer to other services.
For example, I would like my config to look like this:
self_service:
mappers:
branche_vertalingen:
data_collector: "#self_service.branche_vertalingen.data_collector"
data_loader: "#self_service.branche_vertalingen.data_loader"
map_data: SelfServiceBundle\Entity\BrancheVertalingMapData
Where self_service is the bundle name, mappers is the 'container' where all the mappers are defined. And branche_vertalingen is one of the defined mappers, there can (and will) be many more. At the moment, each mapper has a data_collector and a data_loader that refer to services defined in the bundle's services.yml, and a map_data property which refers to an entity's class name.
I have put this configuration in SelfServiceBundle/Resources/config/config.yml and import it in app/config/config.yml.
I have create a SelfServiceExtension class according to this article. In the extension's load() method I receive my defined configuration as an array. So far, so good.
The problem I am having is that the value for data_collector I receive is just the defined string, and not the service I was expecting. No problem, I thought. I have a $container available, I will just look it up, but I can't get the service there.
The question: How do I make sure I can get the service I reference in the config?
I had already tried doing the same in a parameters block so that I wouldn't even need a bundle Extension, but doing that I got this error: You cannot dump a container with parameters that contain references to other services. So after that I tried to do it via an Extension.
As I wrote in the comments, I think tagged services are a good fit for this. It allows you to very easily add or remove mappers just by tagging a service. This way there's no hard requirement for all mappers to live at the same place or similar.
Using interfaces to ensure that everything is wired correctly also allows for easy extension.
To see how this can even incorporate your initial idea, see the example at the end of this answer.
usage
/** #var $mapperManager MapperManager */
$mapperManager = $this->get('app.mapper_manager');
dump($mapperManager);
foreach ($mapperManager->getMappers('branche_vertalingen') as $mapper) {
dump($mapper);
}
implementation
(closely following the official docs):
service.yml
services:
app.mapper_manager:
class: AppBundle\Mapper\Manager
# mappers
app.mapper_1:
public: false
class: AppBundle\Mapper\DefaultMapper
arguments:
- "a"
- "b"
- SelfServiceBundle\Entity\BrancheVertalingMapData
tags:
- { name: app.mapper, branch: branche_vertalingen }
app.mapper_2:
public: false
class: AppBundle\Mapper\DefaultMapper
arguments:
- "c"
- "d"
- SelfServiceBundle\Entity\BrancheVertalingMapData
tags:
- { name: app.mapper, branch: branche_vertalingen }
app.mapper_3:
public: false
class: AppBundle\Mapper\DefaultMapper
arguments:
- "e"
- "f"
- SelfServiceBundle\Entity\BrancheVertalingMapData
tags:
- { name: app.mapper, branch: other_branch }
the compiler pass:
<?php
// src/AppBundle/DependencyInjection/Compiler/MapperCompilerPass.php
namespace AppBundle\DependencyInjection\Compiler;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\Reference;
class MapperCompilerPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container)
{
if (!$container->has('app.mapper_manager')) {
return;
}
$definition = $container->findDefinition('app.mapper_manager');
$taggedServices = $container->findTaggedServiceIds('app.mapper');
foreach ($taggedServices as $id => $tags) {
foreach ($tags as $attributes) {
$definition->addMethodCall('addMapper', [$attributes['branch'], new Reference($id)]);
}
}
}
}
using the compiler pass:
<?php
// src/AppBundle/AppBundle.php
namespace AppBundle;
use AppBundle\DependencyInjection\Compiler\MapperCompilerPass;
use Symfony\Component\HttpKernel\Bundle\Bundle;
use Symfony\Component\DependencyInjection\ContainerBuilder;
class AppBundle extends Bundle
{
public function build(ContainerBuilder $container)
{
$container->addCompilerPass(new MapperCompilerPass());
}
}
a simple manager class (call it whatever fits better):
<?php
// src/AppBundle/Mapper/Manager.php
namespace AppBundle\Mapper;
class Manager
{
private $mappers = [];
public function addMapper($branch, MapperInterface $mapper)
{
if (!array_key_exists($branch, $this->mappers)) {
$this->mappers[$branch] = [];
}
$this->mappers[$branch][] = $mapper;
}
public function getMappers($branch)
{
if (!array_key_exists($branch, $this->mappers)) {
// handle invalid access
// throw new \InvalidArgumentException('%message%');
}
return $this->mappers[$branch];
}
}
a default mapper class (this is actually not required, but could make things easier to start with):
<?php
// src/AppBundle/Mapper/DefaultMapper.php
namespace AppBundle\Mapper;
class DefaultMapper implements MapperInterface
{
private $dataCollector;
private $dataLoader;
private $mapData;
public function __construct($dataCollector, $dataLoader, $mapData)
{
$this->dataCollector = $dataCollector;
$this->dataLoader = $dataLoader;
$this->mapData = $mapData;
}
public function getDataCollector()
{
return $this->dataCollector;
}
public function getDataLoader()
{
return $this->dataLoader;
}
public function getMapData()
{
return $this->mapData;
}
}
and finaly a simple interface to use with the data mappers:
<?php
// src/AppBundle/Mapper/MapperInterface.php
namespace AppBundle\Mapper;
interface MapperInterface
{
public function getDataCollector();
public function getDataLoader();
public function getMapData();
}
a little extra
With an additional compiler pass (or only, see code-comments) you could also extend the above solution:
using an extra compiler pass, like:
<?php
// src/AppBundle/DependencyInjection/Compiler/MapperCollectionCompilerPass.php
namespace AppBundle\DependencyInjection\Compiler;
use AppBundle\Mapper\DefaultMapper;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\Definition;
class MapperCollectionCompilerPass implements CompilerPassInterface
{
private $parameterName;
public function __construct($parameterName)
{
$this->parameterName = $parameterName;
}
public function process(ContainerBuilder $container)
{
if (!$container->has('app.mapper_manager')) {
return;
}
if (!$container->hasParameter($this->parameterName)) {
return;
}
$definition = $container->findDefinition('app.mapper_manager');
$mappers = $container->getParameter($this->parameterName);
foreach ($mappers as $branch => $meta) {
$mapper = new Definition(DefaultMapper::class, [
new Reference($meta['data_collector']),
new Reference($meta['data_loader']),
$meta['map_data'],
]);
$mapper
->setPublic(false)
->addTag('app.mapper', ['branch' => $branch])
;
$container->addDefinitions([$mapper]);
// If you don't want to use tags, simply add the 'addMethodCall'
// from MapperCompilerPass here
// $definition->addMethodCall('addMapper', [$branch, $mapper]);
}
}
}
adding it to the bundle:
<?php
// src/AppBundle/AppBundle.php
namespace AppBundle;
use Symfony\Component\HttpKernel\Bundle\Bundle;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use AppBundle\DependencyInjection\Compiler\MapperCompilerPass;
use AppBundle\DependencyInjection\Compiler\MapperCollectionCompilerPass;
class AppBundle extends Bundle
{
public function build(ContainerBuilder $container)
{
$container->addCompilerPass(new MapperCollectionCompilerPass('mappers'));
$container->addCompilerPass(new MapperCompilerPass());
}
}
and adding the config:
# app/config/services.yml
parameters:
mappers:
branche_vertalingen:
# !note the missing #
data_collector: app.some_service
data_loader: app.some_service
map_data: SelfServiceBundle\Entity\BrancheVertalingMapData

Call controller or view (twig file) from self defined twig extension in smyfony2

i've got a problem in symfony2 at the moment and i don't know how i can solve it.
Within a self defined new twig extension I want to call a controller or or a view (a twig file).
How is the correct way to realize this? Can you help me? I've read many symfony2 internet pages but i didn't found a good programming approach for me.
For better understanding why i want to do something like this, here is an exmaple what is my idea:
I want to source out some html code into an separate view. This new view is embedded in another view by calling the twig extension.
So how can i realize this?
Thanxs for your help.
As you are using Symfony2, you can inject the templating service to your Twig extension and then call the ->render method.
The extension
<?php
namespace YourPackage\YourBundle\Twig\Extension;
use Symfony\Component\Templating\EngineInterface;
class Test_Extension extends \Twig_Extension
{
protected $templating;
public function __construct(EngineInterface $templating)
{
$this->templating = $templating;
}
public function getFunctions()
{
return array(
new \Twig_SimpleFunction('my_test', array($this->myTest()), array('is_safe' => array('html')))
);
}
public function myTest()
{
// do some stuffs
$data = $this->templating->render("SomeBundle:Directory:file.html.twig");
// ...
return $data;
}
public function getName()
{
return 'test';
}
}
services.yml
# src/YourPackage/YourBUndle/Resources/config/services.yml
services:
test.test_extension:
class: YourPackage\YourBundle\Twig\Extension\TestExtension
arguments: ['#templating']
tags:
- { name: twig.extension }

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?
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.
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.

How to get Doctrine to work inside a helper function on Symfony2

I need to get doctrine working inside my helper, im trying to use like i normaly do in a controller:
$giftRepository = $this->getDoctrine( )->getRepository( 'DonePunctisBundle:Gift' );
But this gave me:
FATAL ERROR: CALL TO UNDEFINED METHOD
DONE\PUNCTISBUNDLE\HELPER\UTILITYHELPER::GETDOCTRINE() IN
/VAR/WWW/VHOSTS/PUNCTIS.COM/HTTPDOCS/SRC/DONE/PUNCTISBUNDLE/HELPER/UTILITYHELPER.PH
What Im missing here?
EDIT:
services file
services:
templating.helper.utility:
class: Done\PunctisBundle\Helper\UtilityHelper
arguments: [#service_container]
tags:
- { name: templating.helper, alias: utility }
Firts lines of helper file
<?php
namespace Done\PunctisBundle\Helper;
use Symfony\Component\Templating\Helper\Helper;
use Symfony\Component\Templating\EngineInterface;
class UtilityHelper extends Helper {
/*
* Dependency injection
*/
private $container;
public function __construct( $container )
{
$this->container = $container;
}
The problem here is that your Helper class is not container-aware; that is, it has no idea about all the services Symfony has loaded (monolog, twig, ...and doctrine).
You fix this by passing "doctrine" to it. This is called Dependency Injection, and is one of the core things that makes Symfony awesome. Here's how it works:
First, give your Helper class a place for the Doctrine service to live, and require it in the Helper's constructor:
class UtilityHelper
{
private $doctrine;
public function __construct($doctrine)
{
$this->doctrine = $doctrine;
}
public function doSomething()
{
// Do something here
}
}
Then, you use services.yml to define how Symfony should construct that instance of Helper:
services:
helper:
class: Done\PunctisBundle\Helper\UtilityHelper
arguments: [#doctrine]
In this case, #doctrine is a placeholder that means "insert the Doctrine service here".
So now, in your Controller, or in anything else that is container-aware, you can get access to Doctrine through the Helper class like this:
class SomeController()
{
public function someAction()
{
$this->get("helper")->doctrine->getRepository(...);
}
}
EDIT
After looking at your edit, it appears that you're injecting the entire service container into the Helper class. That's not a best practice -- you should only inject what you need. However, you can still do it:
services.yml
services:
helper:
class: Done\PunctisBundle\Helper\UtilityHelper
arguments: [#service_container]
UtilityHelper.php
class UtilityHelper
{
private $container;
public function __construct($container)
{
$this->container = $container;
}
public function doSomething()
{
// This won't work, because UtilityHelper doesn't have a getDoctrine() method:
// $this->getDoctrine()->getRepository(...)
// Instead, think about what you have access to...
$container = $this->container;
// Now, you need to get Doctrine
// This won't work... getDoctrine() is a shortcut method, available only in a Controller
// $container->getDoctrine()->getRepository(...)
$container->get("doctrine")->getRepository(...)
}
}
I've included a few comments there that highlight some common pitfalls. Hope this helps.
In Helper, Services etc you cannot use it like in actions.
You need to pass it like argument to youre Helper via service description in conf file(services.yml or *.xml).
Example:
services:
%service_name%:
class: %path_to_youre_helper_class%
arguments: [#doctrine.orm.entity_manager]
tags:
- { name: %name% }
And dont forget catch it in __construct of youre Helper.
Example:
use Doctrine\ORM\EntityManager;
....
private $em;
public function __construct(EntityManager $em)
{
$this->em = $em;
}
You can use it like:
public function myMethod()
{
$repo = $this->em->getRepository('DonePunctisBundle:Gift');
}

Categories