Can twig macros return values? - php

I'm trying to write a template in Twig. Inside it I would like it to perform some manipulations on the string data that comes from the controller. In particular, there's a common manipulation (convert from underscore_case to CamelCase) that I'd like to bring out into a separate function. Then later I can use it repeatedly like {% set x = magic(a) ~ magic(b) %}. However I cannot find how to make such a reusable function inside the template itself. There are macros, but those don't seem to be able to return values. Filters are another option that seem to fit the bill, but I can only define those on PHP side.
Can this be done? Or should I do all the advanced string manipulation controller-side? It kinda feels like I'm pulling parts of display logic in there; things that should be in the view.

Twig is for outputting data. If you need to "transform" the data you need to do that before you send it to twig or you need to extend twig
Ideally, all the data you send to twig is just variables and arrays that needs the least amount of manipulation on their own.
When you're actually "in" twig, the data processing can be assumed to be "done" and only needs to be outputted in the appropriate places with minimal logic to decide user interface styles.
So revisit your logic and prepare your data better before sending it to twig.
An example for extending a toolkit class that contains our magic methods to do real wizardry.
class CustomToolkit
{
public function magic_a($a)
{
return strtolower($a); }
public function magic_b($b)
{
return camel_case($b);
}
public function magic_tidle($a, $b)
{
return $this->magic_a($a) ~ $this->magic_b($b);
}
}
Then you add this to your twig instance. I added here a complete instantiation loop. if you have a service provider you can just grab the instance from there and add it to that one.
$twig = new Twig_Environment(new Twig_Loader_Array([
'html' => $contents
]),[
'auto_reload' => true,
'debug' => false,
]);
$twig->addExtension('toolkit', new CustomToolkit ());
echo $twig->render('html', $values);
Then in your twig code you should be able to do something along the lines of
{% set x = toolkit.magic_tidle("value","value_b") %}

You are right, macros do not have return values and you cannot really make them have any. All they do is outputting strings.
Still, you are able to capture string output using set: https://twig.symfony.com/doc/2.x/tags/set.html
The syntax looks similar to this:
{% set var %}
{{ call.macro() }}
{% endset %}
The output of the macro call is then stored inside var. You may want to strip the whitespace though.
But then, consider rethinking what you are doing. Is this still presentation logic, or is your controller simply "too lazy" to transform the strings prior to passing them to twig? If it's really presentation logic, simply adding a twig filter by extending twig surely is worth the hassle. Not only because your code becomes testable.

Can this be done?
Yes! However, Twig templates are not the ideal places to run logic. At least, we should do our best to avoid it. Instead, Controller should return what Twig template needs. Logic should run in Service, Utility, Helper (you name it) etc. and Controller returns it to Twig. Twig then just displays it.
Can twig macros return values?
Yes! Look at this example. It accepts parameters and returns (not a real "return" thing but you get the idea) something back.
Example:
Assuming that the data you are trying to manipulate is something simple.
use Doctrine\Common\Inflector\Inflector;
Controller
{
action() {
$data = Inflector::camelize('hello_world'); // This will become helloWorld
return ....;
}
}
Look into Inflector class. It has useful stuff.

Related

What is more effective: filling variables with twig filters or passing them through the controller?

I've recently been using my own custom made twig filters. I've also been looking at ways to improve my performance.
I struggle in being able to distinguish when I should build a function in a Service, that then gets used by a Controller or if i should make it a twig filter instead. I am wondering which one executes faster or if there is any difference at all? To clarify my question I want to show how I fill the variable photos with both.
The twig template will look like this. It will just make image elements with the array of photos.
/content/photos.html.twig
{% for photo in photos %}
<img class="lozad card-img read-more-card-item-img" data-src="{{ photo }}">
{% endfor %}
Method 1: Pass the photos variable in a controller.
The controller would probably use a service called PhotoService and call the getReadMore function, while passing this to the twig template:
function controllerAction(){
$response = $this->render(
'/content/photos.html.twig',
array(
'photos' => $photoService->getReadMorePhotos($posts),
'loadMoreUrl' => $url,
'limit' => $limit
)
}
Method 2: Use a custom twig filter instead:
public function getFilters()
{
return array (new \Twig\TwigFilter('readMorePhotos', array($this, 'getReadMorePhotos')));
}
//Twig filter
public function getReadMorePhotos($posts)
{
...
$photos = [];
foreach ($posts as $post) {
$image = $this->getAbsoluteThumbsPath($post->getImage());
if ($image != null && $image != "" && strpos($image, '/img/posts/default-post.png') === false) {
$gallery[] = $image;
}
}
...
return $photos;
}
In the twig template the photos variable will be filled like this:
{{set photos = posts|readMorePhotos}}
Is there any difference in both methods?
very opinionated, so this answer essentially ... is an opinion.
the way I see it: the more non-standard functions/filters are used in a template, the worse off you're semantically. On one hand, whoever is editing or create the templates has to know of those functions/filter, which - in my opinion - is not desirable.
Also, as DarkBee already mentioned in the question's comment, filters are for transforming data, not fetching it. a twig function would be more appropriate, but I don't like it either because of the reason above (it also exposes all templates to that function, unless precautions are taken, on the other hand, it is expected to exist in all templates).
On the other hand, the template editor/creator also have to know about which variables may exist in the current context. So ultimately there is some prior knowledge required.
I want to propose a slightly different (and in my arrogant but humble opinion also better) option ...
As far as I see it, it would be better, if you could just use
{% for photo in post.photos %}
{# display of photos #}
{% endfor %}
because they are semantically connected to the post, and also it makes sense to assume they are also structurally connected. Since your use case seems a little different, it would possibly be:
{% for photo in posts.photos %}
which of course has different semantics and really screws with posts being an array, where photos be one element. However, imaginge the collection being an object, that has extra functions (I'll shamelessly extend doctrine's ArrayCollection):
<?php
namespace App\Collection;
use Doctrine\Common\Collections\ArrayCollection;
use App\Service\PhotoService;
class PostCollection extends ArrayCollection {
public $photoService;
public function __construct(array $elements, PhotoService $photoService) {
parent::__construct($elements);
$this->photoService = $photoService;
}
public function getPhotos() {
return $this->photoService->getReadMorePhotos($this->toArray())
}
}
and this is then to be called in your controller:
$response = $this->render(
'/content/photos.html.twig',
array(
'posts' => new PostCollection($posts, $photoService),
'loadMoreUrl' => $url,
'limit' => $limit
)
);
and can then be used as written above.
Why does this work? ArrayCollection implements a few interfaces among which there is ArrayAccess as well as IteratorAggregate (which extends Traversable), which lets you use it in a loop, and it will provide the collection it was given in its constructor (or which is changed in the modification methods), if the need arises, you can always retrieve an array via ArrayCollection::toArray.
Why do this? To get clean code.
Do I do this? No, tbh. But this is partly due to my habit of realizing almost all relations in the database, which in combination with ORM give me some of this for free.

Does Twig support this?

I'm playing around with a CMS idea for Symfony and I'm not sure if what I want to implement is possible. I'm looking for some guidance.
I want to create a twig function that acts more like a block.
I want to be able to write template like this:
{% content('main_content') %}
<p>Some markup here</p>
{% end content('main_content') %}
where content($id) is a function that gives me access to the markup inside and allows my twig extension to change that markup if needed.
The content($id) function's goal will be to change the mark up based on what $id is given.
Obviously my train of thought on this implementation is off because I cannot find any docs relevant to this.
I'm not sure how to word this question : Is it possible to write a twig function that acts like a block and gives me access to the inner data. If possible is there some examples or blogs you can link me to? I'm using Symfony so answers with Symfony implementations are a plus.
edit: an example of the content($id) function
class TwigExtension extends \Twig_Extension{
public function content ($id, $content) {
/* pseudo code now*/
if($id = "some condition"){
return $someNewContent;
}
return $content;
}
}
You need to create a new Twig tag. More documentation here: https://github.com/twigphp/Twig/blob/master/doc/advanced.rst#tags
Also the Twig_Node_Block will give a good idea of what you need to build:
https://github.com/twigphp/Twig/blob/1.x/lib/Twig/Node/Block.php

Blade templates - Toggle auto escape on the fly?

I have a template which calls a number of built in macros which I am including this template from several other places.
Sometimes, I need all of the macros to be called like this:
{{ Form::label('foo', 'Foo') }}
Other times I need them all to be called like this:
{{{ Form::label('foo', 'Foo') }}}
At the moment, I have two separate templates which are identical except for the extra { }, which means I have to edit two files every time I want to change anything.
Is there a way to switch the auto escaping on/off, so that I can use the same file for both situations?
Thanks
No, there's no feature in Laravel that would allow you to do that -- additionally, it'd probably be a bad idea from a code maintenance/security-audit point of view. Looking at a template a few weeks later and not knowing which variables were or were not escaped would be madness.
If you need to do this, "The Right" way would be to extend Blade with your own directive -- something like #escapeIsConfigIsOn and then put your logic for when to escape content in there. The top level function e is what blade uses internally for escaping.
#File: login/vendor/laravel/framework/src/Illuminate/Support/helpers.php
function e($value)
{
return htmlentities($value, ENT_QUOTES, 'UTF-8', false);
}

Smarty caching components

The Smarty FAQ suggests one way to handle cacheable fragments, but it needs each page controller to do tons of work up-front, instead of encapsulating things properly.
We want to get to a stage where we can do something like:
<div id="header">
{our_categories}
{our_subcategories category=$current_category}
</div>
The output of the our_ prefixed functions should be completely cacheable, only relying on the named parameters (if any.) If we referred to {our_categories} in more than one template, they should all refer to the same cached content.
(it's probably worth mentioning that we tried using {insert name="..."} and coding up our own functions, but the results weren't cacheable, and we ended up hand-cranking the HTML retunred, rather than benefiting from Smarty's template processing.)
Our first crack at this uses a custom function smarty_function_our_categories, but the caching's going horribly wrong. Here's what our function looks like:
function smarty_function_our_categories($params, &$smarty) {
$smarty->caching = 2;
$smarty->cache_lifetime = 3600; # 1 hour
if (!$smarty->is_cached(...)) {
// ... do db access to fetch data for template...
$smarty->assign(....);
}
return $smarty->fetch(...);
}
The problem is: calling $smarty->fetch() within a function confuses smarty, making it lose track of which templates have insert-tags, and which don't. The end result is that Smarty forgets to replace certain markers when serving up cached content (markers it puts there to say: "replace this with the results of some non-caching {insert ...} call.) In our case, this manifests itself with our site showing a couple of md5 checksums and a php-serialized memento where our main menu should be - that's not good.
We assume we've made a mistake in how we're building our components, so the question finally becomes:
How do you safely create a caching component using Smarty to render itself?
You should not change caching parameters from inside Smarty function. Wheither or not the result of the plugin output is cacheable is defined when you register plugin.
http://www.smarty.net/manual/en/caching.cacheable.php
To create uncachable content inside cachable template just use {dynamic} blocks like this:
//Registering dynamic non-caching block with Smarty
$template->register_block('dynamic', 'smarty_block_dynamic', false);
function smarty_block_dynamic($param, $content, &$smarty) {
return $content;
}

Templates in PHP, and the best way to notify the application that one exists?

I'm using CodeIgniter, and will likely use their template library as I want to keep things extremely simple to use. The content for the template variables will come from the database, but I want the business admins to know what content areas are available. Basically the names of the parameters when they choose a specific template. For instance, Joomla uses an extra XML file that defines each area, whereas Wordpress uses comments within a page template to inform the system that the PHP file is a template. I like the Joomla approach because you don't have to parse the PHP file to find the areas, but I like the Wordpress approach because you don't have an extra XML file associated with every template. Are there other approaches that I'm missing?
I think the nicest way would be to add a small hack to the template parser class. The code looks quite readable and clean in system/libraries/Parser.php. You could insert a hook in that class that can be used to keep track of the variables. I don't know, if it works, but here's a snippet:
class CI_Parser {
var $varCallback;
function setVarCallback($callbackFunction) {
$this->varCallback = $callbackFunction;
}
...
function _parse_single(...) {
$callback = $this->varCallback;
$callback($key);
}
...
//Somewhere in your code
function storeVarName($variableName) {
// Persist the variable name wherever you want here
}
$this->parser->setVarCallback('storeVarName');
You could do this directly in the controller:
// in the controller
print_r($data);
$this->load->view("main", $data);
Or a little more rudimentary, but you could pass to the template a PHP array of variables (or an object):
// in the controller
$data = array();
$data["namespace"] = array(
"title" => "My website",
"posts" => array("hi", "something else")
);
$this->load->view("main", $data);
And then in the view, have a flag to print_r the namespace to show all the vars available, so that business admins know exactly what to use.
// in the view
if(isset($namespace["showAllVars"])) print_r($namespace);
One option would be to call token_get_all on the PHP file (only when your business admins are loading it up), and parse the output of that.
The best approach, in my opinion, is to keep the variable definitions in another place (such as a database table, or a separate file). This will help with testing (i.e., a programmer can't just remove a tag and it's gone) and making sure things are still working as you move on with the application development in time.
Another advantage is that your application logic will be independent from the templating engine.
On a side note, if you expect a lot of traffic, you may consider using smarty instead. We have done extensive testing with most of the templating engines around and smarty is the fastest.

Categories