How to properly count template usage? - php

I'm using Symfony 6.1 with Twig on PHP 8.1. I've already set up my complete layout including fragments/partials/components or whatever we call reusable pieces of the layout at the moment. These fragments include certain kinds of navigations, quotes, tabs and so on which are always supposed to look and work the same and usually only receive a variable with the same name as an array with a few options.
This is how I'm embedding a YouTube-Video for example:
{% set youtube_video = { 'id': 'dQw4w9WgXcQ', 'title': 'Secret Video (unlisted)', 'language': 'en' } %} {% include 'fragment/youtube_video.html.twig' %}
Most of these fragments can be used multiple times on one page (= in the main template of the view or within the base template/s). Some of them however are supposed to be used only once and using them multiple times would create layout issues (e.g. a navigation for mobile devices with a specific CSS id).
For other fragments I would like to have a counter to add a CSS id in addition to a normal class:
<div class="fragment_video" id="fragment_video_{{ counter }}> ... </div>
The question now is how I can count within a fragment template like 'fragment/youtube_video.html.twig' how often this template has been used in that page already. I don't see any Twig functions or anything within the "app" variable for that.
Now I could create a custom Twig function "counter" and call that with with a unique name:
<div class="fragment_video" id="fragment_video_{{ counter('fragment_video') }}> ... </div>
or
{% if counter('fragment_video') == 1 %} ... {% endif %}
BUT how would I store the current count per given name? I don't want to use $GLOBALS in Twig or rather Symfony and storing that information in the session would keep it past the current request. Is there another solution available?
This is how it would look like as a Twig function:
public function getCounter(string $name): int
{
$name = 'twig_counter_'.$name;
if (isset($GLOBALS[$name])) {
++$GLOBALS[$name];
} else {
$GLOBALS[$name] = 1;
}
return $GLOBALS[$name];
}

As suggested by #DarkBee the instance of a TwigExtension object can use properties to keep track of some information:
/**
* #var array<int>
*/
private array $count = [];
...
public function getCounter(string $name): int
{
if (isset($this->count[$name])) {
++$this->count[$name];
} else {
$this->count[$name] = 1;
}
return $this->count[$name];
}

Related

Alternative to template_from_string for processing strings with calls to custom Twig functions

Is there an alternative to using template_from_string() for evaluating strings that contain calls to custom Twig functions (or any other complex Twig code for that matter)?
This is a simplified version of the code I have right now.
{% set content = {
'item_1' : {
'images' : {
'src': 'assets/images/img_1-600.jpg',
'srcset': "{{ assets('assets/images/img_1-600.jpg') }} 600w, {{ assets('assets/images/img_1-1200.jpg') }} 1200w",
}
}
# additional items follow
}
{% for data in content %}
<img src="{{ data.images.src }}" srcset="{{ include(template_from_string(data.images.srcset)) }}" alt="">
{% endfor %}
The assets() function simply returns a revisioned version of the static asset for the given path (i.e. assets('assets/images/img_1-600.jpg) renders as something like 'assets/images/img_1-600-a4c2254a6d.jpg).
The problems start with img srcset attribute which can become pretty complex and usually contains references to multiple assets that need to return revisioned values for static assets.
Now I know that I could modify the assets() function to support that kind of complex scenario but I'd like to keep things simple and let assets() only handle 1:1 transformations.
The only way to achieve this by using functionality provided by Twig seems to be template_from_string() combined with include, which in itself is not terrible but it does kind of look bulky for a simple operation as evaluating and rendering a string.
Not to mention that template_from_string requires StringLoaderExtension() to be loaded by default, which again I'd like to avoid using for performance reasons.
While the concatenation approach is not a bad idea, after much consideration I came to the conclusion that this is one of those places where having a custom Twig function makes much more sense.
The main argument in favor of using a function compared to simple concatenation is that with a function there is no need to worry about properly formating the template code (i.e. forget a space between the asset and the size descriptor in the srcset attribute value).
This approach also completely eliminates any need to use template_from_string() or additional extension dependencies.
Here is the final solution.
The template code, plain and simple, with plenty of overview.
{% set content = {
'item_1' : {
'images' : {
'src': 'assets/images/img-600.jpg',
'srcset': srcset([
[ assets('assets/images/img-600.jpg'), '600w' ],
[ assets('assets/images/img-800.jpg'), '800w' ],
[ assets('assets/images/img-1000.jpg'), '1000w' ],
[ assets('assets/images/img-1200.jpg'), '1200w' ]
]),
}
}
# additional items follow
}
{% for data in content %}
<img src="{{ data.images.src }}" srcset="{{ data.images.srcset }}" alt="">
{% endfor %}
The actual Twig function called srcset, that is used to generate a value for the srcset attribute from provided data in the above template.
public function srcset(array $srcset)
{
$output = [];
foreach ($srcset as $set) {
// Both parameters need to exist or the set isn't added to the output...
if (!(isset($set[0]) && isset($set[1]))) {
// ... just some exception handling that isn't of relevance for the question
// ...
continue;
}
$output[] = sprintf('%s %s', $set[0], $set[1]);
}
return implode(', ', $output);
}
To avoid any extra plugins, you could just concat the variables
{% set content = {
'item_1' : {
'images' : {
'src': 'assets/images/img_1-600.jpg',
'srcset': assets('assets/images/img_1-600.jpg')~' 600w,'~assets('assets/images/img_1-1200.jpg')~' 1200w',
}
}
} %}

OctoberCMS global page properties?

Is it possible to set a series of global properties (such as social media usernames) that are available to all page views in OctoberCMS rather than having them associated to one CMS page or Static Page at a time?
For example, being able to use {{ twitter_username }} in any template, but it wouldn't show up as a field in any page form on the backend.
UPDATE: this can be achieved by registering a Twig function using registerMarkupTags in your plugin:
use System\Classes\PluginBase;
class Plugin extends PluginBase
{
public function registerMarkupTags()
{
return [
'functions' => [
'globals' => function($var) {
switch ($var) {
case 'twitter_username':
return 'mytwitterusername';
}
return null;
},
],
];
}
}
In this case, calling {{ globals('twitter_username') }} from any template prints mytwitterusername.
Hmm yes better you need to add code to life-cycle method in layouts, so now page which are using that layout will have this info already loaded.
In layout code block you can use something like this
use RainLab\Pages\Classes\Page as StaticPage;
function onStart() {
$pageName = 'static-test'; // this will be static page name/filename/title
$staticPage = StaticPage::load($this->controller->getTheme(), $pageName);
$this['my_title'] = $staticPage->viewBag['title'];
$this['twitter_username'] = $staticPage->viewBag['twitter_username'];
}
now inside your cms page you can use this variable
<h1>{{ my_title }} </h1>
<h3>{{ twitter_username }} </h3>
let me know if it you find any issues
You could also use theme config file which gives you more flexibility rather than hardcoding the values in to the code block.
https://octobercms.com/docs/themes/development#customization

ezplatform render links with url and object name from multi-relational content item in content type

does anyone know now to create a custom view type for ez platform? The default 3 have been exhausted and we need a new one for 'link'
Alternatively, does anyone know how to use the render( controller( with a custom template as this would also resolve out block right now.
Basically, we have a multi-relational field in a content object used and we need to print links to all the related contentIds, path works great but we cannot find a way to extract the name of the content object for the link without doing some fairly funky tpl logic of passing in params.
EG: As a hack for now we can pass in "embed_type" as a custom param with the render(controller("ez_content:viewAction" to pull in an alternate view for the content object for a specific content type and view type.
{% if embed_type is defined %}
{% include "embed/#{embed_type}.html.twig" %}
{% else %}
<h1>{{ ez_field_value( content, 'name') }}</h1>
{% endif %}
However, this is very ugly and all we really want to do is use 1 template for all content types, so all we need to do is loop through the relational field and print links (as the only thing available in the content field: "destination ids"). I am sure there used to be this option in the docs but i cannot find it anymore eg:
{% set links = ez_field_value( footer, "first_links_row" ).destinationContentIds%}
{% for id in links %}
{{ render(controller("ez_content:viewAction", {"contentId": id, "template": "link.html.twig"})) }}
{% endfor %}
Where the link.html.twig would simple print the link:
<a href="{{ path( "ez_urlalias", {"contentId": id} ) }}">
{{ ez_field_value( content, "name" ) }}
</a>
If using a custom tpl is not possible with the render (controller ( helper then a new custom view type would also fix this issue, but i cannot find documentation for either.
You can create a twig function that would do that. We have something like this:
Definition:
new Twig_SimpleFunction(
'content_name',
array($this, 'getContentName')
),
Implementation:
public function getContentName($content, $forcedLanguage = null)
{
if (!$content instanceof Content && !$content instanceof ContentInfo) {
$contentInfo = $this->repository->getContentService()->loadContentInfo($content);
} elseif ($content instanceof Content) {
$contentInfo = $content->contentInfo;
} else {
$contentInfo = $content;
}
return $this->translationHelper->getTranslatedContentNameByContentInfo($contentInfo, $forcedLanguage);
}
which enables you to provide either content id, content info or content itself, and it returns translated content name

D8 Body Classes

I'm having a hell of a time trying to get classes on my body element in D8. I'm using a custom theme and the code I have so far looks like this:
themename.theme
use Drupal\Component\Utility\Html;
/**
* Preprocess variables for html templates.
*/
function HOOK_preprocess_html(&$variables) {
$path_class = !$variables['root_path'] ? 'path-frontpage' : 'path-' . Html::getClass($variables['root_path']);
if (isset($path_class)) {
$variables['attributes']['class'][] = $path_class;
}
}
html.html.twig
{%
set body_classes = [
not root_path ? 'path-frontpage' : 'path-' ~ root_path|clean_class,
]
%}
<body{{ attributes.addClass(body_classes) }}>
I get classes on my body element, but the NID is empty, reading, 'page-node' where I need it to read, 'page-node-NID'.
You have to replace HOOK with the name of your theme mytheme_preprocess_html otherwise Drupal will not call the hook.

Symfony2/Datatable Bundle - error: set a datatable id in your action with "setDatatableId" using the id from in your view

I import the Bundle AliDatatableBundle on github following the doc step one by one except the part $ bin/vendor install in the installation because with Symfony2.6, there's no need to do this.
When I create the function in my controller, and the rendering in my twig, I have this error:
An exception has been thrown during the rendering of a template ("No
instance found for datatable, you should set a datatable id in your
action with "setDatatableId" using the id from your view ") in
MySpaceGestionPatrimoinesBundle:Batiments:indexBatiments.html.twig at
line 37.
Here's the code for the controller:
/**
* set datatable configs
*
* #return \Ali\DatatableBundle\Util\Datatable
*/
private function _datatable()
{
$controller_instance = $this;
return $this->get('datatable')
->setDatatableId('batiments')
//->setEntity("MySpaceDatabaseBundle:Batiments", "b")
// replace "XXXMyBundle:Entity" by your entity
->setFields(
array(
"Nom" => 'b.nom',
// Declaration for fields:
"Reférence" => 'b.referencebatiment',
// "label" => "alias.field_attribute_for_dql"
"Ensembles" => 'b.ensembles',
"_identifier_" => 'b.id')
// you have to put the identifier field without label. Do not replace the "_identifier_"
)
//->setWhere(
// set your dql where statement
//'x.address = :address',
//array('address' => 'Paris')
//)
//->setOrder("x.created", "desc")
// it's also possible to set the default order
->setHasAction(true);
// you can disable action column from here by setting "false".
}
/**
* Grid action
* #return Response
*/
public function gridAction()
{
return $this->_datatable()->execute();
// call the "execute" method in your grid action
}
/**
* Lists all entities.
* #return Response
*/
public function indexAction()
{
$this->_datatable();
// call the datatable config initializer
return $this->render('MySpaceGestionPatrimoinesBundle:Batiments:indexBatiments.html.twig');
// replace "XXXMyBundle:Module:index.html.twig" by yours
}
And then the code for my twig:
{% extends "MySpaceWelcomeBundle::layout.html.twig" %}
{% block content %}
<div class="jumbotron">
<h4><u>Rechercher un bâtiment et ses affectations spécifiques:</u></h4>
<br>
<div>
{{ datatable({
'id' : 'batiments',
'js' : {
'sAjaxSource' : path('mySpace_formulaire_recherche_batiments')
}
})
}}
</div>
</div>
{% endblock %}
I really don't understand, someone could help me to fix this?
I have already clear the cache, install assets and took over again from the beginning the doc, I do not understand.
Thank you in advance.
It seems like everything was good. I can't see the problem here, if you followed the doc of aliBundle. Try using the Jquery datatable here, the installation is easier I think and better for your project if it's to manage your entities in Symfony.
If you have questions for Jquery DataTables, don't hesitate to ask, I'm using it for a Symfony project too.

Categories