src/Twig/TwigExtension.php line 45

Open in your IDE?
  1. <?php
  2. namespace App\Twig;
  3. use App\Entity\HtmlBlocks;
  4. use App\Entity\Locales;
  5. use App\Entity\Setting;
  6. use App\Service\SimpleCache;
  7. use Doctrine\Persistence\ManagerRegistry;
  8. use Twig\Environment;
  9. use Twig\Extension\AbstractExtension;
  10. use Twig\TwigFilter;
  11. use Twig\TwigFunction;
  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. new TwigFilter('html_entity_decode', fn ($string) => html_entity_decode($string)),
  29. ];
  30. }
  31. #[\Override]
  32. public function getFunctions()
  33. {
  34. return [
  35. new TwigFunction('renderComponents', fn ($positionId, $pageComponents) => $this->renderComponents($positionId, $pageComponents)),
  36. new TwigFunction('removeBracesFromSlug', fn ($string) => $this->removeBracesFromSlug($string)),
  37. new TwigFunction('generatePath', fn ($request, $pageID, $parametersArray = []) => $this->generatePath($request, $pageID, $parametersArray)),
  38. new TwigFunction('imageCache', fn ($pathtofile, $filter, $width = 0, $height = 0, $background = 'transparent') => $this->imageCache($pathtofile, $filter, $width, $height, $background)),
  39. new TwigFunction('fetchLocales', fn () => $this->fetchLocales()),
  40. new TwigFunction('breadcrumbs', function ($pageEntity = null, $currentUrl = null) {
  41. $this->breadcrumbs($pageEntity, $currentUrl);
  42. }),
  43. new TwigFunction('renderHtmlBlock', $this->renderHtmlBlock(...), ['needs_environment' => true]),
  44. new TwigFunction('allowInlineEditor', fn ($entity, $field) => $this->allowInlineEditor($entity, $field)),
  45. new TwigFunction('showAdminControlLinks', fn ($entity, $route) => $this->showAdminControlLinks($entity, $route)),
  46. new TwigFunction('moneyFormat', fn ($number) => $this->moneyFormat($number)),
  47. new TwigFunction('domCheckIgnore', fn ($value) => $this->domCheckIgnore($value)),
  48. new TwigFunction('componentEntities', fn ($pageComponents, $entityName = null, $field = null) => $this->componentEntities($pageComponents, $entityName, $field)),
  49. new TwigFunction('componentEntity', fn ($pageComponents, $field = null) => $this->componentEntity($pageComponents, $field)),
  50. new TwigFunction('replaceIfComponentDataExists', fn ($pageComponents, $field = null, $fallback = null) => $this->replaceIfComponentDataExists($pageComponents, $field, $fallback)),
  51. new TwigFunction('forceRenderHtmlBlock', fn ($identifier) => $this->forceRenderHtmlBlock($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((string) $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 componentEntities($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 componentEntity($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 'componentEntity' 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 renderComponents($positionId, $pageComponents)
  190. {
  191. if ($pageComponents) {
  192. if ('domcheck' == $pageComponents[0]['position']) {
  193. echo "<div data-cms='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 renderHtmlBlock(Environment $environment, $positionId, $pageHtmlblocks, $page = null)
  205. {
  206. if ($pageHtmlblocks) {
  207. if ('domcheck' == $pageHtmlblocks[0]['position']) {
  208. echo "<div data-cms='domcheckhtml'>".$positionId.'</div>';
  209. }
  210. foreach ($pageHtmlblocks as $block) {
  211. $block['page'] = $page ?? null;
  212. if ($block['position'] == $positionId && array_key_exists('data', $block)) {
  213. if ($this->authorizationChecker->isGranted('ROLE_PAGE_EDITOR')) {
  214. // return $this->getInlineEditorHTML(HtmlBlocks::class, 'html', $block['data'], $block['blockId'], 'HtmlBlock');
  215. }
  216. return $environment->render('@theme/common/htmlblock.html.twig', ['block' => $block]);
  217. }
  218. }
  219. }
  220. }
  221. // Inserts the relevant htmlblock into the page template
  222. // This function is not used tied to the CMS - renders same block on every page
  223. public function forceRenderHtmlBlock($identifier)
  224. {
  225. if ($identifier) {
  226. $block = $this->doctrine->getRepository(HtmlBlocks::class)->findOneBy(['title' => $identifier, 'deleted' => false, 'active' => true]);
  227. if (null !== $block) {
  228. // if ($this->authorizationChecker->isGranted('ROLE_PAGE_EDITOR')) {
  229. // return $this->getInlineEditorHTML(HtmlBlocks::class, 'html', $block->getHtml(), $block->getId(), 'HtmlBlock');
  230. // }
  231. return $block->getHtml();
  232. }
  233. }
  234. }
  235. public function allowInlineEditor($entity, $field)
  236. {
  237. $namespaceMeta = $this->serviceController->getBundleNameFromEntity($entity, $field);
  238. $getterMethod = 'get'.ucwords((string) $field);
  239. $editText = $entity->{$getterMethod}();
  240. if ('' == $editText) {
  241. $editText = '&nbsp;';
  242. // return null;
  243. }
  244. $request = $this->requestStack->getCurrentRequest();
  245. // $request = $this->container->get('request');
  246. if ($request->query->has('preview')) {
  247. return $editText;
  248. }
  249. if ($this->authorizationChecker->isGranted('ROLE_PAGE_EDITOR')) {
  250. // return $this->getInlineEditorHTML($namespaceMeta['full'], $field, $editText, $entity->getId(), $namespaceMeta['short'], $namespaceMeta['fieldmeta']);
  251. }
  252. return $editText;
  253. }
  254. public function showAdminControlLinks($entity, $route)
  255. {
  256. $namespaceMeta = $this->serviceController->getBundleNameFromEntity($entity);
  257. $url = $this->router->generate($route, ['id' => $entity->getId()]);
  258. if ($this->authorizationChecker->isGranted('ROLE_ADMIN')) {
  259. $buttons = "<div class='adminControlButtons'>";
  260. $buttons .= " <div class='inlineEditorToolboxContainer'>Admin Control (".$namespaceMeta['short'].')</div>';
  261. $buttons .= " <div class='inlineEditorToolboxLink'><a href='".$url."' data-toggle='tooltip' title='View/Edit' ><span class='glyphicon glyphicon-pencil'></span>&nbsp;View/Edit</a></div>";
  262. $buttons .= '</div>';
  263. return $buttons;
  264. }
  265. }
  266. public function getInlineEditorHTML($namespace, $field, $content, $id, $entityname = null, $fieldmeta = null)
  267. {
  268. // show inline editor
  269. $request = $this->requestStack->getCurrentRequest();
  270. $locale = $request->getLocale();
  271. $showFullEditor = 1;
  272. if (null != $fieldmeta && 'string' == $fieldmeta['type']) {
  273. $showFullEditor = 0;
  274. }
  275. // Redactor required uniqueIDs - classes conflicted if multiple editors were used
  276. $uniqueID = substr(md5(random_int(1, 9999)), 0, 7);
  277. $editor = "<div class='inlineEditorContainer'>";
  278. $editor .= " <div id='inlineEditor-message-".$uniqueID."' class='inlineEditorToolboxContainer'>Editable (".$entityname.':'.$field.')</div>';
  279. $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>";
  280. // $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>";
  281. $editor .= " <div class='inlineEditor' data-fulleditor='".$showFullEditor."' id='".$uniqueID."' data-entitynamespace='".$namespace."' data-entityfield='".$field."' data-id='".$id."' data-locale='".$locale."' >";
  282. if (0 == $showFullEditor) {
  283. $editor .= '<p>';
  284. }
  285. $editor .= $content;
  286. if (0 == $showFullEditor) {
  287. $editor .= '</p>';
  288. }
  289. $editor .= ' </div>';
  290. // if($showFullEditor ==1){
  291. // $editor .= " <textarea class='inlineEditor' data-fulleditor='".$showFullEditor."' id='".$uniqueID."' data-entitynamespace='".$namespace."' data-entityfield='".$field."' data-id='".$id."' data-locale='".$locale."' >";
  292. // $editor .= $content;
  293. // $editor .= " </textarea>";
  294. // }else{
  295. // $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."' />";
  296. // }
  297. $editor .= '</div>';
  298. return $editor;
  299. }
  300. // simple function to fetch all locales
  301. // done this way to ensure locale switch buttons work on non cms pages
  302. public function fetchLocales()
  303. {
  304. return $this->doctrine->getRepository(Locales::class)->findBy(['active' => true]);
  305. }
  306. public function breadcrumbs($pageEntity = null, $currentUrl = null)
  307. {
  308. // // this function generates a full url category path
  309. // $currentUrl = $this->container->get('request')->getUri();
  310. if (null != $pageEntity && null != $currentUrl) {
  311. $exploded = explode('/', (string) $currentUrl);
  312. unset($exploded[0]);
  313. $exploded = array_values($exploded);
  314. $structure = [];
  315. $explodedCount = count($exploded);
  316. for ($i = 0; $i < $explodedCount; ++$i) {
  317. if (array_key_exists($i - 1, $structure)) {
  318. $structure[$i] = $structure[$i - 1]['url'].'/'.$exploded[$i];
  319. } else {
  320. $structure[$i] = '/'.$exploded[$i];
  321. }
  322. $url = array_key_exists($i - 1, $structure) ? $structure[$i - 1]['url'].'/'.$exploded[$i] : '/'.$exploded[$i];
  323. $structure[$i] = ['url' => $url, 'title' => $exploded[$i]];
  324. }
  325. // print_r($structure);
  326. $seperater = ' > ';
  327. $html = '<div class="breadcrumb">';
  328. $html .= '<span><a href="/">Home</a>'.$seperater.'</span>';
  329. $count = 0;
  330. foreach ($structure as $item) {
  331. ++$count;
  332. if (count($structure) == $count) {
  333. $seperater = '';
  334. }
  335. $html .= '<span><a href="'.$item['url'].'">'.str_replace('-', ' ', ucfirst($item['title'])).'</a>'.$seperater.'</span>';
  336. }
  337. $html .= '</div>';
  338. echo $html;
  339. }
  340. }
  341. // simple function to pluralise text string (adds 's' if array count >1 )
  342. public function pluralize($text, $array, $plural_version = null)
  343. {
  344. return (is_countable($array) ? count($array) : 0) > 1 ? ($plural_version ?: $text.'s') : $text;
  345. }
  346. // simple word limiter function
  347. public function wordLimiter($str, $limit = 30)
  348. {
  349. $words = explode(' ', strip_tags((string) $str));
  350. if ($words > $limit) {
  351. return implode(' ', array_splice($words, 0, $limit)).'...';
  352. }
  353. return $str;
  354. }
  355. /**
  356. * Cache bust specified asset.
  357. */
  358. public function cacheBust(mixed $asset)
  359. {
  360. $asset = '/'.ltrim((string) $asset, '/');
  361. $assetPath = sprintf('%s/../public/%s', $this->projectRoot, $asset);
  362. // If we are assuming a CSS or JS file exists when it doesn't,
  363. // we probably need to know about it.
  364. if (!file_exists($assetPath)) {
  365. throw new \RuntimeException(sprintf('Asset: %s is missing!', $asset));
  366. }
  367. $modified = filemtime($assetPath);
  368. return $asset.sprintf('?version=%d', $modified);
  369. }
  370. }