Grav - Pass JSON data from Plugin to Twig - 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>

Related

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

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.

Can I extract an associative array into variables?

Lets say I have an associative array like so:
{% set settings = { 'foo':'bar', 'cat':'mouse', 'apple':'banana' } %}
To use this data I would do the following:
{{ settings.foo }}
{{ settings.cat }}
{{ settings.apple }}
However, I wondered if there is a way to extract the keys to variables, and the values to values? Essentially the same as the PHP Extract function. So I can just do this instead:
{{ foo }}
{{ cat }}
{{ apple }}
My very amateurish attempt to do this started out like this:
{% for key,val in settings %}
{% set key = val %}
{% endfor %}
But obviously that doesn't work (or I wouldn't be here). Is there another approach I could take?
Thanks,
Mark
As most things in Twig this can be done by extending Twig
ProjectTwigExtension.php
class ProjectTwigExtension extends Twig_Extension {
public function getFunctions() {
return array(
new Twig_SimpleFunction('extract', array($this, 'extract'), ['needs_context' => true, ]),
);
}
public function extract(&$context, $value) {
foreach($value as $k => $v) $context[$k] = $v;
}
public function getName() {
return 'ProjectTwigExtension';
}
}
Register class in Twig
$twig = new Twig_Environment($loader);
$twig->addExtension(new ProjectTwigExtension());
template.twig
{{ extract({'foo': 'bar', }) }}
{{ foo }} {# output : bar #}
(sidenote) Seems you can't do this by using a closure (see example below) because the compiler of Twig passes the variables in an array, thus creating a copy
With closure
$twig->addFunction(new Twig_SimpleFunction('extract', function (&$context, $value) {
foreach($value as $k => $v) $context[$k] = $v;
}, ['needs_context' => true, ]));
Compiled result
echo twig_escape_filter($this->env, call_user_func_array($this->env->getFunction('extract')->getCallable(), array($context, array("foo" => "bar", "foobar" => "foo"))), "html", null, true);

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.

Only Display a # of Characters in Twig

I am very unfamiliar with twig. Here's what I have:
{% if wp.get_post_meta(post.ID, '_property_website').0 %}
<tr>
<th>{{ wp.__('Website', 'aviators') }}:</th>
<td>{{ wp.get_post_meta(post.ID, '_property_website').0 }}
</td>
</tr>
{% endif %}
I need to restrict this output to 35 characters without killing the link. It needs to still be active but only display 35 characters, plus ideally it would end with... to designate that the url is cut off but that's a bonus. Can anyone help?
I believe http://twig.sensiolabs.org/doc/filters/slice.html is what you are looking for
EDIT
Just found that Twig has an extension called text it includes the wordwrap filter that is exactly what you're looking for
Link: https://github.com/fabpot/Twig-extensions/blob/master/lib/Twig/Extensions/Extension/Text.php
You can make your own Twig Extension. It's very easy.
First you must create the file with the filter code:
<?php
//Acme/AcmeBundle/Twig/AnExtension.php
namespace Acme\AcmeBundle\Twig;
class AnExtension extends \Twig_Extension
{
public function getFilters()
{
return array(
new \Twig_SimpleFilter('cutText', array($this, 'cutTextFilter'))
);
}
public function cutTextFilter($text, $size = 50)
{
if (strlen($text) > $size)
{
return substr($text, 0, $size) . '...';
}
else
{
return $text;
}
}
public function getName()
{
return 'an_extension';
}
}
Then edit the services.yml file from this bundle, located at: /Acme/AcmeBundle/Resources/config/services.yml and add:
services:
acme.twig.an_extension:
class: Acme\AcmeBundle\Twig\AnExtension
tags:
- { name: twig.extension }
and now you can use the filter in your code:
<a href="http://{{ wp.get_post_meta(post.ID, '_property_website').0 }}">
{{ wp.get_post_meta(post.ID, '_property_website').0 | cutText(30) }}
</a>
More info: http://symfony.com/doc/current/cookbook/templating/twig_extension.html

Resources