Check if a custom Twig function exists and then call it - symfony

I test if a custom Twig function exists:
{% if methodExist('sg_datatables_render') %}
{{ sg_datatables_render(datatable) }}
{% else %}
{{ datatable_render((datatable)) }}
{% endif %}
methodExist is a simple Twig_Function:
/**
* #param $name
* #return bool
*/
public function methodExist($name){
if($this->container->get('twig')->getFunction($name)){
return true;
}else{
return false;
}
}
But I get an exception:
Unknown "sg_datatables_render" function. Did you mean "datatable_render"?
500 Internal Server Error - Twig_Error_Syntax

I tried to replicate this, and indeed, {{ sg_datatables_render(datatable) }} seems to always cause a Twig_Error_Syntax exception when sg_datatables_render has not been registered as a Twig function.
I then tried something like this. It's ugly, but I wanted to know if it works. The idea is that a non-existing function will be created to avoid the exception being thrown:
$twig->addFunction(new Twig_Function('methodExist', function(Twig_Environment $twig, $name) {
$hasFunction = $twig->getFunction($name) !== false;
if (!$hasFunction) {
// The callback function defaults to null so I have omitted it here
return $twig->addFunction(new Twig_Function($name));
}
return $hasFunction;
}, ['needs_environment' => true]));
But it didn't work. I also tried to add a simple callback function to the new function with no success.
I tried the same trick with filters, i.e.:
{% if filterExists('sg_datatables_render') %}
{{ datatable|sg_datatables_render }}
{% else %}
{{ datatable|datatable_render }}
{% endif %}
It didn't work either.
Solution 1: {{ renderDatatable(datatable) }}
Something like this does work (yay!):
$twig->addFunction(new Twig_Function('renderDatatable', function(Twig_Environment $twig, $datatable) {
$sgFunction = $twig->getFunction('sg_datatables_render');
if ($sgFunction !== false) {
return $sgFunction->getCallable()($datatable);
}
return $twig->getFunction('datatable_render')->getCallable()($datatable);
}, ['needs_environment' => true]));
And then in Twig:
{{ renderDatatable(datatable) }}
The renderDatatable function is specific to rendering datatables, i.e. it's not a general/multipurpose function like your methodExist is, but it works. You can of course try to create a more general implementation yourself.
Solution 2: {{ fn('sg_datatables_render', datatable) }}
Here's a more general approach. Create an additional Twig function to accompany methodExist:
$twig->addFunction(new Twig_Function('fn', function(Twig_Environment $twig, $name, ...$args) {
$fn = $twig->getFunction($name);
if ($fn === false) {
return null;
}
// You could add some kind of error handling here
return $fn->getCallable()(...$args);
}, ['needs_environment' => true]));
Then you could modify your original code to this:
{% if methodExist('sg_datatables_render') %}
{{ fn('sg_datatables_render', datatable) }}
{% else %}
{{ datatable_render((datatable)) }}
{% endif %}
Or even use the ternary operator:
{{ methodExist('sg_datatables_render') ? fn('sg_datatables_render', datatable) : datatable_render(datatable) }}
PS
Here's how I'd write the methodExist function:
$twig->addFunction(new Twig_Function('methodExists', function(Twig_Environment $twig, $name) {
return $twig->getFunction($name) !== false;
}, ['needs_environment' => true]));
I added s to the end of the function's name because the function checks whether a method/function exists.
I added ['needs_environment' => true] so I can use $twig instead of $this->container->get('twig'). (Kudos to yceruto for this tip.)
getFunction returns false if the function doesn't exist (see the docs), so I simplified the function body to a single-line return statement.

Related

Pass custom data to Form Theme in Symfony/Twig

I'm new to Symfony, currently working with 4.4, and am trying to implement a simple form theme for one specific form, i.e. the theme is in the same file as the form's html.twig file. I have my own form_row block and I'm trying to pass in custom data (an icon to use within the div) when calling it, so something like (this is highly summarised!):
{{ form_row(signUpForm.email, {
attr: { placeholder: 'e.g. bobsmith#gmail.com' },
icon: 'envelope'
}) }}
then try to render in the form as
{%- block form_row -%}
<div>
{{ form_label(form) }}
{{ form_widget(form, {attr: class: 'input'}) }}
<i class="icon {{ icon }}"></i>
</div>
I tried also passing icon via the formBuilder, along the lines of
$builder
->add('email', EmailType::class, [
'attr'=> ['icon' => 'envelope']
])
but no joy. Surely this must be possible! Any assistance would be much appreciated. Thanks
I'm not sure but you can access to your form variable with {{form}} in your theme. So you can use it.
Hope this help
Edit:
You can add property in your entity and use it in template like this :
{%- block form_row -%}
<div>
{{ form_label(form) }}
{{ form_widget(form, {attr: class: 'input'}) }}
{% set myData = form.vars.value %}
<i class="icon {% if myData.type == 'mail' %}envelope{% endif %}"></i>
I think you have a better way to do this but i do something like that and it's work.
So, I managed to find the "proper" way to do what I want: a Form Type Extension. The Symfony Casts tutorial on it is pretty good. In short, you create a class to extend your main form input class; in my case I was dealing with a text based input, so I created an App\Form\ypeExtension\TextIconExtension class, extended from FormTypeExtensionInterface, then implemented configureOptions and buildView (I removed the functions in the interface I didn't fill in):
class TextIconExtension implements FormTypeExtensionInterface
{
public function buildView(FormView $view, FormInterface $form, array $options)
{
$view->vars['icon'] = $options['icon'];
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'icon' => 'user'
]);
}
public function getExtendedType()
{
return TextType::class;
}
public function getExtendedTypes(): iterable
{
return [TextType::class];
}
}
Then, in my form template, I can simply pass a value for icon:
{{ form_row(signUpForm.email, {
attr: { placeholder: 'e.g. bobsmith#gmail.com' },
icon: 'envelope'
}) }}

Passing multiple arguments through twig path

I have this twig code:
<div style="padding-left: 5em" class="comment">
<p>{{ comment.author.name }} - {{ comment.created|date('j. n. Y H:i') }}</p>
<p>{{ comment.text }}</p>
<p>Odpovědět na komentář</p>
{% for child in comment.children %}
{% include 'BlogApplicationBundle:Post:_comment.html.twig' with {'comment' : child}%}
{% endfor %}
</div>
and this is function that processes the output from link in twig code:
/**
* #Route("/post/{id}/newcommentresponse", name="comment_response_new")
* #Template("BlogApplicationBundle:Post:form.html.twig")
*/
public function commentResponceAction($id,$idc)
{
$comment = new Comment();
$form = $this->createForm(new CommentType(), $comment);
return array(
'form' => $form->createView()
);
}
when i try to run code i get this error :
Controller "Cvut\Fit\BiWt1\Blog\ApplicationBundle\Controller\CommentController::commentResponceAction()"
requires that you provide a value for the "$idc" argument (because
there is no default value or because there is a non optional argument
after this one).
So it seems that second argument passsed through link is ignored and i have no idea what am i doing wrong.
You are missing the $idc definition in your #Route annotation. It should look something like this:
#Route("/post/{id}/newcommentresponse/{idc}", name="comment_response_new")
or this:
#Route("/post/{id}/{idc}/newcommentresponse", name="comment_response_new")
You can also leave it out of the route and function declaration and grab it directly from the Controller:
/**
* #Route("/post/{id}/newcommentresponse", name="comment_response_new")
* #Template("BlogApplicationBundle:Post:form.html.twig")
*/
public function commentResponceAction($id)
{
$idc = $request->query->get('idc');

How to get params in twig file

how can i use $_GET params in TWIG file like using PHP and alerting with JS.
URI-> ?comment=added...
in TWIG,
if($_GET['comment'] == "added"){
...echo '<script>alert("in TWIG file!");</script>';
}
hope it will help you
{% if app.request.get('comment') == "added" %}
<script>alert("in TWIG file!");</script>
{% endif %}
Depending on what you're really trying to achieve, the "Symfony way" of showing confirmation messages would be to use "Flash Messages":
YourController.php:
public function updateAction()
{
$form = $this->createForm(...);
$form->handleRequest($this->getRequest());
if ($form->isValid()) {
// do some sort of processing
$this->get('session')->getFlashBag()->add(
'notice',
'Your changes were saved!'
);
return $this->redirect($this->generateUrl(...));
}
return $this->render(...);
}
Your TwigTemplate.twig:
{% for flashMessage in app.session.flashbag.get('notice') %}
<div class="flash-notice">
{{ flashMessage }}
</div>
{% endfor %}
This way you have multiple advantages:
Redirecting after action prevents form reloading.
Message cannot be triggered from outside.
Flash messages are only fetched once.
See the official documentation on this topic.
The "correct" solution would be use your controller to provide a function to Twig rather than switching on the querystring. This will be more robust and provide better security:
Controller:
function someAction()
{
$params = array('added' => false);
if( /* form logic post */ )
{
//some logic to define 'added'
$params['added'] = true;
}
$this->render('template_name', $params);
}
view:
{% if added %}
<script>alert('added');</script>
{% endif %}
The reasoning is that this is more secure (I can't trigger the alert by just browsing to the url), it maintains all business logic in the controller and you're also able to handle any errors - e.g. if you browse to foo.php?comment=added and there is an error wherein your comment isn't added, the user will still receive the alert.

How to apply a filter whose name is stored in a variable

Basically, I am looking for the filter equivalent of the attribute() function for objects and arrays. I want to be able to apply a filter, whose name is stored in a variable.
{#
This works and is really useful
prints object.someVar
#}
{% set varName = 'someVar' %}
{{ attribute(object,varName) }}
{#
The function "filter" does not exist
#}
{% set filterName = 'somefilter' %}
{{ filter(object,filterName) }}
To reach this goal you have to extend your TwigFilter.
How to initially write you Extension you can read here.
Assuming that you have created you extension, you have define your function, let's say applyFilter.
//YourTwigFilterExtension.php
public function getFunctions()
{
return array(
...
'apply_filter' => new \Twig_Function_Method($this, 'applyFilter'),
);
}
Then, you have to define this function
public function applyFilter($context, $filterName)
{
// handle parameters here, by calling the
// appropriate filter and pass $context there
}
After this manipulations you'll be able to call in Twig:
{{ apply_filter(object, 'filterName') }}
Cheers ;)

How to load a controller function and render it in a twig tag using Symfony2?

I am using Symfony2 and Twig. I have a function (below) in my controller that returns a specific text. Is it possible to call that function directly from my template and change the {{text}} in my template to whatever the function returns, possibly via Ajax?
Here's my function:
public function generateCode($url) {
$url = $_SERVER['SERVER_NAME'] . '/embed/' . $url;
$return = '<iframe>'.$url.'</iframe>';
return $return;
}
Another controller function calls the function above and renders my template:
public function getCodeAction($url) {
$text = $this->generateCode($url);
return $this->render('MyMyBundle:User:code.html.twig', array('text' => $text));
}
In my template I am using:
{{ text }}
to display the value.
In Symfony 2.2, this was changed.
The render tag signature and arguments changed.
Before:
{% render 'BlogBundle:Post:list' with { 'limit': 2 }, { 'alt': BlogBundle:Post:error' } %}
After:
{% render controller('BlogBundle:Post:list', { 'limit': 2 }), { 'alt': 'BlogBundle:Post:error' } %}
or
{{ render(controller('BlogBundle:Post:list', { 'limit': 2 }), { 'alt': 'BlogBundle:Post:error'}) }}
Note: The function is the preferred way.
See https://github.com/symfony/symfony/blob/2.2/UPGRADE-2.2.md
You can use ajax if you have dynamic data, but as far as I can see from your brief info, you can always execute that controller function directly from your view:
{% render "MyMyBundle:User:generateCode" with { 'url': 'your url here' } %}
More Information on this available at:
http://symfony.com/doc/2.0/quick_tour/the_view.html, under Embedding other Controllers
For the record, in new versions you need to use the absolute URL:
{{ render url('my_route_id', {'param': value}) }}
{{ render(controller("AcmeDemoBundle:Demo:topArticles", {'num': 10})) }}
In Silex I solved it like this:
{{ render(url('route_name', {'param': value})) }}
If you do not have the route name, URL can be used:
{{ render(app.request.baseUrl ~ '/some-path/' ~ value) }}
If using URL we should always concat the baseUrl.
Symfony 2.6+
in twig:
{{ render(controller('AppBundle:PropertySearch:featuredProperties', {'limit': 15})) }}
controller:
/**
* featuredPropertiesAction
*
* #param Request $request
* #param int $limit
*
* #return Response
*/
public function featuredPropertiesAction(Request $request, $limit)
{
$search = $this->resultsHelper->featuredSearch($limit);
return $this->render('HASearchBundle::featured_properties.html.twig', [
'search' => $search,
]);
}

Resources