Does somebody know how to create a notification system for the menu items in the sidebar?
For example if you have a sidebar entry
Articles
and in the background, a new article has been added (e.g. by importing via a sql script). Then the menu entry should be displayed as
Articles (1)
Is there a tutorial for my concern?
You can modify the sidebar menu items.
To do this, you must create a listener that configure the menu in the manner that you want. You can do this with this code:
app.menu_listener:
class: AppBundle\EventListener\MenuBuilderListener
tags:
- { name: kernel.event_listener, event: sonata.admin.event.configure.menu.sidebar, method: addMenuItems }
calls:
- [ setDependencies, [ #doctrine.orm.entity_manager ] ]
After that you can write the class that modify the menu:
namespace AppBundle\EventListener;
use AppBundle\Entity\Configuration;
use Sonata\AdminBundle\Event\ConfigureMenuEvent;
use Doctrine\ORM\EntityManager;
class MenuBuilderListener {
/** #var EntityManager $em */
private $em;
public function addMenuItems(ConfigureMenuEvent $event)
{
$articles = $this->em->getRepo('AppBundle:Article')->findAll();
$menu = $event->getMenu();
$articleMenu = $menu->getChild('sonata.admin.group.articles');
$articleMenu->setLabel('Articles <span>' . $articles->count() . '</span>')
}
public function setDependencies(EntityManager $em, Translator $translator) {
$this->em = $em;
}
}
This is only an example, but is the way that i will take if i need to do this feature, i hope this may help you
You have more info about this here: https://sonata-project.org/bundles/admin/master/doc/cookbook/recipe_knp_menu.html
Related
I try to override layout template in Sonata Admin but depends of logged user. If logged user belong to group customers has some ROLE - show other layout.
I want change -
layout" => "#SonataAdmin/standard_layout.html.twig"
Where is best place to do it ?
I found that i can do this in admin class - override getTemplate.
But is possible to do this is some listener and switch globaly without edit admin classes ?
UPDATE 1
i create class
class SonataTemplateRegistry implements MutableTemplateRegistryInterface
{
/**
* #var string[]
*/
private $templates = [];
/**
* #param string[] $templates
* #param ContactService $contactService
*/
public function __construct(array $templates = [], ContactService $contactService)
{
$templates['layout']= '#SonataAdmin/layout1.html.twig';
// $templates['layout']= '#SonataAdmin/standard_layout.html.twig';
// echo '<pre>'; var_dump($templates); die();
$this->templates = $templates;
}
register it
sonata.admin.global_template_registry:
class: App\Service\SonataTemplateRegistry
public: true
arguments: ['%sonata.admin.configuration.templates%', '#mea.contact']
class is fired - die() show templates but main template is not changed when i change here.
Update 2
in admin class when i get layout template i get correct #SonataAdmin/layout1.html.twig
protected function configureListFields(ListMapper $listMapper)
{
var_dump($this->configurationPool->getTemplate('layout'));
but it is not loaded, still see #SonataAdmin/standard_layout.html.twig
UPDATE 3
I found a strange behavior - main page sonata admin - switching template works but already under the pages use the default template
UPDATE 4
I found something interesting , each admin panel has sub service like here :
php bin/console debug:container |grep app.admin.social
app.admin.social.accounts App\SocialManager\Admin\SocialAccountAdmin
app.admin.social.accounts.template_registry Sonata\AdminBundle\Templating\TemplateRegistry
app.admin.social.order App\SocialManager\Admin\SocialManagementOrderAdmin
app.admin.social.order.template_registry Sonata\AdminBundle\Templating\TemplateRegistry
i override parameters :
parameters:
sonata.admin.global_template_registry: App\Service\SonataTemplateRegistry
and service
sonata.admin.global_template_registry:
class: App\Service\SonataTemplateRegistry
public: true
arguments: ['%sonata.admin.configuration.templates%', '#mea.contact']
so why sonata still use Sonata\AdminBundle\Templating\TemplateRegistry
protected function configureListFields(ListMapper $listMapper)
{
$this->getTemplateRegistry()
give Sonata\AdminBundle\Templating\TemplateRegistry
You can use workaround by service tags.
First create your own Template registry and implement MutableTemplateRegistryInterface.
Then create compiler pass.
<?php
namespace App\DependencyInjection\Compiler;
use App\Registry\TemplateRegistry;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;
/**
* Class SonataAdminTemplateRegistryPass
* #package App\DependencyInjection\Compiler
*/
class SonataAdminTemplateRegistryPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container)
{
foreach ($container->findTaggedServiceIds('sonata.admin.template_registry') as $id => $tags)
{
//because #see src/vendor/sonata-project/admin-bundle/src/DependencyInjection/Compiler/AddDependencyCallsCompilerPass.php:405
$adminServiceId = str_replace(".template_registry", "", $id);
$def = $container->getDefinition($adminServiceId);
$def->removeMethodCall('setTemplateRegistry');
$def->addMethodCall('setTemplateRegistry', [new Reference(TemplateRegistry::class)]);
}
}
}
then add compiler pass to src/Kernel.php like this
protected function build(ContainerBuilder $container)
{
$container->addCompilerPass(new SonataAdminTemplateRegistryPass());
}
And you will get always override TemplateRegistry in any admin class.
So you can implement your own logic in your own TemplateRegistry :)
You can do this using single Twig template:
{# layout.html.twig #}
{# mutatis mutandis #}
{% extends app.user ?
('ROLE_ADMIN' in app.user.role ?
'admin_layout.html.twig' :
'customer_layout.html.twig'
) :
'fallback.html.twig'
%}
I would like to show on EasyAdmin a custom property, here is an example :
class Book
{
/**
* #ORM\Id()
* #ORM\GeneratedValue(strategy="AUTO")
* #ORM\Column(type="integer")
*/
public $id;
/**
* #ORM\Column(type="string")
*/
public $name;
/**
* #ORM\Column(type="float")
*/
public $price;
public function getBenefit(): float
{
// Here the method to retrieve the benefits
}
}
In this example, the custom parameter is benefit it's not a parameter of our Entity and if we configure EasyAdmin like that, it works !
easy_admin:
entities:
Book:
class: App\Entity\Book
list:
fields:
- { property: 'title', label: 'Title' }
- { property: 'benefit', label: 'Benefits' }
The problem is if the function is a bit complexe and need for example an EntityRepository, it becomes impossible to respect Controller > Repository > Entities.
Does anyone have a workaround, maybe by using the AdminController to show custom properties properly in EasyAdmin ?
You shouldn't put the logic to retrieve the benefits inside the Book entity, especially if it involves external dependencies like entityManager.
You could probably use the Doctrine events to achieve that. Retrieve the benefits after a Book entity has been loaded from the DB. Save the benefits before or after saving the Book entity in the DB.
You can find out more about it here https://symfony.com/doc/current/doctrine/event_listeners_subscribers.html
class Book
{
...
public $benefits;
}
// src/EventListener/RetrieveBenefitListener.php
namespace App\EventListener;
use Doctrine\Common\Persistence\Event\LifecycleEventArgs;
use App\Entity\Book;
class RetrieveBenefitListener
{
public function postLoad(LifecycleEventArgs $args)
{
$entity = $args->getObject();
// only act on some "Book" entity
if (!$entity instanceof Book) {
return;
}
// Your logic to retrieve the benefits
$entity->benefits = methodToGetTheBenefits();
}
}
// src/EventListener/SaveBenefitListener.php
namespace App\EventListener;
use Doctrine\Common\Persistence\Event\LifecycleEventArgs;
use App\Entity\Book;
class SaveBenefitListener
{
public function postUpdate(LifecycleEventArgs $args)
{
$entity = $args->getObject();
// only act on some "Book" entity
if (!$entity instanceof Book) {
return;
}
// Your logic to save the benefits
methodToSaveTheBenefits($entity->benefits);
}
}
// services.yml
services:
App\EventListener\RetrieveBenefitListener:
tags:
- { name: doctrine.event_listener, event: postLoad }
App\EventListener\SaveBenefitListener:
tags:
- { name: doctrine.event_listener, event: postUpdate }
This is just an example, I haven't tested the code. You will probably have to add the logic for the postPersist event if you create new Book objects.
Depending on the logic to retrieve the benefits (another DB call? loading from an external API?), you might want to to approach the problem differently (caching, loading them in your DB via a cron job, ...)
I have a little problem with sonata admin on Symfony.
I would like to change the default admin label in the breadcrumb:
but I can't find any solution. Can someone help me?
I found this function, but it doesn't work. It looks like that this function is not called.
public function buildBreadcrumbs($action, MenuItemInterface $menu = null) {
$breadCrumb = parent::buildBreadcrumbs($action, $menu);
return $breadCrumb;
}
I use Symfony 2.8.
Try to override classNameLabel property in your admin class:
// in your ProductAdmin class
public function configure()
{
$this->classnameLabel = "Products";
}
The simplest way to achieve what you want is to change translations messages.
If you really want to change the labels you can implement your own label generation strategy.
namespace Blast\CoreBundle\Translator;
use Sonata\AdminBundle\Translator\LabelTranslatorStrategyInterface;
/**
* Class LibrinfoLabelTranslatorStrategy.
*
* Provides a specific label translation strategy for Librinfo.
* It is based on UnderscoreLabelTranslatorStrategy, but without the context,
* and labels are prefixed by "librinfo.label."
*
* i.e. isValid => librinfo.label.is_valid
*/
class LibrinfoLabelTranslatorStrategy implements LabelTranslatorStrategyInterface
{
/**
* {#inheritdoc}
*/
public function getLabel($label, $context = '', $type = '')
{
$label = str_replace('.', '_', $label);
return sprintf('%s.%s.%s', "librinfo", $type, strtolower(preg_replace('~(?<=\\w)([A-Z])~', '_$1', $label)));
}
}
define it as a service
blast_core.label.strategy.librinfo:
class: Blast\CoreBundle\Translator\LibrinfoLabelTranslatorStrategy
then pass it to the definition of your admin service like so:
crm.organism:
class: Librinfo\CRMBundle\Admin\OrganismAdmin
arguments: [~, Librinfo\CRMBundle\Entity\Organism, LibrinfoCRMBundle:OrganismAdmin]
tags:
- name: sonata.admin
manager_type: orm
group: Customers Relationship Management
label_translator_strategy: blast_core.label.strategy.librinfo
You will have full control of your admin labels
Also see: SonataAdmin: replace ID in breadcrumbs
I'm trying to redirect from the onKernelController event listener to another controller in my bundle. The redirection succeed but in the $newController the container is NULL so I can't really do any thing with it like rendering pages.
Why is the container NULL when it's been created like this? and how can I inject the container service to the $newController in this case?
Thanks! :)
The final working version of the event listener:
namespace MyApp\MainBundle\EventListener;
use MyApp\MainBundle\Interfaces\UserCheckInterface;
use Symfony\Component\HttpKernel\Event\FilterControllerEvent;
use Symfony\Component\HttpKernel\Controller\ControllerResolver;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
class ControllerListener
{
private $resolver;
/**
* #param ControllerResolver $resolver The injected controller_resolver service
*/
public function __construct($resolver)
{
$this->resolver = $resolver;
}
public function onKernelController(FilterControllerEvent $event)
{
$controller = $event->getController();
if (!is_array($controller))
{
// not an object but a different kind of callable. Do nothing
return;
}
/* #var $controllerObject Controller */
$controllerObject = $controller[0];
if ( $controllerObject instanceof UserCheckInterface )
{
$newRequest = $event->getRequest()->duplicate(null, null, array('_controller' => 'MyApp\MainBundle\Controller\ErrorController::notLoggedInAction'));
/* #var $newController Controller */
$newController = $this->resolver->getController($newRequest);
$event->setController($newController);
}
}
}
Here is the config.yml
kernel.listener.ControllerListener:
class: MyApp\MainBundle\EventListener\ControllerListener
arguments: [ "#controller_resolver" ]
tags:
- { name: kernel.event_listener, event: kernel.controller, method: onKernelController }
The component resolver (Symfony\Component\HttpKernel\Controller\ControllerResolver) does not know anything about the container.
Instead, the Symfony FrameworkBundle provides Symfony\Bundle\FrameworkBundle\Controller\ControllerResolver which is container aware. It's worth taking a look at the code to see where setContainer is being called.
Inject the controller_resolver service into your listener then use it instead of creating a new resolver.
OK... I found the answer...
When creating the $newController like I did the container by default is NULL and need to be set with the container of the original controller like this
$newController[0]->setContainer($controllerObject->get("service_container"));
I'm working in a project using Symfony 2,
I'm using Assetic with rewrite and less filter, and it work fine,
Now I'm planing to let administrator (connected user) to controle some features in css like font and main color.
The problem that I'm facing is :
- how can I proceed to integrate these css changes from entity to the css management
I can't let assetic use routing rule to include custom css
Eaven if I success to get this work, every time I have a changes to the custom css I have to install assets to web folder and make the assetic:dump and clearing cache from a controller.
If you (or someone else) still need this:
I solved this by putting all generic CSS in a asset handled by Assetic like usual and putting the dynamic CSS generation in a Controller action and rendering the CSS with Twig.
As suggested by Steffen you should put the dynamic CSS in a Twig template.
But now you might suffer from that part of the css being a full request to a symfony application instead of a css (HTTP 302 and such) which increases server load.
Thats why I would advise you to do 3 things (you can skip step 2 if your css doesn't change without interaction, e.g. date based):
Implement a service which caches the current output to e.g. web/additional.css.
Write and register a RequestListener to update the css regularly
Extend all controller actions that could introduce changes to the css with the service call
Example (assumes you use Doctrine and have an entity with some color information):
Service
<?php
//Acme\DemoBundle\Service\CSSDeployer.php
namespace Acme\DemoBundle\Service;
use Doctrine\ORM\EntityManager;
class CSSDeployer
{
/**
* #var EntityManager
*/
protected $em;
/**
* Twig Templating Service
*/
protected $templating;
public function __construct(EntityManager $em, $templating)
{
$this->em = $em;
$this->templating = $templating;
}
public function deployStyle($filepath)
{
$entity = $this->em->getRepository('AcmeDemoBundle:Color')->findBy(/* your own logic here */);
if(!$entity) {
// your error handling
}
if(!file_exists($filepath)) {
// your error handling, be aware of the case where this service is run the first time though
}
$content = $this->templating->render('AcmeDemoBundle:CSS:additional.css.twig', array(
'data' => $entity
));
//Maybe you need to wrap below in a try-catch block
file_put_contents($filepath, $content);
}
}
Service Registration
#Acme\DemoBundle\Resources\config\services.yml
services:
#...
css_deployer:
class: Acme\DemoBundle\Service\CSSDeployer
arguments: [ #doctrine.orm.entity_manager, #templating ]
RequestListener
<?php
//Acme\DemoBundle\EventListener\RequestListener.php
namespace Acme\DemoBundle\EventListener;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\Debug\Exception\ContextErrorException;
use \DateTime;
use Doctrine\ORM\EntityManager;
class RequestListener
{
/**
* #var ContainerInterface
*/
protected $container;
/**
* #var EntityManager
*/
protected $em;
public function __construct(ContainerInterface $container, $em)
{
$this->container = $container;
$this->em = $em;
}
/**
* Checks filemtime (File modification time) of web/additional.css
* If it is not from today it will be redeployed.
*/
public function onKernelRequest(GetResponseEvent $event)
{
$kernel = $event->getKernel();
$container = $this->container;
$path = $container->get('kernel')->getRootDir().'/../web'.'/additional.css';
$time = 1300000000;
try {
$time = #filemtime($path);
} catch(ContextErrorException $ex) {
//Ignore
} catch(\Exception $ex) {
//will never get here
if(in_array($container->getParameter("kernel.environment"), array("dev","test"))) {
throw $ex;
}
}
if($time === FALSE || $time == 1300000000) {
file_put_contents($path, "/*Leer*/");
$time = 1300000000;
}
$modified = new \DateTime();
$modified->setTimestamp($time);
$today = new \DateTime();
if($modified->format("Y-m-d")!= $today->format("Y-m-d")) {
//UPDATE CSS
try {
$container->get('css_deployer')->deployStyle($path);
} catch(\Exception $ex) {
if(in_array($container->getParameter("kernel.environment"), array("dev","test"))){
throw $ex;
}
}
} else {
//DO NOTHING
}
}
}
RequestListener registration
#Acme\DemoBundle\Resources\config\services.yml
acme_style_update_listener.request:
class: Acme\DemoBundle\EventListener\RequestListener
arguments: [ #service_container, #doctrine.orm.entity_manager ]
tags:
- { name: kernel.event_listener, event: kernel.request, method: onKernelRequest }
Controller actions
public function updateAction()
{
// Stuff
$path = '....';
$this->get('css_deployer')->deployStyle($path);
}
Hope this helps someone in the future.