src/Twig/TwigExtension.php line 152

Open in your IDE?
  1. <?php
  2. namespace App\Twig;
  3. use Twig\TwigFilter;
  4. use Twig\TwigFunction;
  5. use App\Entity\Locales;
  6. use App\Entity\Setting;
  7. use Gregwar\Image\Image;
  8. use App\Entity\HtmlBlocks;
  9. use App\Service\SimpleCache;
  10. use Twig\Extension\AbstractExtension;
  11. use Doctrine\Persistence\ManagerRegistry;
  12. class TwigExtension extends AbstractExtension
  13. {
  14. private ?\Symfony\Component\DependencyInjection\ContainerInterface $container = null;
  15. 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)
  16. {
  17. $this->container = $this->kernel->getContainer();
  18. }
  19. public function getName()
  20. {
  21. return 'twig_extension';
  22. }
  23. #[\Override]
  24. public function getFilters()
  25. {
  26. return [
  27. new TwigFilter('cacheBust', fn ($asset) => $this->cacheBust($asset)),
  28. ];
  29. }
  30. #[\Override]
  31. public function getFunctions()
  32. {
  33. return [
  34. new TwigFunction('renderPcgcComponents', fn ($positionId, $pageComponents) => $this->renderPcgcComponents($positionId, $pageComponents)),
  35. new TwigFunction('removeBracesFromSlug', fn ($string) => $this->removeBracesFromSlug($string)),
  36. new TwigFunction('generatePath', fn ($request, $pageID, $parametersArray = []) => $this->generatePath($request, $pageID, $parametersArray)),
  37. new TwigFunction('imageCache', fn ($pathtofile, $filter, $width = 0, $height = 0, $background = 'transparent') => $this->imageCache($pathtofile, $filter, $width, $height, $background)),
  38. new TwigFunction('fetchLocales', fn () => $this->fetchLocales()),
  39. new TwigFunction('breadcrumbs', function ($pageEntity = null, $currentUrl = null) {
  40. $this->breadcrumbs($pageEntity, $currentUrl);
  41. }),
  42. new TwigFunction('renderPcgcHtmlBlock', fn ($positionId, $pageHtmlblocks) => $this->renderPcgcHtmlBlock($positionId, $pageHtmlblocks)),
  43. new TwigFunction('allowInlineEditor', fn ($entity, $field) => $this->allowInlineEditor($entity, $field)),
  44. new TwigFunction('showAdminControlLinks', fn ($entity, $route) => $this->showAdminControlLinks($entity, $route)),
  45. new TwigFunction('moneyFormat', fn ($number) => $this->moneyFormat($number)),
  46. new TwigFunction('domCheckIgnore', fn ($value) => $this->domCheckIgnore($value)),
  47. new TwigFunction('pcgcComponentEntities', fn ($pageComponents, $entityName = null, $field = null) => $this->pcgcComponentEntities($pageComponents, $entityName, $field)),
  48. new TwigFunction('pcgcComponentEntity', fn ($pageComponents, $field = null) => $this->pcgcComponentEntity($pageComponents, $field)),
  49. new TwigFunction('replaceIfComponentDataExists', fn ($pageComponents, $field = null, $fallback = null) => $this->replaceIfComponentDataExists($pageComponents, $field, $fallback)),
  50. new TwigFunction('forceRenderPcgcHtmlBlock', fn ($identifier) => $this->forceRenderPcgcHtmlBlock($identifier)),
  51. new TwigFunction('forceRenderPcgcHtmlBlockByIdentifier', fn ($identifier) => $this->forceRenderPcgcHtmlBlockByIdentifier($identifier)),
  52. new TwigFunction('renderSetting', fn ($id, $field) => $this->renderSetting($id, $field), ['is_safe' => ['html']]),
  53. ];
  54. }
  55. public function renderSetting($id, $field)
  56. {
  57. /** @var array $setting */
  58. $setting = $this->doctrine->getRepository(Setting::class)->findActiveSetting($id);
  59. if (! $setting) {
  60. return '';
  61. }
  62. if (! array_key_exists($field, $setting)) {
  63. return '';
  64. }
  65. return nl2br($setting[$field]);
  66. }
  67. // fetch field from selected entity from a page component - use if you have multiple components on the same page
  68. public function pcgcComponentEntities($pageComponents, $entityName = null, $field = null)
  69. {
  70. $component = [];
  71. $checkfield = substr((string) $field, 0, 3);
  72. if ('get' != $checkfield) {
  73. $field = 'get'.ucwords((string) $field);
  74. }
  75. if (isset($component['urlKey'])) {
  76. foreach ($pageComponents as $component) {
  77. if (strtolower((string) $component['urlKey']) === strtolower((string) $entityName) && method_exists($component['entity'], $field)) {
  78. return call_user_func([$component['entity'], $field]);
  79. }
  80. }
  81. }
  82. }
  83. // fetch field from url component - only if using a url based component
  84. public function pcgcComponentEntity($pageComponents, $field = null)
  85. {
  86. $checkfield = substr((string) $field, 0, 3);
  87. if ('get' != $checkfield) {
  88. $field = 'get'.ucwords((string) $field);
  89. }
  90. // ADDED AS VAR WAS MISSING _ CW
  91. $entityName = '';
  92. foreach ($pageComponents as $component) {
  93. if (isset($component['urlKey']) && strtolower((string) $component['urlKey']) === strtolower($entityName) && method_exists($component['entity'], $field)) {
  94. return call_user_func([$component['entity'], $field]);
  95. }
  96. }
  97. }
  98. // Simlar to the above 'pcgcComponentEntity' function except you can use a fallback string
  99. // This was intented to be used as an alternative to an if statement
  100. public function replaceIfComponentDataExists($pageComponents, $field = null, $fallback = null)
  101. {
  102. $data = null;
  103. $checkfield = substr((string) $field, 0, 3);
  104. if ('get' != $checkfield) {
  105. $field = 'get'.ucwords((string) $field);
  106. }
  107. foreach ($pageComponents as $component) {
  108. if ((isset($component['urlKey']) && null != $component['urlKey']) && null != $component['data'] && method_exists($component['entity'], $field)) {
  109. $data = call_user_func([$component['entity'], $field]);
  110. }
  111. }
  112. if (null == $data) {
  113. return $fallback;
  114. }
  115. return $data;
  116. }
  117. public function moneyFormat($number)
  118. {
  119. return number_format($number, 2, ',', '.');
  120. }
  121. // fix for the domcrawler (which gathers component positions on the add/edit page controller )
  122. // used to ignore app.request or app.user twig functions - wasn't an issue on my testing machine but did effect TREV
  123. public function domCheckIgnore($value)
  124. {
  125. if (is_array($value)) {
  126. return null;
  127. }
  128. return $value;
  129. }
  130. // fetch image or generate new depending on parameter provided - this function may get overriden by the App/Twig/AdminTwigExtension
  131. public function imageCache($pathtofile, $filter, $width = 0, $height = 0, $background = 'transparent')
  132. {
  133. echo $this->imageCacheService->imageCache($pathtofile, $filter, $width, $height, $background);
  134. }
  135. // generate page url which translates to the current locale
  136. // get all slugs stored in the array cache to generate the path
  137. // example : <a href="{{generatePath( app.request, 7, {'blog_category': post.category.slug, 'blog_post_slug': post.slug} )}}">
  138. public function generatePath($request, $pageID, $parametersArray = [])
  139. {
  140. $locale = $request->getLocale();
  141. $session = $request->getSession();
  142. $localePages = $session->get('localePages');
  143. $not_found = [];
  144. $slugCache = $this->cache->get('slugCache');
  145. if (null == $slugCache) { // prevents error in page admin ( dom crawer issue with app.request )
  146. return false;
  147. }
  148. foreach (unserialize($slugCache) as $page) {
  149. if ($page['id'] == $pageID) {
  150. $finalUrl = $page['slug'];
  151. $confirmedPagePieces = explode('/', (string) $page['slug']);
  152. foreach ($parametersArray as $key => $parameter) {
  153. $slugCheck = str_replace(' ', '', (string) $key);
  154. $slugKey = array_search('{'.$slugCheck.'}', $confirmedPagePieces);
  155. if (!is_numeric($slugKey)) {
  156. $not_found[$key] = $parameter;
  157. } else {
  158. $finalUrl = str_replace('{'.$slugCheck.'}', $parameter, (string) $finalUrl);
  159. }
  160. }
  161. $getparams = '';
  162. if (count($not_found) > 0) {
  163. $getparams = '?';
  164. foreach ($not_found as $extraParam => $extraParamValue) {
  165. $getparams .= $extraParam.'='.$extraParamValue.'&';
  166. }
  167. $getparams = rtrim($getparams, '&');
  168. }
  169. return '/'.str_replace('//', '/', $finalUrl.$getparams);
  170. }
  171. }
  172. $getparams = '';
  173. if (count($not_found) > 0) {
  174. $getparams = '?path=notfound&';
  175. foreach ($not_found as $extraParam => $extraParamValue) {
  176. $getparams .= $extraParam.'='.$extraParamValue.'&';
  177. }
  178. $getparams = rtrim($getparams, '&');
  179. }
  180. return '#'.$getparams;
  181. }
  182. // used to tidy page {slugs}
  183. public function removeBracesFromSlug($string)
  184. {
  185. return preg_replace('#{[\s\S]+?}#', '', (string) $string);
  186. }
  187. // Inserts the relevant component into the page template
  188. // Also assists the new/edit page component selector (domcrawler picks up on the domcheck attribute)
  189. public function renderPcgcComponents($positionId, $pageComponents)
  190. {
  191. if ($pageComponents) {
  192. if ('domcheck' == $pageComponents[0]['position']) {
  193. echo "<div data-pcgc='domcheck'>".$positionId.'</div>';
  194. }
  195. foreach ($pageComponents as $component) {
  196. if ($component['position'] == $positionId && array_key_exists('data', $component)) {
  197. return $component['data'];
  198. }
  199. }
  200. }
  201. }
  202. // Inserts the relevant htmlblock into the page template
  203. // Also assists the new/edit page component selector (domcrawler picks up on the domcheck attribute)
  204. public function renderPcgcHtmlBlock($positionId, $pageHtmlblocks)
  205. {
  206. if ($pageHtmlblocks) {
  207. if ('domcheck' == $pageHtmlblocks[0]['position']) {
  208. echo "<div data-pcgc='domcheckhtml'>".$positionId.'</div>';
  209. }
  210. foreach ($pageHtmlblocks as $block) {
  211. if ($block['position'] == $positionId && array_key_exists('data', $block)) {
  212. if ($this->authorizationChecker->isGranted('ROLE_PAGE_EDITOR')) {
  213. return $this->getInlineEditorHTML(HtmlBlocks::class, 'html', $block['data'], $block['blockId'], 'HtmlBlock');
  214. }
  215. return $block['data'];
  216. }
  217. }
  218. }
  219. }
  220. // Inserts the relevant htmlblock into the page template
  221. // This function is not used tied to the CMS - renders same block on every page
  222. public function forceRenderPcgcHtmlBlock($identifier)
  223. {
  224. if ($identifier) {
  225. $block = $this->doctrine->getRepository(HtmlBlocks::class)->findOneBy(['title' => $identifier, 'deleted' => false, 'active' => true]);
  226. if (null !== $block) {
  227. if ($this->authorizationChecker->isGranted('ROLE_PAGE_EDITOR')) {
  228. return $this->getInlineEditorHTML(HtmlBlocks::class, 'html', $block->getHtml(), $block->getId(), 'HtmlBlock');
  229. }
  230. return $block->getHtml();
  231. }
  232. }
  233. }
  234. // Inserts the relevant htmlblock into the page template
  235. // This function is not used tied to the CMS - renders same block on every page
  236. public function forceRenderPcgcHtmlBlockByIdentifier($identifier)
  237. {
  238. if ($identifier) {
  239. $block = $this->doctrine->getRepository(HtmlBlocks::class)->findOneBy(['id' => $identifier, 'deleted' => false, 'active' => true]);
  240. if (null !== $block) {
  241. if ($this->authorizationChecker->isGranted('ROLE_PAGE_EDITOR')) {
  242. return $this->getInlineEditorHTML(HtmlBlocks::class, 'html', $block->getHtml(), $block->getId(), 'HtmlBlock');
  243. }
  244. return $block->getHtml();
  245. }
  246. }
  247. }
  248. public function allowInlineEditor($entity, $field)
  249. {
  250. $namespaceMeta = $this->serviceController->getBundleNameFromEntity($entity, $field);
  251. $getterMethod = 'get'.ucwords((string) $field);
  252. $editText = $entity->{$getterMethod}();
  253. if ('' == $editText) {
  254. return null;
  255. }
  256. $request = $this->requestStack->getCurrentRequest();
  257. // $request = $this->container->get('request');
  258. if ($request->query->has('preview')) {
  259. return $editText;
  260. }
  261. if ($this->authorizationChecker->isGranted('ROLE_PAGE_EDITOR')) {
  262. return $this->getInlineEditorHTML($namespaceMeta['full'], $field, $editText, $entity->getId(), $namespaceMeta['short'], $namespaceMeta['fieldmeta']);
  263. }
  264. return $editText;
  265. }
  266. public function showAdminControlLinks($entity, $route)
  267. {
  268. $namespaceMeta = $this->serviceController->getBundleNameFromEntity($entity);
  269. $url = $this->router->generate($route, ['id' => $entity->getId()]);
  270. if ($this->authorizationChecker->isGranted('ROLE_ADMIN')) {
  271. $buttons = "<div class='adminControlButtons'>";
  272. $buttons .= " <div class='inlineEditorToolboxContainer'>Admin Control (".$namespaceMeta['short'].')</div>';
  273. $buttons .= " <div class='inlineEditorToolboxLink'><a href='".$url."' data-toggle='tooltip' title='View/Edit' ><span class='glyphicon glyphicon-pencil'></span>&nbsp;View/Edit</a></div>";
  274. $buttons .= '</div>';
  275. return $buttons;
  276. }
  277. }
  278. public function getInlineEditorHTML($namespace, $field, $content, $id, $entityname = null, $fieldmeta = null)
  279. {
  280. // show inline editor
  281. $request = $this->requestStack->getCurrentRequest();
  282. $isPreview = $this->requestStack->getMainRequest()->query->getBoolean('preview');
  283. if ($isPreview) {
  284. return $content;
  285. }
  286. $locale = $request->getLocale();
  287. $showFullEditor = 1;
  288. if (null != $fieldmeta && 'string' == $fieldmeta['type']) {
  289. $showFullEditor = 0;
  290. }
  291. // Redactor required uniqueIDs - classes conflicted if multiple editors were used
  292. $uniqueID = substr(md5(random_int(1, 9999)), 0, 7);
  293. $editor = "<div class='inlineEditorContainer'>";
  294. $editor .= " <div id='inlineEditor-message-".$uniqueID."' class='inlineEditorToolboxContainer'>Editable (".$entityname.':'.$field.')</div>';
  295. $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>&nbsp;Save</a></div>";
  296. // $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>";
  297. $editor .= " <div class='inlineEditor' data-fulleditor='".$showFullEditor."' id='".$uniqueID."' data-entitynamespace='".$namespace."' data-entityfield='".$field."' data-id='".$id."' data-locale='".$locale."' >";
  298. if (0 == $showFullEditor) {
  299. $editor .= '<p>';
  300. }
  301. $editor .= $content;
  302. if (0 == $showFullEditor) {
  303. $editor .= '</p>';
  304. }
  305. $editor .= ' </div>';
  306. // if($showFullEditor ==1){
  307. // $editor .= " <textarea class='inlineEditor' data-fulleditor='".$showFullEditor."' id='".$uniqueID."' data-entitynamespace='".$namespace."' data-entityfield='".$field."' data-id='".$id."' data-locale='".$locale."' >";
  308. // $editor .= $content;
  309. // $editor .= " </textarea>";
  310. // }else{
  311. // $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."' />";
  312. // }
  313. $editor .= '</div>';
  314. return $editor;
  315. }
  316. // simple function to fetch all locales
  317. // done this way to ensure locale switch buttons work on non cms pages
  318. public function fetchLocales()
  319. {
  320. return $this->doctrine->getRepository(Locales::class)->findBy(['active' => true]);
  321. }
  322. public function breadcrumbs($pageEntity = null, $currentUrl = null)
  323. {
  324. // // this function generates a full url category path
  325. // $currentUrl = $this->container->get('request')->getUri();
  326. if (null != $pageEntity && null != $currentUrl) {
  327. $exploded = explode('/', (string) $currentUrl);
  328. unset($exploded[0]);
  329. $exploded = array_values($exploded);
  330. $structure = [];
  331. $explodedCount = count($exploded);
  332. for ($i = 0; $i < $explodedCount; ++$i) {
  333. if (array_key_exists($i - 1, $structure)) {
  334. $structure[$i] = $structure[$i - 1]['url'].'/'.$exploded[$i];
  335. } else {
  336. $structure[$i] = '/'.$exploded[$i];
  337. }
  338. $url = array_key_exists($i - 1, $structure) ? $structure[$i - 1]['url'].'/'.$exploded[$i] : '/'.$exploded[$i];
  339. $structure[$i] = ['url' => $url, 'title' => $exploded[$i]];
  340. }
  341. // print_r($structure);
  342. $seperater = ' / ';
  343. $html = '<div class="breadcrumb">';
  344. $html .= '<span><a href="/"><i class="fa-regular fa-house-blank"></i><span class="sr-only">Home</span></a></span>' .$seperater;
  345. $count = 0;
  346. foreach ($structure as $item) {
  347. ++$count;
  348. if (count($structure) == $count) {
  349. $seperater = '';
  350. }
  351. $html .= '<span><a href="'.$item['url'].'">'.str_replace('-', ' ', ucfirst($item['title'])).'</a></span>' .$seperater;
  352. }
  353. $html .= '</div>';
  354. echo $html;
  355. }
  356. }
  357. // simple function to pluralise text string (adds 's' if array count >1 )
  358. public function pluralize($text, $array, $plural_version = null)
  359. {
  360. return (is_countable($array) ? count($array) : 0) > 1 ? ($plural_version ?: $text.'s') : $text;
  361. }
  362. // simple word limiter function
  363. public function wordLimiter($str, $limit = 30)
  364. {
  365. $words = explode(' ', strip_tags((string) $str));
  366. if ($words > $limit) {
  367. return implode(' ', array_splice($words, 0, $limit)).'...';
  368. }
  369. return $str;
  370. }
  371. /**
  372. * Cache bust specified asset.
  373. */
  374. public function cacheBust(mixed $asset)
  375. {
  376. $asset = '/'.ltrim((string) $asset, '/');
  377. $assetPath = sprintf('%s/../public/%s', $this->projectRoot, $asset);
  378. // If we are assuming a CSS or JS file exists when it doesn't,
  379. // we probably need to know about it.
  380. if (!file_exists($assetPath)) {
  381. throw new \RuntimeException(sprintf('Asset: %s is missing!', $asset));
  382. }
  383. $modified = filemtime($assetPath);
  384. return $asset.sprintf('?version=%d', $modified);
  385. }
  386. }