Sonata Admin Override template depend of user - symfony

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'
%}

Related

sonata admin label on breadcrumb

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

How to kill execution in Twig after dump()?

I'm using the {{ dump(foo) }} function in Twig to debug my templates. However, if the template is throwing errors after the dump() function, you will only see Symfony's debugging page informing you of the error. You can obviously comment out the offending lines of code in the Twig template, but is there a way to kill the execution of the template immediately after so that the output of the dump() function is the last thing printed on the screen. Naively I'm thinking of something like {{ dump(foo) }} {{ die() }}. Any ideas on how you could achieve this?
You could create a simple twig extension that handled this.
Your twig file..
namespace Acme\SomeBundle\Twig;
class DevExtension extends \Twig_Extension
{
/**
* {#inheritdoc}
*/
public function getFunctions()
{
return array(
new \Twig_SimpleFunction('die', 'die'),
);
}
/**
* {#inheritdoc}
*/
public function getName()
{
return 'acme_dev';
}
}
Your services file (YAML)..
services:
acme.twig.dev_extension:
class: Acme\SomeBundle\Twig\DevExtension
tags:
- { name: twig.extension }
Additionally you could pass in the current environment and then either die or fail silently depending on the environment in case you have left the die in your code for some reason.
Your twig extension..
class DevExtension extends \Twig_Extension
{
/**
* #string
*/
private $environment;
/**
* #param string $environment
*/
public function __construct($environment)
{
$this->environment = $environment;
}
/**
* {#inheritdoc}
*/
public function getFunctions()
{
return array(
new \Twig_SimpleFunction('die', array($this,'killRender')),
);
}
/**
* #param string|null $message
*/
public function killRender($message = null)
{
if ('dev' === $this->environment) {
die($message);
}
return '';
}
...
}
Your services file..
services:
acme.twig.dev_extension:
class: Acme\SomeBundle\Twig\DevExtension
arguments:
- %kernel.environment%
tags:
- { name: twig.extension }
I don't think you should stop PHP execution inside your twig template (even though this is possible using a custom Twig extension). The result would not be what you'd expect because there is a lot more happening between rendering your template and sending it to the browser. If you simply stop execution all this will not happen any more and I'd suspect that you'll get a simple white page.
Perhaps it's a better approach to dump the variable inside the controller. Doing that will send the dump output to the web profiler toolbar which is available even on symfony's error page.
Oh and well, what about just using a comment ({# ... #}) to disable the non-working part of your template?

How to create event listener that inject the view data in symfony2?

I want to create event listener that add some results of db query to all symfony actions
for example:
class BlogController extends Controller
{
/**
* #Route("/blog/")
* #Template()
*/
public function indexAction()
{
....
return array(
'entries' => $posts
);
}
}
This controller is passing entries variable to the view, I want to create listener that take the returned value of all actions and inject another index to the returned array to be (for example)
array(
'entries' => $posts,
'categories' => $categories
);
so I can call the $categories var from any where in my application views
I hope my question is clear to you guys. Thanks in advance.
You should consider creating a global variable or twig extension to make categories available in your templates, you can't do that by using events (since the template is parsed inside the controller, not before/after it)
This approach, although valid and commonly used in some frameworks, is not very common in Symfony as it suits more MVC than HMVC architecture.
I would suggest you a different one with the same result:
Instead of adding parameter to every controller return, render another controller which returns just a subview of what you're trying to show. Simple example:
// article/index.html.twig
<div class="category-bar">{{ render(controller('MyVendorMyBundle:CategoryController:bar')) }}</div>
<div class="article-list">
{% for article in articles %>
{# Print article here #}
{% endfor %}
</div>
// CategoryController
class CategoryController extends Controller
{
/**
* #Template
*/
public function barAction()
{
return ['categories' => $this->fetchCategoriesSomehow()];
}
}
So when you render your article list action, twig will fire a subrequest to render categories bar above it.
Furthermore, if you don't like making subrequests, nothing stops you from creating a twig extension service which would fetch categories and render template for you.
In most cases I would go with #Wouter J's suggestion and create a twig extension or a global variable.
However, what you want to do is actually possible (regardless if that's the right solution or not).
The #Template annotation has a vars attribute, which lets you to specify which atttributes from the request should be passed to the template:
/**
* #ParamConverter("post", class="SensioBlogBundle:Post")
* #Template("SensioBlogBundle:Post:show.html.twig", vars={"post"})
*/
public function showAction()
{
}
Note, that request attributes can be set by you:
$request->attributes->set('categories', []);
So, you could implement a listener which would set the categories attribute on the request and than configure the vars on the #Template annotation:
/**
* #Template("SensioBlogBundle:Post:show.html.twig", vars={"categories"})
*/
public function showAction(Post $post)
{
}
Have a look at the TemplateListener from the SensioFrameworkExtraBundle for more insight. The listener defines template vars on kernel.controller and uses them to render the view on kernel.view.
You could avoid defining vars on the annotation if your listener was registered after the TemplateListener::onController(). It would have to add categories to the _template_vars request attribute.
Use Twig extension to create function that will return list of available categories
<?php
class CategoriesExtension extends \Twig_Extension
{
public function getFunctions()
{
return [
new \Twig_SimpleFunction('getCategories', [$this, 'getCategoriesList'])
];
}
/**
* #return null|string
*/
public function getCategoriesList()
{
return CategoryQuery::create()->find();
}
/**
* Returns the name of the extension.
*
* #return string The extension name
*/
public function getName()
{
return 'list_categories';
}
}
You can pass parameter to function if You would like do some conditions on query.
The trick is to get the twig service in your listener and then use addGlobal to add your categories
namespace Cerad\Bundle\CoreBundle\EventListener;
use Symfony\Component\DependencyInjection\ContainerAware;
use Symfony\Component\HttpKernel\HttpKernel;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\HttpKernel\Event\FilterControllerEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
class MyEventListener extends ContainerAware implements EventSubscriberInterface
{
public static function getSubscribedEvents()
{
return array(
KernelEvents::CONTROLLER => array(
array('doCategories', -1100),
);
}
public function doCategories(FilterControllerEvent $eventx)
{
// Query your categories
$categories = array('cat1','cat2');
// Make them available to all twig templates
$twig = $this->container->get('twig');
$twig->addGlobal('categories',$categories);
}
# services.yml
cerad_core__my__event_listener:
class: '%cerad_core__my__event_listener__class%'
calls:
- [setContainer, ['#service_container']]
tags:
- { name: kernel.event_subscriber }

How can I make the entity field type available in silex?

I have been using Silex for my latest project and I was trying to follow along with the "How to Dynamically Modify Forms Using Form Events" in the Symfony cookbook. I got to the part that uses the entity field type and realized it is not available in Silex.
It looks like the symfony/doctrine-bridge can be added to my composer.json which contains the "EntityType". Has anyone successfully got entity type to work in Silex or run into this issue and found a workaround?
I was thinking something like this might work:
$builder
->add('myentity', new EntityType($objectManager, $queryBuilder, 'Path\To\Entity'), array(
))
;
I also found this answer which looks like it might do the trick by extending the form.factory but haven't attempted yet.
I use this Gist to add EntityType field in Silex.
But the trick is register the DoctrineOrmExtension form extension by extending form.extensions like FormServiceProvider doc says.
DoctrineOrmExtension expects an ManagerRegistry interface in its constructor, that can be implemented extending Doctrine\Common\Persistence\AbstractManagerRegistry as the follow:
<?php
namespace MyNamespace\Form\Extensions\Doctrine\Bridge;
use Doctrine\Common\Persistence\AbstractManagerRegistry;
use Silex\Application;
/**
* References Doctrine connections and entity/document managers.
*
* #author Саша Стаменковић <umpirsky#gmail.com>
*/
class ManagerRegistry extends AbstractManagerRegistry
{
/**
* #var Application
*/
protected $container;
protected function getService($name)
{
return $this->container[$name];
}
protected function resetService($name)
{
unset($this->container[$name]);
}
public function getAliasNamespace($alias)
{
throw new \BadMethodCallException('Namespace aliases not supported.');
}
public function setContainer(Application $container)
{
$this->container = $container['orm.ems'];
}
}
So, to register the form extension i use:
// Doctrine Brigde for form extension
$app['form.extensions'] = $app->share($app->extend('form.extensions', function ($extensions) use ($app) {
$manager = new MyNamespace\Form\Extensions\Doctrine\Bridge\ManagerRegistry(
null, array(), array('default'), null, null, '\Doctrine\ORM\Proxy\Proxy'
);
$manager->setContainer($app);
$extensions[] = new Symfony\Bridge\Doctrine\Form\DoctrineOrmExtension($manager);
return $extensions;
}));

Symfony2 - checking if file exists

I have a loop in Twig template, which returns multiple values. Most important - an ID of my entry. When I didn't use any framework nor template engine, I used simply file_exists() within the loop. Now, I can't seem to find a way to do it in Twig.
When I display user's avatar in header, I use file_exists() in controller, but I do it because I don't have a loop.
I tried defined in Twig, but it doesn't help me. Any ideas?
If you want want to check the existence of a file which is not a twig template (so defined can't work), create a TwigExtension service and add file_exists() function to twig:
src/AppBundle/Twig/Extension/TwigExtension.php
<?php
namespace AppBundle\Twig\Extension;
class FileExtension extends \Twig_Extension
{
/**
* Return the functions registered as twig extensions
*
* #return array
*/
public function getFunctions()
{
return array(
new Twig_SimpleFunction('file_exists', 'file_exists'),
);
}
public function getName()
{
return 'app_file';
}
}
?>
Register your service:
src/AppBundle/Resources/config/services.yml
# ...
parameters:
app.file.twig.extension.class: AppBundle\Twig\Extension\FileExtension
services:
app.file.twig.extension:
class: %app.file.twig.extension.class%
tags:
- { name: twig.extension }
That's it, now you are able to use file_exists() inside a twig template ;)
Some template.twig:
{% if file_exists('/home/sybio/www/website/picture.jpg') %}
The picture exists !
{% else %}
Nope, Chuck testa !
{% endif %}
EDIT to answer your comment:
To use file_exists(), you need to specify the absolute path of the file, so you need the web directory absolute path, to do this give access to the webpath in your twig templates
app/config/config.yml:
# ...
twig:
globals:
web_path: %web_path%
parameters:
web_path: %kernel.root_dir%/../web
Now you can get the full physical path to the file inside a twig template:
{# Display: /home/sybio/www/website/web/img/games/3.jpg #}
{{ web_path~asset('img/games/'~item.getGame.id~'.jpg') }}
So you'll be able to check if the file exists:
{% if file_exists(web_path~asset('img/games/'~item.getGame.id~'.jpg')) %}
I've created a Twig function which is an extension of the answers I have found on this topic. My asset_if function takes two parameters: the first one is the path for the asset to display. The second parameter is the fallback asset, if the first asset does not exist.
Create your extension file:
src/Showdates/FrontendBundle/Twig/Extension/ConditionalAssetExtension.php:
<?php
namespace Showdates\FrontendBundle\Twig\Extension;
use Symfony\Component\DependencyInjection\ContainerInterface;
class ConditionalAssetExtension extends \Twig_Extension
{
private $container;
public function __construct(ContainerInterface $container)
{
$this->container = $container;
}
/**
* Returns a list of functions to add to the existing list.
*
* #return array An array of functions
*/
public function getFunctions()
{
return array(
'asset_if' => new \Twig_Function_Method($this, 'asset_if'),
);
}
/**
* Get the path to an asset. If it does not exist, return the path to the
* fallback path.
*
* #param string $path the path to the asset to display
* #param string $fallbackPath the path to the asset to return in case asset $path does not exist
* #return string path
*/
public function asset_if($path, $fallbackPath)
{
// Define the path to look for
$pathToCheck = realpath($this->container->get('kernel')->getRootDir() . '/../web/') . '/' . $path;
// If the path does not exist, return the fallback image
if (!file_exists($pathToCheck))
{
return $this->container->get('templating.helper.assets')->getUrl($fallbackPath);
}
// Return the real image
return $this->container->get('templating.helper.assets')->getUrl($path);
}
/**
* Returns the name of the extension.
*
* #return string The extension name
*/
public function getName()
{
return 'asset_if';
}
}
Register your service (app/config/config.yml or src/App/YourBundle/Resources/services.yml):
services:
showdates.twig.asset_if_extension:
class: Showdates\FrontendBundle\Twig\Extension\ConditionalAssetExtension
arguments: ['#service_container']
tags:
- { name: twig.extension }
Now use it in your templates like this:
<img src="{{ asset_if('some/path/avatar_' ~ app.user.id, 'assets/default_avatar.png') }}" />
I've had the same problem as Tomek. I've used Sybio's solution and made the following changes:
app/config.yml => add "/" at the end of web_path
parameters:
web_path: %kernel.root_dir%/../web/
Call file_exists without "asset" :
{% if file_exists(web_path ~ 'img/games/'~item.getGame.id~'.jpg') %}
Hope this helps.
Here is my solution, using SF4, autowire and autoconfigure:
namespace App\Twig;
use Twig\Extension\AbstractExtension;
use Twig\TwigFunction;
use Symfony\Component\Filesystem\Filesystem;
class FileExistsExtension extends AbstractExtension
{
private $fileSystem;
private $projectDir;
public function __construct(Filesystem $fileSystem, string $projectDir)
{
$this->fileSystem = $fileSystem;
$this->projectDir = $projectDir;
}
public function getFunctions(): array
{
return [
new TwigFunction('file_exists', [$this, 'fileExists']),
];
}
/**
* #param string An absolute or relative to public folder path
*
* #return bool True if file exists, false otherwise
*/
public function fileExists(string $path): bool
{
if (!$this->fileSystem->isAbsolutePath($path)) {
$path = "{$this->projectDir}/public/{$path}";
}
return $this->fileSystem->exists($path);
}
}
In services.yaml:
services:
App\Twig\FileExistsExtension:
$projectDir: '%kernel.project_dir%'
In templates:
# Absolute path
{% if file_exists('/tmp') %}
# Relative to public folder path
{% if file_exists('tmp') %}
I am new to Symfony so every comments are welcome!
Also, as initial question is about Symfony 2, maybe my answer is not relevant and I would better ask a new question and answer by myself?
Improving on Sybio's answer, Twig_simple_function did not exist for my version and nothing here works for external images for example. So my File extension file is like this:
namespace AppBundle\Twig\Extension;
class FileExtension extends \Twig_Extension
{
/**
* {#inheritdoc}
*/
public function getName()
{
return 'file';
}
public function getFunctions()
{
return array(
new \Twig_Function('checkUrl', array($this, 'checkUrl')),
);
}
public function checkUrl($url)
{
$headers=get_headers($url);
return stripos($headers[0], "200 OK")?true:false;
}
Just add a little comment to the contribution of Sybio:
The Twig_Function_Function class is deprecated since version 1.12 and
will be removed in 2.0. Use Twig_SimpleFunction instead.
We must change the class Twig_Function_Function by Twig_SimpleFunction:
<?php
namespace Gooandgoo\CoreBundle\Services\Extension;
class TwigExtension extends \Twig_Extension
{
/**
* Return the functions registered as twig extensions
*
* #return array
*/
public function getFunctions()
{
return array(
#'file_exists' => new \Twig_Function_Function('file_exists'), // Old class
'file_exists' => new \Twig_SimpleFunction('file_exists', 'file_exists'), // New class
);
}
public function getName()
{
return 'twig_extension';
}
}
The rest of code still works exactly as said Sybio.

Resources