I am making an administration menu bundle. I want other bundles to be able to add menu items to the bundle, but I also want the menu items to be deleted when a bundle is removed. What would be the best way to do this?
I could create a 'regenerate admin menu' action that scans all bundles for a certain YML and then store that in cache or database.
Is there a better way to do this ?
You could use tags to find services from other bundles (or even the same) and use them to build menu from them in compiler pass.
In this example I will assume you have your menu defined as a service (I will use service id acme_menu.menu).
// src/Acme/MenuBundle/DependencyInjection/Compiler/BuildMenuCompilerPass.php
namespace Acme\MenuBundle\DependencyInjection\Compiler;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\Reference;
class BuildMenuCompilerPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container)
{
if (!$container->hasDefinition('acme_menu.menu')) {
return;
}
$definition = $container->getDefinition('acme_menu.menu');
$taggedServices = $container->findTaggedServiceIds('acme_menu.item');
foreach ($taggedServices as $id => $attributes) {
$definition->addMethodCall(
'addMenuItem',
array(new Reference($id))
);
}
}
}
Register it with your menu bundle:
// src/Acme/MenuBundle/AcmeMenuBundle.php
namespace Acme\MenuBundle;
use Symfony\Component\HttpKernel\Bundle\Bundle;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Acme\MenuBundle\DependencyInjection\Compiler\BuildMenuCompilerPass;
class AcmeMenuBundle extends Bundle
{
public function build(ContainerBuilder $container)
{
parent::build($container);
$container->addCompilerPass(new BuildMenuCompilerPass());
}
}
And every service tagged as acme_menu.item will be added to the menu - them method addMenuItem will be called on the menu on it's creation with the tagged service as a parameter. So simply define:
# services.yml
services:
acme_demo.menu.item1:
# ...
tags:
- { name: acme_menu.item }
acme_demo.menu.item2:
# ...
tags:
- { name: acme_menu.item }
Related
How can I have a global variable in symfony template?
I did read this
but I prefer to fetch parameter from database, I think this service will be loaded on startup before it can fetch anything from db. Is it possible to do a trick to do so?
EDIT: Update in 2019 with Symfony 3.4+ syntax.
Create a Twig extension where you inject the Entity Manager:
Fuz/AppBundle/Twig/Extension/DatabaseGlobalsExtension.php
<?php
namespace Fuz\AppBundle\Twig\Extension;
use Doctrine\ORM\EntityManager;
use Twig\Extension\AbstractExtension;
use Twig\Extension\GlobalsInterface;
class DatabaseGlobalsExtension extends AbstractExtension implements GlobalsInterface
{
protected $em;
public function __construct(EntityManager $em)
{
$this->em = $em;
}
public function getGlobals()
{
return [
'myVariable' => $this->em->getRepository(FuzAppBundle\Entity::class)->getSomeData(),
];
}
}
Register your extension in your Fuz/AppBundle/Resources/config/services.yml:
services:
_defaults:
autowire: true
autoconfigure: true
Fuz\AppBundle\Twig\Extension\DatabaseGlobalsExtension: ~
Now you can do the requests you want using the entity manager.
Don't forget to replace paths and namespaces to match with your application.
As of this day, the class signature has changed. You must implement \ Twig_Extension_GlobalsInterface, without it, your globals won't show up.
class MyTwigExtension extends \Twig_Extension implements Twig_Extension_GlobalsInterface
{ }
Bye!
you can register a twig extension
services:
twig_extension:
class: Acme\DemoBundle\Extension\TwigExtension
arguments: [#doctrine]
tags:
- { name: twig.extension }
And then in the TwigExtension you can do as follows:
class TwigExtension extends \Twig_Extension
{
public function getGlobals() {
return array(
// your key => values to make global
);
}
}
So you could get a value from the database in this TwigExtension and pass it to the template with the getGlobals() function
Stay away from global variables.
Instead make a custom twig extension then inject the database connection as a parameter.
Something like:
services:
acme.twig.acme_extension:
class: Acme\DemoBundle\Twig\AcmeExtension
arguments: [#doctrine.dbal.default_connection]
tags:
- { name: twig.extension }
Details:
http://symfony.com/doc/current/cookbook/templating/twig_extension.html
namespace Acme\AdminBundle\Admin;
use Sonata\AdminBundle\Admin\Admin;
use Sonata\AdminBundle\Form\FormMapper;
use Sonata\AdminBundle\Datagrid\DatagridMapper;
use Sonata\AdminBundle\Datagrid\ListMapper;
use Sonata\AdminBundle\Show\ShowMapper;
use FOS\UserBundle\Model\UserManagerInterface;
class LessonAdmin extends Admin
{
public function
{
//I have tried these, but in vain.
$items = $this->container->getParameter('items');
or
$items = $this->getContainer()->getParameter('items');
I think this problem is related with Dependency Injection though, still unclear for me.
How can I inject getContainer item here??
In SonataAdmin, the DI container can be fetched from the Admin configuration pool:
<?php
use Sonata\AdminBundle\Admin\Admin;
class YourAdmin extends Admin
{
protected function yourAdminMethod()
{
$this->getConfigurationPool()->getContainer()->getParameter('your_parameter');
}
}
`
You can also inject parameters through setter in admin service definition:
services:
sonata.admin.lesson:
class: Acme\AdminBundle\Admin\LessonAdmin
tags:
- { name: sonata.admin, manager_type: orm, ...}
arguments:
- ~
- Acme\AppBundle\Entity\Lesson
- ~
calls:
- [ setItems, ["%items%"]]
And in your admin class:
class LessonAdmin extends Admin
{
public function setItems($items)
{
$this->items = $items;
}
In this way your parameter is accessible in your whole admin class.
I'd like to display new notifications on every page of my symfony 2 webapplication.
I was advised to use a Twig Extension for this. I've created a function getFriendRequests in that extension, but I don't know if it's good practice to get data through my custom repository in the twig extension: Right now it's giving me the error, that it can't find the getDoctrine method.
<?php
namespace Tennisconnect\DashboardBundle\Extension;
class NotificationTwigExtension extends \Twig_Extension
{
public function getFriendRequests($user)
{
$users = $this->getDoctrine()
->getRepository('TennisconnectUserBundle:User')
->getFriendRequests();
return count($users);
}
public function getName()
{
return 'notification';
}
public function getFunctions()
{
return array(
'getFriendRequests' => new \Twig_Function_Method($this, 'getFriendRequests'));
}
}
I don't think it is so bad to fetch your data directly from your twig extension. After all, if you don't do it here, you will need to fetch those records before and then pass them to the extension for display anyway.
The important point is to do the DQL/SQL stuff in the repository like you are already doing. This is important to separate database statements from other part of your project.
The problem you having is that the method getDoctrine does not exist in this class. From what I understand, you took this code from a controller which extends the FrameworkBundle base controller. The base controller of the FrameworkBundle defines this method.
To overcome this problem, you will have to inject the correct service into your extension. This is based on the dependency injection container. You certainly defined a service for your twig extension, something like this definition:
services:
acme.twig.extension.notification:
class: Acme\WebsiteBundle\Twig\Extension\NotificationExtension
tags:
- { name: twig.extension }
The trick is now to inject the dependencies you need like this:
services:
acme.twig.extension.notification:
class: Acme\WebsiteBundle\Twig\Extension\NotificationExtension
arguments:
doctrine: "#doctrine"
tags:
- { name: twig.extension }
And then, in you extension, you define a constructor that receives the doctrine dependency:
use Symfony\Bridge\Doctrine\RegistryInterface;
class NotificationTwigExtension extends \Twig_Extension
{
protected $doctrine;
public function __construct(RegistryInterface $doctrine)
{
$this->doctrine = $doctrine;
}
// Now you can do $this->doctrine->getRepository('TennisconnectUserBundle:User')
// Rest of twig extension
}
This is the concept of dependency injection. You can see another question I answered sometime ago about accessing services outside controller: here
Hope this helps.
Regards,
Matt
The same but with mongo:
in config.yml
services:
user.twig.extension:
class: MiProject\CoreBundle\Twig\Extension\MiFileExtension
arguments:
doctrine: "#doctrine.odm.mongodb.document_manager"
tags:
- { name: twig.extension }
and in your Twig\Extensions\MiFile.php
<?php
namespace MiProject\CoreBundle\Twig\Extension;
use Symfony\Component\HttpKernel\KernelInterface;
class MiFileExtension extends \Twig_Extension
{
protected $doctrine;
public function __construct( $doctrine){
$this->doctrine = $doctrine;
}
public function getTransactionsAmount($user_id){
return $results = $this->doctrine
->createQueryBuilder('MiProjectCoreBundle:Transaction')
->hydrate(false)
->getQuery()
->count();
}
Rest of mi code ...
}
I've got an Entity that I want to associate with the users session.
I created a service so that I could reach this info from where ever.
in the service i save the entities id in an session variable
and in the getEntity() method i get the session variable and with doctrine find the entity and return it.
this way to the template i should be able to call {{ myservice.myentity.myproperty }}
The problem is that myservice is used all over the place, and I don't want to have to get it in every since Action and append it to the view array.
Is there a way to make a service accessible from all views like the session {{ app.session }} ?
The solution
By creating a custom service i can get to that from where ever by using
$this->get('myservice');
this is all done by http://symfony.com/doc/current/book/service_container.html
But I'll give you some demo code.
The Service
This first snippet is the actual service
<?php
namespace MyBundle\AppBundle\Extensions;
use Symfony\Component\HttpFoundation\Session;
use Doctrine\ORM\EntityManager;
use MyBundle\AppBundle\Entity\Patient;
class AppState
{
protected $session;
protected $em;
function __construct(Session $session, EntityManager $em)
{
$this->session = $session;
$this->em = $em;
}
public function getPatient()
{
$id = $this->session->get('patient');
return isset($id) ? $em->getRepository('MyBundleStoreBundle:Patient')->find($id) : null;
}
}
Register it in you config.yml with something like this
services:
appstate:
class: MyBundle\AppBundle\Extensions\AppState
arguments: [#session, #doctrine.orm.entity_manager]
Now we can as I said before, get the service in our controllers with
$this->get('myservice');
But since this is a global service I didn't want to have to do this in every controller and every action
public function myAction()
{
$appstate = $this->get('appstate');
return array(
'appstate' => $appstate
);
}
so now we go create a Twig_Extension
Twig Extension
<?php
namespace MyBundle\AppBundle\Extensions;
use MyBundle\AppBundle\Extensions\AppState;
class AppStateExtension extends \Twig_Extension
{
protected $appState;
function __construct(AppState $appState) {
$this->appState = $appState;
}
public function getGlobals() {
return array(
'appstate' => $this->appState
);
}
public function getName()
{
return 'appstate';
}
}
By using dependency injection we now have the AppState Service that we created in the twig extension named appstate
Now we register that with the symfony (again inside the services section inside the config-file)
twig.extension.appstate:
class: MyBundle\AppBundle\Extensions\AppStateExtension
arguments: [#appstate]
tags:
- { name: twig.extension }
The important part being the "tags", since this is what symfony uses to find all twig extensions
We are now set to use our appstate in our twig templates by the variable name
{{ appstate.patient }}
or
{{ appstate.getPatient() }}
Awesome!
Maybe you can try this in your action ? : $this->container->get('templating')->addGlobal($name, $value)
I wrote custom Twig loader that fetch templates from database and it works in Twig "standalone" library.
Now i want to use that in Symfony2 but can't find where to change Twig loader via Symfony2 settings.
Thx in advance for any tips on that
Register your own twig loader + tell Twig_Loader_Chain to try loading with your loader at first. You can create and add as many loaders to your Twig_Loader_Chain as you want.
services:
Acme.corebundle.twig.loader.filesystem:
class: Acme\CoreBundle\Twig\Loader\Filesystem
tags:
- { name: templating.loader }
Acme.corebundle.twig_chain_loader:
class: Twig_Loader_Chain
calls:
- [ addLoader, [#Acme.corebundle.twig.loader.filesystem] ]
- [ addLoader, [#twig.loader] ]
Now you should create your loader. Twig loaders have to implement Twig_LoaderInterface.
Acme/CoreBundle/Twig/Loader/Filesystem.php
PSEUDOCODE:
namespace Acme\CoreBundle\Twig\Loader;
use Twig_LoaderInterface;
class Filesystem implements Twig_LoaderInterface {
/**
* {#inheritdoc}
*/
public function getSource($name)
{
//code...
}
/**
* {#inheritdoc}
*/
protected function findTemplate($name)
{
//code...
}
/**
* {#inheritdoc}
*/
public function isFresh($template, $time)
{
//code...
}
//...
}
Now we have defined our services and created a new loader.
Problem is that Twig doesn't know about our new Twig_Loader and still uses its own -default- "twig.loader".
To check run on CLI:
app/console container:debug twig.loader
In order to modify services outside of your own bundle you have to use CompilerPasses.
Create our own that assigns your loader service to the twig environment:
Acme/CoreBundle/DependencyInjection/Compiler/TwigFileLoaderPass.php
<?php
namespace Acme\CoreBundle\DependencyInjection\Compiler;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
class TwigFileLoaderPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container)
{
$definition = $container->getDefinition('twig');
$definition->addMethodCall('setLoader', array(new Reference('Acme.corebundle.twig_chain_loader')));
}
}
There is the "addMethodCall" call which does nothing more than defining a setter injection as in the service definitions. The difference is that in a compiler pass you can access every service, not only your own ones. As you can see the chain loader has been defined as the new loader for the twig environment.
To Accomplish this task you have to tell Symfony that it should use this compiler pass. Compiler passes can be added in your bundle class:
Acme/CoreBundle/AcmeCoreBundle.php
<?php
namespace Acme\CoreBundle;
use Symfony\Component\HttpKernel\Bundle\Bundle;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Acme\CoreBundle\DependencyInjection\Compiler\TwigFileLoaderPass;
class AcmeCoreBundle extends Bundle
{
public function build(ContainerBuilder $container)
{
parent::build($container);
$container->addCompilerPass(new TwigFileLoaderPass());
}
}
If the corresponding file does not exist your new Twig_Loader_Filesystem throws an error and the chain loader continues with default twig loader as fallback.
Have a look at this page at GitHub. Specially <parameter key="twig.loader.class">Symfony\Bundle\TwigBundle\Loader\Loader</parameter>
You can configure this key in your config.yml
To overwrite the key in your config.yml you need to do it under services not twig as it's not support in the configuration parser at the moment (2.0.9)
twig:
cache:...
debug:...
...
services:
twig.loader:
class: Acme\CoreBundle\Twig\Loader\FilesystemLoader