<?php
declare(strict_types=1);
namespace App\Module\Cms\Twig\Functions;
use App\Module\Catalog\Provider\CategoryTreeProvider;
use App\Module\Cms\Model\Stories;
use App\Module\Cms\Provider\CmsContentProvider;
use App\Module\Cms\RichText\InternalLinkAwareRichTextSchema;
use InvalidArgumentException;
use Psr\Log\LoggerInterface;
use Storyblok\RichtextRender\Resolver;
use Twig\Environment;
use Twig\Error\LoaderError;
use Twig\Error\RuntimeError;
use Twig\Error\SyntaxError;
use Twig\Extension\AbstractExtension;
use Twig\TwigFunction;
use function sprintf;
/**
* Render extension for storyblok
* Example: {{ render_storyblok(component, {'class': 'foo'}) }}
* component are the Storyblock array and the array with class is optional
*
* @package App\Module\Cms\Twig\Functions
*/
class StoryblokExtension extends AbstractExtension
{
/**
* @var string
*/
private const COMPONENT_PATH_PATTERN = 'components/%s/%s/%s.html.twig';
private CmsContentProvider $provider;
private CategoryTreeProvider $categoryTreeProvider;
private InternalLinkAwareRichTextSchema $richTextSchema;
private LoggerInterface $logger;
/**
* StoryblokExtension constructor.
*
* @param CmsContentProvider $provider
* @param InternalLinkAwareRichTextSchema $internalLinkAwareSchema
* @param CategoryTreeProvider $categoryTreeProvider
*/
public function __construct(
LoggerInterface $logger,
CmsContentProvider $provider,
InternalLinkAwareRichTextSchema $internalLinkAwareSchema,
CategoryTreeProvider $categoryTreeProvider
) {
$this->provider = $provider;
$this->richTextSchema = $internalLinkAwareSchema;
$this->categoryTreeProvider = $categoryTreeProvider;
$this->logger = $logger;
}
/**
* @param Environment $twig
* @param array|null $componentData
* @param array $options
* @return string
* @throws LoaderError
* @throws RuntimeError
* @throws SyntaxError
*/
public function render(Environment $twig, array $componentData = null, array $options = []): string
{
// belongs to the DEN-844 ticket
if (!is_array($componentData)) {
$error = new InvalidArgumentException();
$this->logger->error(
'Invalid Argument exception -> see DEN-844 ticket for more information',
['trace' => $error->getTrace()]
);
return '';
}
if (count($componentData) === 1) {
$componentData = $componentData[0];
}
if (count($options) > 0) {
$componentData = array_merge($componentData, $options);
}
if (isset($componentData['atomicType'], $componentData['component'])) {
$template = sprintf(
static::COMPONENT_PATH_PATTERN,
$componentData['atomicType'],
$componentData['component'],
$componentData['component']
);
if (!$twig->getLoader()->exists($template)) {
$this->logger->error(sprintf('Template "%s" not found', $template));
return '';
}
$result = $twig->render($template, $componentData);
return ($componentData['_editable'] ?? '') . $result;
}
return '';
}
/**
* @param array $markup
* @return string
*/
public function renderRichText(array $markup): string
{
$resolver = new Resolver(
['marks' => $this->richTextSchema->getMarks(), 'nodes' => $this->richTextSchema->getNodes()]
);
return $resolver->render($markup);
}
/**
* @param Stories $stories
* @param string $filter
* @return array
*/
public function getContentsFilterOptions(Stories $stories, string $filter): array
{
$storiesArray = iterator_to_array($stories);
// get 2D array of story->content->article_topics = [article_topicsA, article_topicsB, ...]
$nestedFilterOptions = array_column(array_column($storiesArray, 'content'), $filter);
// merge arrays into a single 1D one
$filterOptions = [];
array_walk_recursive($nestedFilterOptions, function($nestedItem) use (&$filterOptions) { $filterOptions[] = $nestedItem; });
// array_values() to close array index gap
return array_values(array_unique($filterOptions));
}
/**
* @param string $contentId
* @return string
*/
public function getStoryLink(string $contentId): string
{
$content = $this->provider->getContentById($contentId);
return $content !== null ? $content->slug : '';
}
/**
* @return array
*/
public function getCategoryChildrenFromTree(): array
{
$currentCategory = $this->provider->getContentBySlugForCurrentPage();
$categories = [];
if ($currentCategory !== null) {
$categories = $this->categoryTreeProvider->getCategoryFromTreeById($currentCategory->uuid)['children'] ?? [];
}
return $categories;
}
/**
* @param mixed $linkObject, Link in Storyblok format
* @return string
*/
public function getHrefFromStoryblokLink($linkObject): ?string
{
if (is_array($linkObject)) {
$href = "";
if ($linkObject['linktype'] == 'story') {
$href = $this->getStoryLink($linkObject['id']);
if (isset($linkObject['anchor'])) {
$href = $href .'#'.$linkObject['anchor'];
}
} elseif ($linkObject['linktype'] == 'email') {
$href = 'mailto:' . $linkObject['email'];
} elseif (isset($linkObject['cached_url'])) {
$href = $linkObject['cached_url'];
}
return $href;
} else {
return $linkObject;
}
}
/**
* Get twig functions
*
* @return array
*/
public function getFunctions(): array
{
return [
new TwigFunction(
'render_storyblok',
[
$this,
'render',
],
[
'is_safe' => ['html'],
'needs_environment' => true,
]
),
new TwigFunction(
'getContent',
[
$this->provider,
'getContentById',
],
[
'is_safe' => ['html'],
]
),
new TwigFunction(
'getContentByTopicsAndTypes',
[
$this->provider,
'getContentByTopicsAndTypes',
],
[
'is_safe' => ['html'],
]
),
new TwigFunction(
'getContentsFilterOptions',
[
$this,
'getContentsFilterOptions',
],
[
'is_safe' => ['html'],
]
),
new TwigFunction(
'getStoryLink',
[
$this,
'getStoryLink',
],
[
'is_safe' => ['html'],
]
),
new TwigFunction(
'getCategoryChildrenFromTree',
[
$this,
'getCategoryChildrenFromTree',
],
[
'is_safe' => ['html'],
]
),
new TwigFunction(
'renderRichText',
[
$this,
'renderRichText',
],
[
'is_safe' => ['html'],
]
),
new TwigFunction(
'getHrefFromStoryblokLink',
[
$this,
'getHrefFromStoryblokLink',
]
),
];
}
}