Get subcategories with twig on listing page - twig

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 %}

Related

Shopware 6 - Find seo Url of variant on product detail page

I am trying to lookup the seoUrl of variants displayed on a product detail page - in the configurator.html.twig file. I have the option Id and have tried passing it as productId for the seoUrl function - but it doesn't return the correct seoUrl.
Searching for a solution, I found this question: Show all variations on the product detail page in Shopware 6 - which also lacks an answer.
But it hinted that you should add the data using a Subscriber - is that really necessary?
You may decorate the ProductDetailRoute, fetch the parent and with its children association and iterate them in the storefront template.
<service id="MyPlugin\Core\Content\Product\SalesChannel\Detail\ProductDetailRouteDecorator" public="true" decorates="Shopware\Core\Content\Product\SalesChannel\Detail\ProductDetailRoute">
<argument type="service" id="MyPlugin\Core\Content\Product\SalesChannel\Detail\ProductDetailRouteDecorator.inner"/>
<argument type="service" id="sales_channel.product.repository"/>
</service>
class ProductDetailRouteDecorator extends AbstractProductDetailRoute
{
private SalesChannelRepositoryInterface $productRepository;
private AbstractProductDetailRoute $decorated;
public function __construct(
AbstractProductDetailRoute $decorated,
SalesChannelRepositoryInterface $productRepository
) {
$this->decorated = $decorated;
$this->productRepository = $productRepository;
}
public function getDecorated(): AbstractProductDetailRoute
{
return $this->decorated;
}
public function load(string $productId, Request $request, SalesChannelContext $context, Criteria $criteria): ProductDetailRouteResponse
{
$response = $this->getDecorated()->load($productId, $request, $context, $criteria);
$product = $response->getProduct();
if (!$product->getParentId()) {
return $response;
}
$criteria = new Criteria([$product->getParentId()]);
$criteria->addAssociation('children');
$parent = $this->productRepository->search($criteria, $context)->first();
$product->setParent($parent);
return new ProductDetailRouteResponse($product, $response->getConfigurator());
}
}
{% if page.product.parent.children is defined %}
{% for child in page.product.parent.children %}
{{ seoUrl('frontend.detail.page', { productId: child.id }) }}
{% endfor %}
<br>
{% endif %}
Sample output:
http://localhost/Intelligent-Marble-Ultra-Beef/0491895660e94e32938022263595f861
http://localhost/Intelligent-Marble-Ultra-Beef/7c9f91d9051e40e0ba13d0e885e98d83
http://localhost/Intelligent-Marble-Ultra-Beef/f25c641abee446df82e1227cf200186c
Variation on this solution
A little more complex but with a lesser performance impact:
$criteria = new Criteria();
$criteria->addFilter(new EqualsFilter('parentId', $product->getParentId()));
$ids = $this->productRepository->searchIds($criteria, $context)->getIds();
$product->addExtension('childrenIds', new ArrayStruct($ids));
{% if page.product.extensions.childrenIds is defined %}
{% for childId in page.product.extensions.childrenIds.all() %}
{{ seoUrl('frontend.detail.page', { productId: childId }) }}
<br>
{% endfor %}
{% endif %}
Yes, the best solution would be to fetch all variants based on the parentId in a subscriber.
This is necessary because you want to have more data then just the productId. You should also consider this information as necessary:
minPurchase
calculatedMaxPurchase
purchaseSteps
And optional:
packUnit
packUnitPlural
Without the necessary product information the end user will possibly encounter errors when adding a product with a invalid quantity to the cart.

How to integrate Symfony 6 form with proper style into EasyAdmin 4?

I have a logic that wouldn't be easy to implement in EasyAdmin so I decided that I implement it in Symfony 6 then integrate it into EA. The integration worked like a charm but I can't figure out which form_theme should I use to look like the other EA forms.
I have created a form type which doesn't belong to any entity since multiple entities will be generated after the validation based on the input data.
This is the controller
<?php
namespace App\Controller\Admin;
use App\Form\Type\NewTextType;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
class TextController extends AbstractController
{
#[Route('/admin/text/new', name: 'new_text')]
public function new(): Response
{
$defaultData = [];
$form = $this->createForm(NewTextType::class, $defaultData);
if ($form->isSubmitted() && $form->isValid()) {
$data = $form->getData();
// process the data and persist them as different entities
// redirect to the empty form and do it again
}
return $this->renderForm('admin/text/new.html.twig', [
'form' => $form,
]);
}
}
and the template
{% extends '#EasyAdmin/page/content.html.twig' %}
{% form_theme form 'foundation_5_layout.html.twig' %}
{% block content_title %}
<h1 class="title">Add new Text</h1>
{% endblock %}
{% block main %}
{{ form(form) }}
{% endblock %}
Unfortunately it looks like a crap.
It looks better if I replace foundation_5_layout with {% form_theme form 'bootstrap_5_layout.html.twig' %} but then the appearance setting is not applied even though it is presented in the BODY tag:
data-ea-dark-scheme-is-enabled="true"
What do I miss here?
I use Symfony 6.1.2 and EasyAdmin 4.3.2
Finally I have found the right template which supports the Light/Dark appearance:
{% form_theme form '#EasyAdmin/crud/form_theme.html.twig' %}
The whole template
{% extends '#EasyAdmin/page/content.html.twig' %}
{% form_theme form '#EasyAdmin/crud/form_theme.html.twig' %}
{% block content_title %}
<h1 class="title">Add new Text</h1>
{% endblock %}
{% block main %}
{{ form(form) }}
{% endblock %}

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

Grav - Pass JSON data from Plugin to Twig

I want to retrieve a JSON from a webservice and incorporate it into a Twig template.
I went through the docs and I found that I could use this option.
I have followed the steps from the doc and I have prepared this plugin:
/var/www/html/grav/user/plugins/category# ls
category.php category.yaml twig
/var/www/html/grav/user/plugins/category# cat category.yaml
enabled: true
/var/www/html/grav/user/plugins/category# cat category.php
<?php
namespace Grav\Plugin;
use \Grav\Common\Plugin;
class CategoryPlugin extends Plugin
{
public static function getSubscribedEvents()
{
return [
'onTwigExtensions' => ['onTwigExtensions', 0]
];
}
public function onTwigExtensions()
{
require_once(__DIR__ . '/twig/CategoryTwigExtension.php');
$this->grav['twig']->twig->addExtension(new CategoryTwigExtension());
}
}
/var/www/html/grav/user/plugins/category# cat twig/CategoryTwigExtension.php
<?php
namespace Grav\Plugin;
class CategoryTwigExtension extends \Twig_Extension
{
public function getName()
{
return 'CategoryTwigExtension';
}
public function getFunctions()
{
return [
new \Twig_SimpleFunction('get_child_category', [$this, 'getChildCategoryFunction'])
];
}
public function getChildCategoryFunction()
{
$json = file_get_contents('http://localhost:8888/get_child_category/2/es_ES');
$obj = json_decode($json);
return $json;
}
}
I then incorporate the following function invocation in the Twig template:
{{ get_child_category() }}
But:
I can get $json string, but how can I pass the whole JSON data and retrieve individually the fields?
In my case if I use:
<span>{{ get_child_category() }}</span>
in Twig I get the following string:
[{"id": 11, "name": "Racoons"}, {"id": 10, "name": "Cats"}]
How would I access individual records in Twig, including iteration over the JSON array and individual field extraction (id, name) for each record?
Your function is returning an array. You need to iterate through it. Here is an example from the Grav docs.
<ul>
{% for cookie in cookies %}
<li>{{ cookie.flavor }}</li>
{% endfor %}
</ul>
A simple list of names from your example is a simple edit.
<ul>
{% for child in get_child_category() %}
<li>{{ child.name }}</li>
{% endfor %}
</ul>

Twig: while-workaround

I've already a solution, but just for JavaScript. Unfortunately while-loops do not exist in Twig.
My Twig-target in JavaScript:
var x = 10; // this is an unknown number
var result = x;
while (100 % result !== 0) {
result++;
}
console.log(result);
Any ideas how I do this in Twig?
What's my target: (not important if you already understood)
I want to get the first number after my unknown number, that satisfy the following condition:
100 divided by (the first number) equals a whole number as result.
EDIT: I have no access to PHP nor Twig-core.
You can make a Twig extension like:
namespace Acme\DemoBundle\Twig\Extension;
class NumberExtension extends \Twig_Extension
{
public function nextNumber($x)
{
$result = $x;
while (100 % $result !== 0) {
$result++;
}
return $result;
}
public function getFunctions()
{
return array(
'nextNumber' => new \Twig_Function_Method($this, 'nextNumber'),
);
}
/**
* Returns the name of the extension.
*
* #return string The extension name
*/
public function getName()
{
return 'demo_number';
}
}
And define it in the service.xml of the bundle:
<service id="twig.extension.acme.demo" class="Acme\DemoBundle\Twig\Extension\NumberExtension" >
<tag name="twig.extension" />
</service>
Then use it in the template:
{{ nextNumber(10) }}
UPDATE
A (not so great) approach but that possibly satisfy your needed is to do something like this:
{% set number = 10 %}
{% set max = number+10000 %} {# if you can define a limit #}
{% set result = -1 %}
{% for i in number..max %}
{% if 100 % i == 0 and result < 0 %} {# the exit condition #}
{% set result = i %}
{% endif %}
{% endfor %}
<h1>{{ result }}</h1>
Hope this help
In my case - I had to output an object with similar subobjects - including a templete with predefined values and setting up a normal if-condition worked.
See http://twig.sensiolabs.org/doc/tags/include.html for more infomration.

Resources