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

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 ;)

Related

call twig function from custom twig function

I created a custom twig function in AppExtension Class. I need to call form_label() from this new function. ¿Is it posible? I tried but does not work:
from template I call:
{{ myFunc(form.someField) }}
public function myFunc( $field )
{
$html = form_label($field);
}
The idea is to render each form field in a different order/way than the form_widget(form) twig function. The "form_label()" function it's not reconized.
Thx for any suggestion.
I feel like this is the wrong approach to handle this. Extensions are for transforming data not really to manipulate the form definition itself.
First of all the order is defined as in the form type, so you can swap those around. To render the fields differently you can use form themes, or even rendering a custom form type.
Alternatively if its a one time thing (you could also create a macro for this) you can also instead of form_widget(form) order them in the way you like.
{{ form_start(form) }}
{{ form_row(form.field3) }}
{{ form_row(form.field1, { attr: { class: 'im-different' } }) }}
{{ form_row(form.field2) }}
{{ form_end(form) }}
Or even go deeper.
{{ form_start(form) }}
{{ form_row(form.field3) }}
<div>
{{ form_label(form.field1) }}
{{ form_widget(form.field1) }}
{{ form_errors(form.field1) }}
</div>
{{ form_row(form.field2) }}
{{ form_end(form) }}
To see these functions and how they all rendered by default you can look at form_div_layout.html.twig.
I agree with Jenne van der Meer and Nico Haase that your approach isn't particularly optimal. If I had the choice, I would go a different route: Instead of rendering in your function, render in twig, then pass the result to the function (like {{ myFunc(form_label(form), form) }}). Since you omit what your function actually needs and/or does, it's hard to provide further advice. However, I'm absolutely sure, that rendering can be done in twig before or after entering your function, via a macro/block, maybe even a form theme).
However, if you really really require your function to render the form field ... the following will possibly help you. I strongly advise against doing this, there's probably a better suited solution.
The form_label function is slightly more complex than a simple function. Instead, it uses twig's compile mechanisms to generate specific php code. It will eventually call:
FormRenderer::searchAndRenderBlock(FormView $view, string $blockNameSuffix, array $variables = [])
Diving deep into the compiler, the template call form_label(form, options) would be turned into:
$this->env->getRuntime('Symfony\Component\Form\FormRenderer')->searchAndRenderBlock(
$form, 'label', $options
);
where the $this->env seems to be the twig environment. That means, to call this in your twig extension you need to have access to the proper Twig environment, and then it should already work with the recipe I just provided. Especially if you can omit the options argument, I didn't take a deeper look into how that one's assembled (but it's probably just straight forward).
So your twig function must be defined via:
public function getFunctions(): array
{
return [
new TwigFunction('myFunc', [&$this, 'myFunc'], [
'needs_environment' => true, // <--- this!
'is_safe' => ['html'],
]),
];
}
public function myFunc(\Twig\Environment $env, $field) {
// other stuff
$html = $env->getRuntime(\'Symfony\Component\Form\FormRenderer\')->searchAndRenderBlock(
$field, 'label', $options
);
return $html;
}

Check if a custom Twig function exists and then call it

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.

How to pass an array in a redirect

I'm trying to pass an array in a redirect
$qtedispo = array($x,$c,$s);
return $this->redirect($this->generateUrl('demandeveh_afficherConf',array('qte'=>$qtedispo));
but when i try to call that array in twig
{%for q in qte %}
{{qte[0]}}
{%endfor%}
it tells me that the variable qte is unknown. Any help please?
You are making an HTTP redirection with an array as parameter.
If the route (on which you are making the redirection) is waiting for an argument (e.g. function afficherConf(array $qte)), you have to go into and pass the $qte parameter to the rendered template.
For instance:
// _controller part of to route "demandeveh_afficherConf"
public function afficherConfAction(array $qte)
{
// ...
return $this->render('AppBundle:Demandeveh:afficherConf.html.twig', array('qte' => $qte));
}
Then in the template ("afficherConf.html.twig" for this example), you should be able to do:
{% for q in qte %}
{{ q }}
{% endfor %}
Depending on the number of dimensions of your $qte var.
Note:
You should have a look at the doc.

How to generate routes ignoring extra passed parameters

Lets say that i have the following route annotations for a controller action:
* #Route("/post/{post_id}/", name="post.view")
* #Route("/post/", name="post.current_view")
And I want to use twig to generate the url for this:
{{ url(basePath~'view', {'post_id':post.postId}) }}
//basePath will either be "post." or "post.current_"
What i currently get is:
domain.com/post/1/
domain.com/post/?post_id=1
What i want though is for the second route to be generated ignoring any "EXTRA" parameters passed to it so that i would only get:
domain.com/post/
Does anyone know if this is something that can be natively accomplished? I know i could right a custom twig function that uses the router and then i can generate the routes and strip the query string but i want to avoid that if there is an easy toggle somewhere that i have missed.
Solution #1 you could just add an if clause
{% if BasePath == 'post.' %}
{{ url(BasePath~'view', {'post_id':post.postId}) }}
{% elseif BasePath == 'post.current_' %}
{{ url(BasePath~'view') }}
{% endif %}
maybe not the most elegant but should work.
Solution #2
spliting url with question marks and getting the first string
{% set myUrl = url(basePath~'view', {'post_id':post.postId}) %}
{{ myUrl|split("?")|first }}
Solution #3 Or you can override the url function by extending RoutingExtension class of twig.
Symfony\Bridge\Twig\Extension\RoutingExtension
can find an example here with path but url should be the same.
You should override this function
public function getUrl($name, $parameters = array(), $schemeRelative = false)
{
return $this->generator->generate($name, $parameters, $schemeRelative ? UrlGeneratorInterface::NETWORK_PATH : UrlGeneratorInterface::ABSOLUTE_URL);
}
your function could look like this:
public function getUrl($name, $parameters = array(), $schemeRelative = false)
{
$yourUrl = parent::getUrl($name, $parameters = array(), $schemeRelative = false);
return strstr($yourUrl, '?' , true);
}
what id oes it removes everything afther the question mark.
To override the default class you have to add to the parameters
twig.extension.routing.class: MyNamespace\MyRoutingExtension
I guess not, you need a preg_replace filter and this isn't natively defined

Using doctrine database object in a template

I am new to Symfony and am finally beginning to understand how to query a database using a Doctrine. However, I am lost as far understanding how to use the database object content in a Twig template.
Lets say my database object contains product Id's, names, prices, for 50 different products. After I am done querying the database in the controller, I do the following, to pass the database object into the Twig template:
public function searchAction($word)
{
//query database using the $word slug and prepare database object accordingly
$dataObject; // contains query results
return $this->render('GreatBundle:Default:search.html.twig', array('word' => $word));
}
This is where I am stuck. Now I have a Twig template, I would like to pass the DB object from the controller and then print out the database data in my Twig template.
I appreciate any suggestions as to how I can accomplish this.
Many thanks in advance!
I'll respond with an example (more easier for me to explain)
You want to search something with a slug (the var $word in your example). Let's say you want to find a article with that.
So your controller :
public function searchAction($word)
{
//query database using the $word slug and prepare database object accordingly
// Search the list of articles with the slug "$word" in your model
$articleRepository = $this->getDoctrine()->getRepositoy('GreatBundle:Article');
$dataObject = $articleRepository->findBySlug($word);
// So the result is in $dataObject and to print the result in your twig, your pass the var in your template
return $this->render('GreatBundle:Default:search.html.twig', array('result' => $dataObject));
}
The twig template 'GreatBundle:Default:search.html.twig'
{% for item in result %}
{{ item.title }} : {{ item.content }}
{% endfor %}
Just look the second example in the Symfony2 Book (Sf2 Book - templating), you have to use the function "for" to parse your object (like an array in php !)
Example in your twig template :
{% for item in word %}
{{ item.id }} - {{ item.name }} - {{ item.description }}{# etc... #}<br>
{% else %}
<h2>Aoutch ! No data !</h2>
{% endfor %}
Ah, and it's not the good var in your render method (but it's was for your example !)
public function searchAction($word)
{
//query database using the $word slug and prepare database object accordingly
$dataObject; // contains query results
return $this->render('GreatBundle:Default:search.html.twig', array('word' => $dataObject));
}

Resources