I'm creating my Twig Extension to extend the actual "FormExtension".
Reason for that is that I need to create new functions without overwriting the current ones and making this available across my entire project.
So building and extension seemed to be the right way to go.
Building the extension is not a problem, my problem is how to render block from there?
What I understood till here, is that I need to create a Twig_Environment where I have to load my actual twig template (containing my blocks).
From there I should be able to render those block using "$mytemplate->displayBlock()".
Sample code:
public function renderWidgetinline(FormView $view, array $variables = array())
{
$loader = new \Twig_Loader_Filesystem(__DIR__.'/../Resources/views/Form');
$twig = new \Twig_Environment($loader);
$this->template = $twig->loadTemplate("form_layout.html.twig");
ob_start();
$this->template->displayBlock(???WHAT-PARAMS???);
$html = ob_get_clean();
return $html;
}
I found those information by looking at the Symfony base FormExtension.php file.
My questions are:
How does displayBlock() works, where can I found the defintion of that function?
Is what I described above the right way to go?
How should I proceed to have access to that new TWIG template together with the base form_div_layout.html template? Can I somehow get the current environment without having to recreated one and load my additional template there?
Thanks!
Have you tried to use renderBlock instead?
The first parameter you need is the name of the block, and the second should be an associative array of values passed to the block.
So what you would have in the case of a service that is rendering a block is the following:
The Service Class:
<?php
namespace Acme\BlockBundle\Blocks;
use Doctrine\Common\Persistence\ObjectManager;
Class Block {
private $om;
private $environment;
private $template;
public function __construct( ObjectManager $om, Twig $environment )
{
$this->om = $om;
$this->environment = $environment;
}
public function render( $template, $data )
{
$this->template = $this->environment->loadTemplate( $template );
// maybe query the DB via doctrine, that is why I have included $om
// in the service arguments
// example:
$entities = $om->getRepository( 'AcmePizzaBundle:Pizza' )->getMeatyOnes()
return $this->template->renderBlock( 'acme_block', array(
'data' => $entities,
));
}
}
The Twig Extension Class
<?php
namespace Acme\BlockBundle\Twig\Extension;
use Twig_Extension;
use Twig_Function_Method;
class BlockExtension extends Twig_Extension
{
protected $container;
public function __construct( $container )
{
$this->container = $container;
}
public function getName()
{
return 'block_extension';
}
public function getFunctions()
{
return array(
'render_block' => new Twig_Function_Method( $this, 'renderBlock', array(
'is_safe' => array( 'html' ),
)),
);
}
public function renderBlock( $template, $data )
{
return $this->container->get( 'acme.block' )->render( $template, $data );
}
}
The services.yml
parameters:
acme.blocks.block.class: Acme\BlocksBundle\Blocks\Block
acme.twig.block_extension.class: Acme\BlocksBundle\Twig\Extension\BlockExtension
services:
acme.blocks.block:
class: '%acme.blocks.block.class%'
arguments:
- '#doctrine.orm.entity_manager'
- '#twig'
acme.twig.block_extension:
class: %acme.twig.block_extension.class%
arguments:
- '#service_container'
tags:
- { name: twig.extension }
don't forget your template:
{% block acme_block %}
{% spaceless %}
{# do something with your data here #}
{% endspaceless %}
{% endblock acme_block %}
Then when you want to display it, you just need to call the twig function you have just created:
{{ render_block( '::block_template.html.twig', someDataOneThePage ) }}
By no mean this is a complete solution, but I have used something similar and it proved to be working.
HTH
Tam
[edit: April 2016 - for reference: this solution was working on a Symfony 2.4 project]
Related
My application is made up with eight bundles, within my main layout I would like to check if a certain bundle exists so I can include a sub template, how do I go about doing this?
Thanks to #DonCallisto, I decided to make a twig function to use in my templates, the following is my twig extension.
<?php
namespace MG\AdminBundle\Twig;
use Symfony\Component\DependencyInjection\ContainerInterface;
class Bundles extends \Twig_Extension {
protected $container;
public function __construct(ContainerInterface $container) {
$this->container = $container;
}
public function getFunctions()
{
return array(
new \Twig_SimpleFunction(
'bundleExists',
array($this, 'bundleExists')
),
);
}
public function bundleExists($bundle){
return array_key_exists(
$bundle,
$this->container->getParameter('kernel.bundles')
);
}
public function getName() {
return 'mg_admin_bundles';
}
}
I then registered it in my services.yml
services:
mg_admin.bundles.extension:
class: MG\AdminBundle\Twig\Bundles
arguments: [#service_container]
tags:
- { name: twig.extension }
Now in my twig templates I can check for registered bundles like this:
{% if bundleExists('MGEmailBundle') %}
{% include 'MGEmailBundle:SideBar:sidebar.html.twig' %}
{% endif %}
$this->container->getParameter('kernel.bundles');
will return all registered bundles (class names). You could pass that list - or parse it direclty - into a controller and pass it to your twig view
Then you should easily reach your target
If the bundle you want to check is a specific bundle, and you know the main class name, the easiest way may be:
if (class_exists('Acme\CommentBundle\AcmeCommentBundle'))
{
// Bundle exists and is loaded by AppKernel...
}
Let's say I have comments in more than one spot in website. How can I create something like
{{ render_widget('comments', {"object": object} ) }} ? That would render the form and list with all comments for that object ?
Create a service:
// src/Acme/HelloBundle/Service/Widget.php
namespace Acme\HelloBundle\Service;
use Symfony\Component\DependencyInjection\ContainerInterface;
class Widget
{
protected $container;
public function __construct(ContainerInterface $container)
{
$this->container = $container;
}
public function getComments()
{
$request = $this->container->get('request'); // use service_container to access request, doctrine, twig, etc...
}
}
Declare a service:
# src/Acme/HelloBundle/Resources/config/services.yml
parameters:
# ...
my_widget.class: Acme\HelloBundle\Service\Widget
services:
my_widget:
class: "%my_widget.class%"
arguments: ["#service_container"]
# scope: container can be omitted as it is the default
Use a service in controller:
namespace Acme\HelloBundle\Controller;
class BlogController {
public function getPostAction($id) {
// get post from db
$widget = $this->get('my_widget'); // get your widget in controller
$comments = $widget->getComments(); // do something with comments
return $this->render('AcmeHelloBundle:Blog:index.html.twig',
array('widget' => $widget) // pass widget to twig
);
}
}
or in twig, if you pass your service in template like above in render() function:
#AcmeHelloBundle:Blog:index.html.twig
{{ widget.getComments()|raw }}
And usefull to read the docs about How to work with Scopes
I have done it another way. I registered Twig Extension with function {{ comments(object) }}
The function is registered that way
'comments' => new \Twig_Function_Method($this, 'comments', array(
'needs_environment' => true,
'is_safe' => array('html')
))
That way I don't need to specify |raw filter.
I am learning symfony2.3, and I am getting an error when I try to get controller name in twig template.
Controller:
namespace Acme\AdminBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
class DefaultController extends Controller
{
public function indexAction($name)
{
return $this->render('AcmeAdminBundle:Default:index.html.twig', array('name' => $name));
}
}
In my TWIG template:
{% extends '::base.html.twig' %}
{% block body %}
{{ app.request.get('_template').get('controller') }}
Hello {{ name }}!!!
{% endblock %}
Output:
Impossible to invoke a method ("get") on a NULL variable ("") in AcmeAdminBundle:Default:index.html.twig at line 3
I want output as "Default"
I am using symfony 2.3, I have also tried on symfony 2.1 but on both version generates the same error.
use this line to display a controller name in twig:
{{ app.request.attributes.get("_controller") }}
Many months ago I had the same problem as you, and "googling" I found a working code and I had adapted it to my necessities. Here we go:
1 - We need to define a TWIG extension for that. We create the folder structure Your\OwnBundle\Twig\Extension if you haven't defined yet.
2 - Inside this folder we create the file ControllerActionExtension.php which code is:
namespace Your\OwnBundle\Twig\Extension;
use Symfony\Component\HttpFoundation\Request;
/**
* A TWIG Extension which allows to show Controller and Action name in a TWIG view.
*
* The Controller/Action name will be shown in lowercase. For example: 'default' or 'index'
*
*/
class ControllerActionExtension extends \Twig_Extension
{
/**
* #var Request
*/
protected $request;
/**
* #var \Twig_Environment
*/
protected $environment;
public function setRequest(Request $request = null)
{
$this->request = $request;
}
public function initRuntime(\Twig_Environment $environment)
{
$this->environment = $environment;
}
public function getFunctions()
{
return array(
'get_controller_name' => new \Twig_Function_Method($this, 'getControllerName'),
'get_action_name' => new \Twig_Function_Method($this, 'getActionName'),
);
}
/**
* Get current controller name
*/
public function getControllerName()
{
if(null !== $this->request)
{
$pattern = "#Controller\\\([a-zA-Z]*)Controller#";
$matches = array();
preg_match($pattern, $this->request->get('_controller'), $matches);
return strtolower($matches[1]);
}
}
/**
* Get current action name
*/
public function getActionName()
{
if(null !== $this->request)
{
$pattern = "#::([a-zA-Z]*)Action#";
$matches = array();
preg_match($pattern, $this->request->get('_controller'), $matches);
return $matches[1];
}
}
public function getName()
{
return 'your_own_controller_action_twig_extension';
}
}
3 - After that we need to specify the service for TWIG to be recognized:
services:
your.own.twig.controller_action_extension:
class: Your\OwnBundle\Twig\Extension\ControllerActionExtension
calls:
- [setRequest, ["#?request="]]
tags:
- { name: twig.extension }
4 - Cache clear to make sure everything is ok:
php app/console cache:clear --no-warmup
5 - And now, if I'm not forgetting anything, you will be able to access those 2 methods in a TWIG template: get_controller_name() and get_action_name()
6 - Examples:
You are in the {{ get_action_name() }} action of the {{ get_controller_name() }} controller.
This will output something like: You are in the index action of the default controller.
You can also use to check:
{% if get_controller_name() == 'default' %}
Whatever
{% else %}
Blablabla
{% endif %}
And that's all!! I hope I helped you, mate :)
Edit: Take care about the clearing cache. If you don't use --no-warmup parameter maybe you will realize that nothing is shown in your templates. That's because this TWIG Extension uses the Request to extract the Controller and Action names. If you "warm up" the cache, the Request is not the same as a browser request, and the methods can return '' or null
Since Symfony 3.x, service request is replaced by request_stack, and Twig Extension declaration changed since Twig 1.12.
I will correct the answer of Dani (https://stackoverflow.com/a/17544023/3665477) :
1 - We need to define a TWIG extension for that. We create the folder structure AppBundle\Twig\Extension if you haven't defined yet.
2 - Inside this folder we create the file ControllerActionExtension.php which code is:
<?php
namespace AppBundle\Twig\Extension;
use Symfony\Component\HttpFoundation\RequestStack;
class ControllerActionExtension extends \Twig_Extension
{
/** #var RequestStack */
protected $requestStack;
public function __construct(RequestStack $requestStack)
{
$this->requestStack = $requestStack;
}
public function getFunctions()
{
return [
new \Twig_SimpleFunction('getControllerName', [$this, 'getControllerName']),
new \Twig_SimpleFunction('getActionName', [$this, 'getActionName'])
];
}
/**
* Get current controller name
*
* #return string
*/
public function getControllerName()
{
$request = $this->requestStack->getCurrentRequest();
if (null !== $request) {
$pattern = "#Controller\\\([a-zA-Z]*)Controller#";
$matches = [];
preg_match($pattern, $request->get('_controller'), $matches);
return strtolower($matches[1]);
}
}
/**
* Get current action name
*
* #return string
*/
public function getActionName()
{
$request = $this->requestStack->getCurrentRequest();
if (null !== $request) {
$pattern = "#::([a-zA-Z]*)Action#";
$matches = [];
preg_match($pattern, $request->get('_controller'), $matches);
return $matches[1];
}
}
public function getName()
{
return 'controller_action_twig_extension';
}
}
3 - After that we need to specify the service for TWIG to be recognized:
app.twig.controller_action_extension:
class: AppBundle\Twig\Extension\ControllerActionExtension
arguments: [ '#request_stack' ]
tags:
- { name: twig.extension }
4 - Cache clear to make sure everything is ok:
php bin/console cache:clear --no-warmup
5 - And now, if I'm not forgetting anything, you will be able to access those 2 methods in a TWIG template: getControllerName() and getActionName()
6 - Examples:
You are in the {{ getActionName() }} action of the {{ getControllerName() }} controller.
This will output something like: You are in the index action of the default controller.
You can also use to check:
{% if getControllerName() == 'default' %}
Whatever
{% else %}
Blablabla
{% endif %}
I don't really see WHY you would need this.
You might better send parameters into your view.
But if you really need it this way, here's a solution:
Your error comes from the second get method
request = app.request // Request object
NULL = request.get('_template') // Undefined attribute, default NULL
NULL.get('controller') // Triggers error
If you want to get the controller called during the request you can access it via the key _controller of the request attribute
app.request.attribute.get('_controller')
Will return
Acme\AdminBundle\Controller\DefaultController::indexAction
You can then parse it the way you want.
Note that this won't return the controller instance, only its name and method called
To get the controller - {{ app.request.attributes.get('_controller') }}
To get the Action - {{ app.request.attributes.get('_template').get('name') }}
Found at - http://forum.symfony-project.org/viewtopic.php?f=23&t=34083
It can vary. If you're using annotations in the controller, e.g. #Template("AcmeDemoBundle:Default:index"), trying to access app.request.get('_template') in your Twig template will return a string e.g. "AcmeDemoBundle:Default:index". So you might need to access it like this:
{% set _template = app.request.get('_template')|split(':') %}
{% set controller = _template[1] %}
{% set bundle = _template[0] %}
If you're not using annotations then you can use the app.request.get('_template').get('_controller')
Controller:
{{ app.request.attributes.get('_template').get('controller') }}
Action:
{{ app.request.attributes.get('_template').get('name') }}
enjoy ;)
I am using sonata media bundle.
and I was wondering how can I access the media url in twig.
I just want the url, I do not need to show the media.
Any suggestions?
You have to use the path media helper:
{% path media, 'small' %}
In the above code, media is an instance of the media entity, and small is the chosen format.
http://sonata-project.org/bundles/media/master/doc/reference/helpers.html#twig-usage
But if you do not want to render the media right there and just store the url in a variable, you need to ask the media provider for the public url.
This was my case, that I needed to pass the url to another template.
I did it creating a custom function in my Twig Extension (see here: http://symfony.com/doc/current/cookbook/templating/twig_extension.html).
Provided that you have the container available in your extension service with $this->container, you can do like this:
public function getMediaPublicUrl($media, $format)
{
$provider = $this->container->get($media->getProviderName());
return $provider->generatePublicUrl($media, $format);
}
Register the function in the extension:
public function getFunctions() {
....
'media_public_url' => new \Twig_Function_Method($this, 'getMediaPublicUrl'),
....
);
}
And call your new helper form your template:
{% set img_url = media_public_url(media, 'small') %}
for instance
regards
#javigzz's is perfect in case of default context. I used custom context, so had to handle $format first taking into account context name:
$provider = $this->container->get($media->getProviderName());
$format = $provider->getFormatName($media, $format);
$url = $provider->generatePublicUrl($media, $format);
Additional Note
Since injecting container is not the best practice, it is better to get provider from the provider pool:
class Foo {
public function __construct(Sonata\MediaBundle\Provider\Pool $pool) {
$this->pool = $pool;
}
public function getUrl($media, $format) {
$provider = $this->pool->getProvider($media->getProviderName());
$format = $provider->getFormatName($media, $format);
$url = $provider->generatePublicUrl($media, $format);
return $url;
}
}
Since #javigzz's answer did not work for me, here is a twig extension that works with the latest version of sonata_media:
namespace Socialbit\SonataMediaTwigExtensionBundle\Twig;
use Sonata\CoreBundle\Model\ManagerInterface;
use Symfony\Component\DependencyInjection\Container;
Class:
/**
* Description of MediaPathExtension
*
* #author thomas.kekeisen
*/
class MediaPathExtension extends \Twig_Extension
{
/**
*
* #var type Container
*/
protected $container;
/**
*
* #var type ManagerInterface
*/
protected $mediaManager;
public function __construct(Container $container, $mediaManager)
{
$this->container = $container;
$this->mediaManager = $mediaManager;
}
public function getFunctions()
{
return array
(
'media_public_url' => new \Twig_Function_Method($this, 'getMediaPublicUrl')
);
}
/**
* #param mixed $media
*
* #return null|\Sonata\MediaBundle\Model\MediaInterface
*/
private function getMedia($media)
{
$media = $this->mediaManager->findOneBy(array(
'id' => $media
));
return $media;
}
public function getMediaPublicUrl($media, $format)
{
$media = $this->getMedia($media);
$provider = $this->container->get($media->getProviderName());
return $provider->generatePublicUrl($media, $format);
}
public function getName()
{
return 'SocialbitSonataMediaTwigExtensionBundleMediaPathExtension';
}
}
services.yml:
services:
socialbit.sonatamediatwigextensionbundle.mediapathextension:
class: Socialbit\SonataMediaTwigExtensionBundle\Twig\MediaPathExtension
public: false
arguments:
- #service_container
- #sonata.media.manager.media
tags:
- { name: twig.extension }
The usage will be the same:
{% set img_url = media_public_url(media, 'reference') %}
{{ dump(img_url) }}
You can use: {% path media, 'reference' %}
#Blauesocke - tried your solution and had exactly the same result for file provider with using both
{% set img_url = media_public_url(media, 'reference') %}
{{ dump(img_url) }}
and
{% path sonata_admin.value, 'reference' %}
I've a mixed array like this one (mobile numbers and entities):
$targets = array();
$targets[] = '+32647651212';
$targets[] = new Customer();
In my Twig template i have to call getMobile() if target is a Customer or just print the number if it's actually a number (string).
Is there something like instanceof operator in Twig?
<ul>
{% for target in targets %}
<li>{{ target instance of MyEntity ? target.getMobile : target }}</li>
{% else %}
<li>Nothing found.</li>
</ul>
In \Twig_Extension you can add tests
public function getTests()
{
return [
'instanceof' => new \Twig_Function_Method($this, 'isInstanceof')
];
}
/**
* #param $var
* #param $instance
* #return bool
*/
public function isInstanceof($var, $instance) {
return $var instanceof $instance;
}
And then use like
{% if value is instanceof('DateTime') %}
UPDATE 10-2021
Please be aware this answer has been written for symfony 3, and twig 2.
If you use a more recent version, please refer to the answer of #Garri Figueroa on this post.
As you can see in the twig documentation the class \Twig_Extension, \Twig_SimpleTest are now deprecated.
If you use a more recent version of symfony (I recommend it), please use the new class AbstractExtension, TwigFunction, etc
https://symfony.com/doc/5.3/templating/twig_extension.html
OLD VERSION : symfony 3.4
Here a nice way to do instanceof operator in twig with Extension :
1) Create your extention file where you want
(ex: src/OC/YourBundle/Twig/InstanceOfExtension.php )
With \Twig_Extension you can do many things, filter, fonction, but now we will create a Test.
So we implement function getTests(), and inside it we create a new \Twig_SimpleTest
The 1st arugment is the name of test you create, and the seconde a callable.
(can be a function() {}).
<?php
namespace OC\YourBundle\Twig;
class InstanceOfExtension extends \Twig_Extension {
public function getTests() {
return array(
new \Twig_SimpleTest('instanceof', array($this, 'isInstanceOf')),
);
}
public function isInstanceOf($var, $instance) {
$reflexionClass = new \ReflectionClass($instance);
return $reflexionClass->isInstance($var);
}
}
2) Register it in services.yml
(ex: src/OC/YourBundle/Resources/config/services.yml)
services:
[...may you have other services ...]
app.twig_extension:
class: OC\YourBundle\Twig\InstanceOfExtension
public: false
tags:
- { name: twig.extension }
3) Then use it in twig like this
{{ myvar is instanceof('\\OC\\YourBundle\\Entity\\YourEntityOrWhatEver') }}
Source from Adrien Brault => https://gist.github.com/adrienbrault/7045544
My solution for Symfony 4.3
1) Create the AppExtension class in src/Twig folder. (The class is automatically detected).
2) Extend the AbstractExtension class:
namespace App\Twig;
use Twig\Extension\AbstractExtension;
use Twig\TwigTest;
class AppExtension extends AbstractExtension
{
public function getTests()
{
return [
new TwigTest('instanceof', [$this, 'isInstanceof'])
];
}
/**
* #param $var
* #param $instance
* #return bool
*/
public function isInstanceof($var, $instance) {
return $var instanceof $instance;
}
}
3) Then use same code of valdas.mistolis answer:
{% if value is instanceof('DateTime') %}
4) Thanks valdas.mistolis and symfony documentation i got my own solution:
Twig Extension templating
Since PHP 5.5.0 you can compare class names next way:
{{ constant('class', exception) is constant('\\Symfony\\Component\\HttpKernel\\Exception\\HttpException') }}
This snippet can help in particular cases when you need strict comparison of class names. If you need to check implementation of interface or to check inheritance would be better to create twig extension described above.
Another solution :
class A {
...
public function isInstanceOfB() {
return $this instanceof B;
}
}
class B extends A {}
class C extends A {}
then in your twig :
{{ a.isInstanceOfB ? ... something for B instance ... : ... something for C instance ... }}
OR
{% if a.isInstanceOfB %}
... do something for B instance ...
{% else %}
... do something for C instance ...
{% endif %}
Another example when iterating through Symfony forms:
namespace App\Twig;
use Twig\Extension\AbstractExtension;
use Twig\TwigTest;
use App\Document\Embeded\Image;
use App\Document\Embeded\Gallery;
use App\Document\Embeded\Article;
class AppExtension extends AbstractExtension
{
public function getTests()
{
return [
new TwigTest('image', [$this, 'isImage']),
new TwigTest('gallery', [$this, 'isGallery']),
new TwigTest('article', [$this, 'isArticle']),
];
}
public function isImage($var) {
return $var instanceof Image;
}
public function isGallery($var) {
return $var instanceof Gallery;
}
public function isArticle($var) {
return $var instanceof Article;
}
}
Twig
{% if form.vars.data is gallery %}
This is a Gallery
{% elseif form.vars.data is article %}
This is an Article
{% endif %}
Other solution without ReflectionClass with a twig filter :
First you need a TwigExtension class. Then, add a function as twig filter or twig function,
namespace App\Twig;
use Twig\Extension\AbstractExtension;
use Twig\TwigFilter;
use Twig\TwigFunction;
class TwigExtension extends AbstractExtension
/**
* #return array|\Twig_Filter[]
*/
public function getFilters()
{
return [
new TwigFilter('is_instance_of', [$this, 'isInstanceOf'])
];
}
/**
* #param $object
* #param $class
* #return bool
*/
public function isInstanceOf($object, $class): bool
{
return is_a($object, $class, true);
}
}
And in twig template :
{% if true is same as (object|is_instance_of('ClassName')) %}
// do some stuff
{% endif %}
Check http://php.net/manual/fr/function.is-a.php
Use default filter in Twig like this:
{{ target.mobile|default(target) }}
Quite old, but I can't see here one more good possibility to achive this:
Enity:
public function __toString()
{
return 'NameOfYourEntity';
}
Twig:
{{ entity }}