<?php
namespace App\Twig;
use App\Entity\HtmlBlocks;
use App\Entity\Locales;
use App\Entity\Setting;
use App\Service\SimpleCache;
use Doctrine\Persistence\ManagerRegistry;
use Twig\Environment;
use Twig\Extension\AbstractExtension;
use Twig\TwigFilter;
use Twig\TwigFunction;
class TwigExtension extends AbstractExtension
{
private ?\Symfony\Component\DependencyInjection\ContainerInterface $container = null;
public function __construct(private readonly \App\Service\ImageCacheService $imageCacheService, private readonly \Symfony\Component\HttpKernel\KernelInterface $kernel, private readonly \Symfony\Component\Routing\RouterInterface $router, private readonly ManagerRegistry $doctrine, private readonly \Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface $authorizationChecker, private readonly \Symfony\Component\HttpFoundation\RequestStack $requestStack, private readonly \App\Service\ServiceController $serviceController, private readonly SimpleCache $cache, private $projectRoot)
{
$this->container = $this->kernel->getContainer();
}
public function getName()
{
return 'twig_extension';
}
#[\Override]
public function getFilters()
{
return [
new TwigFilter('cacheBust', fn ($asset) => $this->cacheBust($asset)),
];
}
#[\Override]
public function getFunctions()
{
return [
new TwigFunction('renderComponents', fn ($positionId, $pageComponents) => $this->renderComponents($positionId, $pageComponents)),
new TwigFunction('removeBracesFromSlug', fn ($string) => $this->removeBracesFromSlug($string)),
new TwigFunction('generatePath', fn ($request, $pageID, $parametersArray = []) => $this->generatePath($request, $pageID, $parametersArray)),
new TwigFunction('imageCache', fn ($pathtofile, $filter, $width = 0, $height = 0, $background = 'transparent') => $this->imageCache($pathtofile, $filter, $width, $height, $background)),
new TwigFunction('fetchLocales', fn () => $this->fetchLocales()),
new TwigFunction('breadcrumbs', function ($pageEntity = null, $currentUrl = null) {
$this->breadcrumbs($pageEntity, $currentUrl);
}),
new TwigFunction('renderHtmlBlock', [$this, 'renderHtmlBlock'], ['needs_environment' => true]),
new TwigFunction('allowInlineEditor', fn ($entity, $field) => $this->allowInlineEditor($entity, $field)),
new TwigFunction('showAdminControlLinks', fn ($entity, $route) => $this->showAdminControlLinks($entity, $route)),
new TwigFunction('moneyFormat', fn ($number) => $this->moneyFormat($number)),
new TwigFunction('domCheckIgnore', fn ($value) => $this->domCheckIgnore($value)),
new TwigFunction('componentEntities', fn ($pageComponents, $entityName = null, $field = null) => $this->componentEntities($pageComponents, $entityName, $field)),
new TwigFunction('componentEntity', fn ($pageComponents, $field = null) => $this->componentEntity($pageComponents, $field)),
new TwigFunction('replaceIfComponentDataExists', fn ($pageComponents, $field = null, $fallback = null) => $this->replaceIfComponentDataExists($pageComponents, $field, $fallback)),
new TwigFunction('forceRenderHtmlBlock', [$this, 'forceRenderHtmlBlock'], ['needs_environment' => true]),
new TwigFunction('renderSetting', fn ($id, $field) => $this->renderSetting($id, $field), ['is_safe' => ['html']]),
];
}
public function renderSetting($id, $field)
{
/** @var array $setting */
$setting = $this->doctrine->getRepository(Setting::class)->findActiveSetting($id);
if (! $setting) {
return '';
}
if (! array_key_exists($field, $setting)) {
return '';
}
return nl2br($setting[$field]);
}
// fetch field from selected entity from a page component - use if you have multiple components on the same page
public function componentEntities($pageComponents, $entityName = null, $field = null)
{
$component = [];
$checkfield = substr((string) $field, 0, 3);
if ('get' != $checkfield) {
$field = 'get'.ucwords((string) $field);
}
if (isset($component['urlKey'])) {
foreach ($pageComponents as $component) {
if (strtolower((string) $component['urlKey']) === strtolower((string) $entityName) && method_exists($component['entity'], $field)) {
return call_user_func([$component['entity'], $field]);
}
}
}
}
// fetch field from url component - only if using a url based component
public function componentEntity($pageComponents, $field = null)
{
$checkfield = substr((string) $field, 0, 3);
if ('get' != $checkfield) {
$field = 'get'.ucwords((string) $field);
}
// ADDED AS VAR WAS MISSING _ CW
$entityName = '';
foreach ($pageComponents as $component) {
if (isset($component['urlKey']) && strtolower((string) $component['urlKey']) === strtolower($entityName) && method_exists($component['entity'], $field)) {
return call_user_func([$component['entity'], $field]);
}
}
}
// Simlar to the above 'componentEntity' function except you can use a fallback string
// This was intented to be used as an alternative to an if statement
public function replaceIfComponentDataExists($pageComponents, $field = null, $fallback = null)
{
$data = null;
$checkfield = substr((string) $field, 0, 3);
if ('get' != $checkfield) {
$field = 'get'.ucwords((string) $field);
}
foreach ($pageComponents as $component) {
if ((isset($component['urlKey']) && null != $component['urlKey']) && null != $component['data'] && method_exists($component['entity'], $field)) {
$data = call_user_func([$component['entity'], $field]);
}
}
if (null == $data) {
return $fallback;
}
return $data;
}
public function moneyFormat($number)
{
return number_format($number, 2, ',', '.');
}
// fix for the domcrawler (which gathers component positions on the add/edit page controller )
// used to ignore app.request or app.user twig functions - wasn't an issue on my testing machine but did effect TREV
public function domCheckIgnore($value)
{
if (is_array($value)) {
return null;
}
return $value;
}
// fetch image or generate new depending on parameter provided - this function may get overriden by the App/Twig/AdminTwigExtension
public function imageCache($pathtofile, $filter, $width = 0, $height = 0, $background = 'transparent')
{
echo $this->imageCacheService->imageCache($pathtofile, $filter, $width, $height, $background);
}
// generate page url which translates to the current locale
// get all slugs stored in the array cache to generate the path
// example : <a href="{{generatePath( app.request, 7, {'blog_category': post.category.slug, 'blog_post_slug': post.slug} )}}">
public function generatePath($request, $pageID, $parametersArray = [])
{
$locale = $request->getLocale();
$session = $request->getSession();
$localePages = $session->get('localePages');
$not_found = [];
$slugCache = $this->cache->get('slugCache');
if (null == $slugCache) { // prevents error in page admin ( dom crawer issue with app.request )
return false;
}
foreach (unserialize($slugCache) as $page) {
if ($page['id'] == $pageID) {
$finalUrl = $page['slug'];
$confirmedPagePieces = explode('/', (string) $page['slug']);
foreach ($parametersArray as $key => $parameter) {
$slugCheck = str_replace(' ', '', (string) $key);
$slugKey = array_search('{'.$slugCheck.'}', $confirmedPagePieces);
if (! is_numeric($slugKey)) {
$not_found[$key] = $parameter;
} else {
$finalUrl = str_replace('{'.$slugCheck.'}', $parameter, (string) $finalUrl);
}
}
$getparams = '';
if (count($not_found) > 0) {
$getparams = '?';
foreach ($not_found as $extraParam => $extraParamValue) {
$getparams .= $extraParam.'='.$extraParamValue.'&';
}
$getparams = rtrim($getparams, '&');
}
return '/'.str_replace('//', '/', $finalUrl.$getparams);
}
}
$getparams = '';
if (count($not_found) > 0) {
$getparams = '?path=notfound&';
foreach ($not_found as $extraParam => $extraParamValue) {
$getparams .= $extraParam.'='.$extraParamValue.'&';
}
$getparams = rtrim($getparams, '&');
}
return '#'.$getparams;
}
// used to tidy page {slugs}
public function removeBracesFromSlug($string)
{
return preg_replace('#{[\s\S]+?}#', '', (string) $string);
}
// Inserts the relevant component into the page template
// Also assists the new/edit page component selector (domcrawler picks up on the domcheck attribute)
public function renderComponents($positionId, $pageComponents)
{
if ($pageComponents) {
if ('domcheck' == $pageComponents[0]['position']) {
echo "<div data-cms='domcheck'>".$positionId.'</div>';
}
foreach ($pageComponents as $component) {
if ($component['position'] == $positionId && array_key_exists('data', $component)) {
return $component['data'];
}
}
}
}
// Inserts the relevant htmlblock into the page template
// Also assists the new/edit page component selector (domcrawler picks up on the domcheck attribute)
public function renderHtmlBlock(Environment $environment, $positionId, $pageHtmlblocks)
{
if ($pageHtmlblocks) {
if ('domcheck' == $pageHtmlblocks[0]['position']) {
echo "<div data-cms='domcheckhtml'>".$positionId.'</div>';
}
foreach ($pageHtmlblocks as $block) {
if ($block['position'] == $positionId && array_key_exists('data', $block)) {
if ($this->authorizationChecker->isGranted('ROLE_PAGE_EDITOR')) {
return $this->getInlineEditorHTML(HtmlBlocks::class, 'html', $block['data'], $block['blockId'], 'HtmlBlock');
}
return $environment->render('@theme/common/htmlblock.html.twig', ['block' => $block]);
}
}
}
}
// Inserts the relevant htmlblock into the page template
// This function is not used tied to the CMS - renders same block on every page
public function forceRenderHtmlBlock(Environment $environment, $identifier)
{
if ($identifier) {
$block = $this->doctrine->getRepository(HtmlBlocks::class)->findOneBy(['title' => $identifier, 'deleted' => false, 'active' => true]);
if (null !== $block) {
if ($this->authorizationChecker->isGranted('ROLE_PAGE_EDITOR')) {
return $this->getInlineEditorHTML(HtmlBlocks::class, 'html', $block->getHtml(), $block->getId(), 'HtmlBlock');
}
$blockForTwig = [
'blockId' => $block->getId(),
'data' => $block->getHtml(),
];
return $environment->render('@theme/common/htmlblock.html.twig', ['block' => $blockForTwig]);
}
}
}
public function allowInlineEditor($entity, $field)
{
$namespaceMeta = $this->serviceController->getBundleNameFromEntity($entity, $field);
$getterMethod = 'get'.ucwords((string) $field);
$editText = $entity->{$getterMethod}();
if ('' == $editText) {
return null;
}
$request = $this->requestStack->getCurrentRequest();
// $request = $this->container->get('request');
if ($request->query->has('preview')) {
return $editText;
}
if ($this->authorizationChecker->isGranted('ROLE_PAGE_EDITOR')) {
return $this->getInlineEditorHTML($namespaceMeta['full'], $field, $editText, $entity->getId(), $namespaceMeta['short'], $namespaceMeta['fieldmeta']);
}
return $editText;
}
public function showAdminControlLinks($entity, $route)
{
$namespaceMeta = $this->serviceController->getBundleNameFromEntity($entity);
$url = $this->router->generate($route, ['id' => $entity->getId()]);
if ($this->authorizationChecker->isGranted('ROLE_ADMIN')) {
$buttons = "<div class='adminControlButtons'>";
$buttons .= " <div class='inlineEditorToolboxContainer'>Admin Control (".$namespaceMeta['short'].')</div>';
$buttons .= " <div class='inlineEditorToolboxLink'><a href='".$url."' data-toggle='tooltip' title='View/Edit' ><span class='glyphicon glyphicon-pencil'></span> View/Edit</a></div>";
$buttons .= '</div>';
return $buttons;
}
}
public function getInlineEditorHTML($namespace, $field, $content, $id, $entityname = null, $fieldmeta = null)
{
// show inline editor
$request = $this->requestStack->getCurrentRequest();
$locale = $request->getLocale();
$showFullEditor = 1;
if (null != $fieldmeta && 'string' == $fieldmeta['type']) {
$showFullEditor = 0;
}
// Redactor required uniqueIDs - classes conflicted if multiple editors were used
$uniqueID = substr(md5(random_int(1, 9999)), 0, 7);
$editor = "<div class='inlineEditorContainer'>";
$editor .= " <div id='inlineEditor-message-".$uniqueID."' class='inlineEditorToolboxContainer'>Editable (".$entityname.':'.$field.')</div>';
$editor .= " <div class='inlineEditorToolboxSave'><a data-toggle='tooltip' title='Save Text' id='btn-save-".$uniqueID."' style='display:none'><span class='glyphicon glyphicon-floppy-disk'></span> Save</a></div>";
// $editor .= " <div class='inlineEditorToolboxClose'><a data-toggle='tooltip' title='Close Editor' id='btn-cancel-".$uniqueID."' style='display:none'><span class='glyphicon glyphicon-remove-sign'></span></a></div>";
$editor .= " <div class='inlineEditor' data-fulleditor='".$showFullEditor."' id='".$uniqueID."' data-entitynamespace='".$namespace."' data-entityfield='".$field."' data-id='".$id."' data-locale='".$locale."' >";
if (0 == $showFullEditor) {
$editor .= '<p>';
}
$editor .= $content;
if (0 == $showFullEditor) {
$editor .= '</p>';
}
$editor .= ' </div>';
// if($showFullEditor ==1){
// $editor .= " <textarea class='inlineEditor' data-fulleditor='".$showFullEditor."' id='".$uniqueID."' data-entitynamespace='".$namespace."' data-entityfield='".$field."' data-id='".$id."' data-locale='".$locale."' >";
// $editor .= $content;
// $editor .= " </textarea>";
// }else{
// $editor .= " <input type='text' class='inlineEditor' data-fulleditor='".$showFullEditor."' value='".$content."' id='".$uniqueID."' data-entitynamespace='".$namespace."' data-entityfield='".$field."' data-id='".$id."' data-locale='".$locale."' />";
// }
$editor .= '</div>';
return $editor;
}
// simple function to fetch all locales
// done this way to ensure locale switch buttons work on non cms pages
public function fetchLocales()
{
return $this->doctrine->getRepository(Locales::class)->findBy(['active' => true]);
}
public function breadcrumbs($pageEntity = null, $currentUrl = null)
{
// // this function generates a full url category path
// $currentUrl = $this->container->get('request')->getUri();
if (null != $pageEntity && null != $currentUrl) {
$exploded = explode('/', (string) $currentUrl);
unset($exploded[0]);
$exploded = array_values($exploded);
$structure = [];
$explodedCount = count($exploded);
for ($i = 0; $i < $explodedCount; $i++) {
if (array_key_exists($i - 1, $structure)) {
$structure[$i] = $structure[$i - 1]['url'].'/'.$exploded[$i];
} else {
$structure[$i] = '/'.$exploded[$i];
}
$url = array_key_exists($i - 1, $structure) ? $structure[$i - 1]['url'].'/'.$exploded[$i] : '/'.$exploded[$i];
$structure[$i] = ['url' => $url, 'title' => $exploded[$i]];
}
// print_r($structure);
$seperater = ' > ';
$html = '<div class="breadcrumb">';
$html .= '<span><a href="/">Home</a>'.$seperater.'</span>';
$count = 0;
foreach ($structure as $item) {
$count++;
if (count($structure) == $count) {
$seperater = '';
}
$html .= '<span><a href="'.$item['url'].'">'.str_replace('-', ' ', ucfirst($item['title'])).'</a>'.$seperater.'</span>';
}
$html .= '</div>';
echo $html;
}
}
// simple function to pluralise text string (adds 's' if array count >1 )
public function pluralize($text, $array, $plural_version = null)
{
return (is_countable($array) ? count($array) : 0) > 1 ? ($plural_version ?: $text.'s') : $text;
}
// simple word limiter function
public function wordLimiter($str, $limit = 30)
{
$words = explode(' ', strip_tags((string) $str));
if ($words > $limit) {
return implode(' ', array_splice($words, 0, $limit)).'...';
}
return $str;
}
/**
* Cache bust specified asset.
*/
public function cacheBust(mixed $asset)
{
$asset = '/'.ltrim((string) $asset, '/');
$assetPath = sprintf('%s/../public/%s', $this->projectRoot, $asset);
// If we are assuming a CSS or JS file exists when it doesn't,
// we probably need to know about it.
if (! file_exists($assetPath)) {
throw new \RuntimeException(sprintf('Asset: %s is missing!', $asset));
}
$modified = filemtime($assetPath);
return $asset.sprintf('?version=%d', $modified);
}
}