Extends template in Twig with Twig_Loader_String - twig

I have to render a template with Twig_Loader_String and I need to extend a template, like below:
$body='{% extends "/views/path/to/my/template" %}
{% block body %} Hello {% endblock %}';
And in PHPs side I wrote:
$loader = new Twig_Loader_String();
$twig = new Twig_Environment($loader);
$twig->render($body,array());
I don't understand why the result after render is just:
/views/path/to/my/template

You only have a string loader, that very string is your actual template. You're extending it, and overriding the body block - but it has no body block.

Expanding the answer by #Maerlyn with some code how to do it:
$loaders = [
new Twig_Loader_Filesystem('path/to/twig/views'),
new Twig_Loader_String()
];
$loader = new Twig_Loader_Chain($loaders);
$twig = new Twig_Environment($loader);

Related

How to use safe filter on custom Django tag?

I'm trying to implement a custom Django tag that will formulate an import statement in Javascript to load my vue3 app as well as its components template html files using a get request in axios.
The custom tag in my templatetags directory looks like this:
templatetags/vuecomponents.py
from django import template
from django.templatetags.static import static
register = template.Library()
#register.simple_tag
def v_load_app_component(app_name, script_name, components):
components = components.strip("[]").split(", ")
app_script = static(f"js/{script_name}.js")
comps = [static(f"components/{name}.html") for name in components]
return f"import {{{ app_name }}} from \"{app_script}?{components[0]}={comps[0]}\""
Right now it only loads the first component as I just want a prototype. The only issue is when I drop this into a template like so:
createpost.html
<script type="module">
{% v_load_app_component "creator" "internalpostform" "[internalpostform]" %}
// OUTPUTS:
// import { creator } from "static/js/internalpostform.js?internalpostform=internalpostform.html"
creator.mount("#app")
</script>
It outputs the relevant import statement as:
import { creator } from "static/js/internalpostform.js?internalpostform=internalpostform.html"
With the double quotes escaped. Even when I tried to apply the safe filter ({% v_load_app_component "creator" "internalpostform" "[internalpostform]"|safe %}) it still escaped the output of my custom tag function.
How can I make it to where the output of my custom tag doesn't automatically have symbols converted to html entities?
I found it after a little digging in Django's documentation. The safe filter is only for variables i.e. {{ variable|safe }} but does not apply to a tag {% tag "argument"|safe %}.
To prevent Django from escaping the output of a tag you simply use {% autoescape off %}
{% autoescape off %}
{% v_load_app_component "creator" "internalpostform" "[internalpostform]" %}
{% endautoescape %}
This results in the desired behavior.

Get subcategories with twig on listing page

I'm trying to get the subcategories on a listing page in Shopware 6. However i can't seem to find the functionality to get an array with the subcategories with the template variables.
My goal is to loop over the children and make some kind of fastlinks in a CMS-element.
Is there a standard functionality build in shopware to get the children by id or name in TWIG?
I've tried to find anything relevant in
page.header.navigation.active
But the child data isn't available.
Thanks!
I don't think there is a build-in function to fetch this, but if you're doing it in a new CMS-Element you can take advantage of it by adding a new DataResolver for your new element and pass the subcategories to your CMS-element.
// myPlugin/src/DataResolver/SubcategoryListCmsElementResolver.php
<?php
namespace MyPlugin\DataResolver;
use Shopware\Core\Content\Category\CategoryDefinition;
use Shopware\Core\Content\Category\CategoryEntity;
use Shopware\Core\Content\Cms\Aggregate\CmsSlot\CmsSlotEntity;
use Shopware\Core\Content\Cms\DataResolver\CriteriaCollection;
use Shopware\Core\Content\Cms\DataResolver\Element\AbstractCmsElementResolver;
use Shopware\Core\Content\Cms\DataResolver\Element\ElementDataCollection;
use Shopware\Core\Content\Cms\DataResolver\ResolverContext\ResolverContext;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsFilter;
class SubcategoryListCmsElementResolver extends AbstractCmsElementResolver
{
public function getType(): string
{
return 'my-subcategory-list';
}
public function collect(CmsSlotEntity $slot, ResolverContext $resolverContext): ?CriteriaCollection
{
/** #var CategoryEntity $categoryEntity */
$categoryEntity = $resolverContext->getEntity();
$criteria = new Criteria([$categoryEntity->getId()]);
$criteria->addAssociation('children');
$criteriaCollection = new CriteriaCollection();
$criteriaCollection->add('category_' . $slot->getUniqueIdentifier(), CategoryDefinition::class, $criteria);
return $criteriaCollection;
}
public function enrich(CmsSlotEntity $slot, ResolverContext $resolverContext, ElementDataCollection $result): void
{
/** #var CategoryEntity $categoryEntity */
$categoryEntity = $result->get('category_' . $slot->getUniqueIdentifier())?->getEntities()->first();
$slot->setData($categoryEntity->getChildren()?->sortByPosition()->filter(static function ($child) {
/** #var CategoryEntity $child */
return $child->getActive();
}));
}
}
services.xml
<service id="MyPlugin\DataResolver\SubcategoryListCmsElementResolver">
<tag name="shopware.cms.data_resolver"/>
</service>
You then provide a new template, e.g.
{% block element_my_subcategory_list %}
{% set subcategories = element.data.elements %}
{% set activeCategory = page.header.navigation.active %}
<ul>
{% for category in subcategories %}
<li>do something with your category</li>
{% endfor %}
</ul>
{% endblock %}
You can read more about data resolvers here in the docs: https://developer.shopware.com/docs/guides/plugins/plugins/content/cms/add-data-to-cms-elements#create-a-data-resolver
Not sure if I understood your problem correctly, but let's explain. If you need to fetch array inside array, you can create a loop inside a new loop something like that:
{% for item in category.item %}
{% for subcategory in category.subcategory %}
{{ subcategory.title}}
{% endfor %}
{% endfor %}

Variable Variable in Twig

I have the following variables in twig:( I can see them with kint)
data.po_user_setting.us_highlight_color1 = '#008080'
data.po_user_setting.us_highlight_color2 = '#00FFFF'
data.po_user_setting.us_highlight_color3 = '#FFFF00'
data.po_user_setting.us_highlight_color4 = '#FF0000'
data.po_user_setting.us_highlight_color5 = '#FF00FF'
and
verse.po_verse_highlight.hl_rating = Returns [1-5]
How can I show the dynamic variable like this? Neither of these lines work:
{{_context['data.po_user_setting.us_highlight_color' ~ verse.po_verse_highlight.hl_rating]}}
{{attribute(_context, 'data.po_user_setting.us_highlight_color' ~ verse.po_verse_highlight.hl_rating)}}
The problem you are facing is the variable you want to access is actually an array.
With your current code, twig is looking for a variable called e.g., data.po_user_setting.us_highlight_color.foo, translated to $_context['data.po_user_setting.us_highlight_color.foo']
To actually access the variable you want, you would need to treat the variable like an array:
{{ {{ _context['data']['po_user_setting']['us_highlight_color'~ verse.po_verse_highlight.hl_rating] }} }}
This is quite long to type every time, so to reduce the typing you could use a macro or extend twig
macro
{% macro get_array_value(context, key) %}
{% set value = null %}
{% for key in key|split('.') %}
{% set value = loop.first ? context[key]|default : value[key]|default %}
{% endfor %}
{{ value }}
{% endmacro %}
As macro's don't have access to the special variable _context you would to pass this every time when you want to call the macro, e.g.
{% import _self as macros %}
{{ macros.get_array_value(_context, 'data.po_user_setting.us_highlight_color' ~ verse.po_verse_highlight.hl_rating) }}
extending twig
<?php
$twig->addFunction(new \Twig\TwigFunction('get_array_value', function ($context, $variable) {
$keys = explode('.', $variable);
if (empty($keys)) return;
$value = $context[array_shift($keys)] ?? [];
foreach($keys as $key) {
$value = $value[$key] ?? [];
}
return !empty($value) ? $value : null;
}, ['needs_context' => true,]));
Then you can call this function like the following in twig
{{ get_array_value('data.po_user_setting.us_highlight_color' ~ verse.po_verse_highlight.hl_rating) }}

Twig include with rerender

I want to include some template and append on button click (by js), and I need each included template to have a unique id.
Here is the logic where I appending the templates:
<button type="submit" id="addTranslationFields">Add translations</button>
// and js
$('#addTranslationFields').on('click', function (event) {
event.preventDefault();
event.stopPropagation();
let part = `{% include 'translationPart.twig' with {'languages': languages,}%}`;
$('.table tbody').append(part);
});
Here is the how I generate uuid in the translationPart.twig
{% set uuid = uuid() %}
{{ uuid }}
The issue is that UUID is the same for all of the created templates. I understand why it's happening, it's b-z twig generated server-side and at the moment of generation it sees only one include. But is there some option to rerender included template for each new included copy? Or maybe some other way to set different UUIDs for each of the included templates.
Updated
uuid() is a custom twig function
$twig->addFunction(
new TwigFunction(
'uuid',
static function(): string {
return Uuid::uuid4()->toString();
}
)
);
You can achieve this via ajax calls, or string replacement.
String replacement
Twig template (translationPart.twig)
{% set uuid = '#_SOME_STRING_TO_REPLACE_#' %}
{{ uuid }}
Javascript
<button type="submit" id="addTranslationFields">Add translations</button>
// and js
$('#addTranslationFields').on('click', function (event) {
event.preventDefault();
event.stopPropagation();
let generatedUuid = generateUuidByJavascript();
let part = `{% include 'translationPart.twig' with {'languages': languages,}%}`.replace('#_SOME_STRING_TO_REPLACE_#', generatedUuid);
$('.table tbody').append(part);
});
Uuidjs can be used for generating uuid.

How can I minify HTML with Twig?

I'm using Twig and I'd like to be able to minify the HTML output. How do I do this? I tried {% spaceless %}, but that requires adding that to all my templates. Can I add minification within the Twig engine?
This may help you little.
use html-compress-twig you can compress html,css,js in one package.use composer to install composer require nochso/html-compress-twig and you have to add Extension with twig by using this code.
$app->extend('twig_theme', function($twig_theme, $ojt) {
$twig_theme->addExtension(new nochso\HtmlCompressTwig\Extension());
return $ojt_theme;});
finally go to your template file add this code.
{% htmlcompress %} ....your coding... {% endhtmlcompress %}
{{ htmlcompress('<ul> <li>') }}
{{ '<ul> <li>'|htmlcompress }}
For example you have the BaseController in your src/Controller directory.
You should create BaseController
Extends it from Controller
Override render method of the Controller class
And use this method in every controller
class BaseController extends Controller {
protected function render($view, array $parameters = array(), Response $response = null)
{
if ($this->container->has('templating')) {
$content = $this->container->get('templating')->render($view, $parameters);
} elseif ($this->container->has('twig')) {
$content = $this->container->get('twig')->render($view, $parameters);
} else {
throw new \LogicException('You can not use the "render" method if the Templating Component or the Twig Bundle are not available. Try running "composer require symfony/twig-bundle".');
}
if (null === $response) {
$response = new Response();
}
$content = preg_replace(array('/<!--(.*)-->/Uis',"/[[:blank:]]+/"),array('',' '),str_replace(array("\n","\r","\t"),'',$content));
$response->setContent($content);
return $response;
}
}
You also can extends BaseController in others controllers.
Use a Listener on kernel.response event to automatize the process:
In config/services.yaml:
services:
// ...
app.listener.compression:
class: App\Event\CompressionSubscriber
arguments:
tags:
- { name: kernel.event_subscriber }
In src/Event/CompressionSubscriber.php:
<?php
namespace App\Event;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\HttpKernel\HttpKernelInterface;
class CompressionSubscriber implements EventSubscriberInterface
{
public static function getSubscribedEvents()
{
return [
KernelEvents::RESPONSE => ['onKernelResponse', -256]
];
}
public function onKernelResponse($event)
{
if ($event->getRequestType() != HttpKernelInterface::MAIN_REQUEST) {
return;
}
$response = $event->getResponse();
$content = preg_replace(
['/<!--(.*)-->/Uis',"/[[:blank:]]+/"],
['',' '],
str_replace(["\n","\r","\t"], '', $response->getContent())
);
$response->setContent($content);
}
}
Based on this post
Use
{% spaceless %}
YOUR WHOLE PAGE GOES HERE HTML, TWIG, JS EVERYTHING...
{% endspaceless %}
It can be that your twig version does not recognize the tags, just update the latest version of twig.
This will minify the output html generated and page load will go up because it only loads the compiled version of html.
While you can still view the code in a readable situation.

Resources