src/Twig/Runtime/PlatformComponentRuntime.php line 444

Open in your IDE?
  1. <?php
  2. namespace App\Twig\Runtime;
  3. use App\Entity\User;
  4. use App\Services\Back\Settings\FrontService;
  5. use Exception;
  6. use JsonException;
  7. use Psr\Log\LoggerInterface;
  8. use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
  9. use Symfony\Component\HttpKernel\KernelInterface;
  10. use Symfony\Component\Security\Core\Security;
  11. use Twig\Environment;
  12. use Twig\Error\LoaderError;
  13. use Twig\Error\RuntimeError;
  14. use Twig\Error\SyntaxError;
  15. use Twig\Extension\RuntimeExtensionInterface;
  16. class PlatformComponentRuntime implements RuntimeExtensionInterface
  17. {
  18. private ParameterBagInterface $params;
  19. private Environment $twig;
  20. private Security $security;
  21. private LoggerInterface $logger;
  22. private FrontService $frontService;
  23. private KernelInterface $kernel;
  24. private string $projectDir;
  25. private array $contentDataCache = [];
  26. private array $componentOptionsCache = [];
  27. /**
  28. * @param ParameterBagInterface $params
  29. * @param Environment $twig
  30. * @param Security $security
  31. * @param LoggerInterface $logger
  32. * @param FrontService $frontService
  33. * @param KernelInterface $kernel
  34. * @param string $projectDir
  35. */
  36. public function __construct(
  37. ParameterBagInterface $params,
  38. Environment $twig,
  39. Security $security,
  40. LoggerInterface $logger,
  41. FrontService $frontService,
  42. KernelInterface $kernel,
  43. string $projectDir
  44. ) {
  45. $this->params = $params;
  46. $this->twig = $twig;
  47. $this->security = $security;
  48. $this->logger = $logger;
  49. $this->frontService = $frontService;
  50. $this->projectDir = $projectDir;
  51. $this->kernel = $kernel;
  52. }
  53. /**
  54. * Retourne le contenu d'un component
  55. *
  56. * @TODO Attention,si on passe le $component comme un tableau avec les valeurs du yaml !
  57. * On ne peut pas utiliser le système d'ACL dynamique
  58. *
  59. * @param string|array $component
  60. * @param string $componentKey
  61. * @param null $data
  62. * @param null $index
  63. * @param bool $debug
  64. *
  65. * @return string
  66. *
  67. * @throws JsonException
  68. */
  69. public function component($component, string $componentKey = '', $data = NULL, $index = NULL, bool $debug = FALSE): string
  70. {
  71. $keys = $componentKey;
  72. if (is_array($component) && isset($component[ 'type' ])) {
  73. $value = $component;
  74. } else {
  75. $keyArr = explode('.', $keys);
  76. if (count($keyArr) === 2) {
  77. $data = $this->getContentData($keyArr[ 1 ], $keyArr[ 0 ]);
  78. $value = $data[ 'pageArray' ];
  79. } else {
  80. $value = $this->checkComponent($component, $keys, $index);
  81. }
  82. }
  83. if (!isset($value[ 'type' ])) {
  84. return '<div class="text-danger">The component ' . $component . ' is typeless!</div>';
  85. }
  86. // try {
  87. return $this->buildComponent($value, $keys, $data, $debug);
  88. // }
  89. // catch (LoaderError|RuntimeError|SyntaxError $e)
  90. // {
  91. // if($this->kernel->getEnvironment() == 'dev') throw $e;
  92. //
  93. // $message = 'Le component ' . (is_array($keys) ? implode('.', $keys) : $value[ 'type' ]) . ' ne peut pas être généré.';
  94. // $this->logger->critical($message . $e->getMessage());
  95. //
  96. // $error = '<div class="text-danger">' . $message;
  97. // /** @var User $currentUser */
  98. // $currentUser = $this->security->getUser();
  99. //
  100. // if (($currentUser !== NULL && $currentUser->isDeveloper()) || $this->kernel->getEnvironment() === 'dev') {
  101. // $error .= '<pre>' . $e->getMessage() . '</pre>';
  102. // }
  103. // $error .= '</div>';
  104. // return $error;
  105. // }
  106. }
  107. /**
  108. * @param string $page
  109. * @param $data
  110. * @param bool $isSecurity
  111. *
  112. * @return string
  113. *
  114. * @throws JsonException
  115. */
  116. public function content(string $page, $data = NULL, bool $isSecurity = FALSE, string $key = null): string
  117. {
  118. $frontType = $isSecurity ? 'security' : 'content';
  119. $contentData = $this->getContentData($page, $frontType);
  120. $pageArray = $contentData[ 'pageArray' ] ?? [];
  121. $frontCat = $contentData[ 'frontCat' ];
  122. $divs = $this->getContentStartDivs($pageArray);
  123. $content = '';
  124. if($key) {
  125. if (!isset($pageArray[$key])) {
  126. return '';
  127. }
  128. $content .= $this->component($pageArray[$key], $contentData[ 'contentKey' ] . '.sections.' . $key, $data, NULL, TRUE);
  129. return implode('', $divs) . $content . str_repeat('</div>', count($divs));
  130. }
  131. $items = $pageArray[ 'sections' ];
  132. foreach ($items as $key => $item)
  133. {
  134. // try {
  135. $content .= $this->component($item, $contentData[ 'contentKey' ] . '.sections.' . $key, $data, NULL, TRUE);
  136. // $content .= $this->component( $frontCat . '.' . $page . '.sections.' . $key, $data );
  137. // } catch (Exception $e) {
  138. // /** @var User $currentUser */
  139. // $currentUser = $this->security->getUser();
  140. // if ($currentUser !== NULL && $currentUser->isDeveloper()) {
  141. // echo '<div style="border: 1px red solid; padding:8px; color:red; text-align:center">' .
  142. // '<strong>' . $frontCat . '.' . $page . '.sections.' . $key . '</strong><br>' .
  143. // $e->getMessage() .
  144. // '</div>';
  145. // }
  146. // }
  147. }
  148. return implode('', $divs) . $content . str_repeat('</div>', count($divs));
  149. }
  150. /**
  151. * @param $item
  152. *
  153. * @return array
  154. */
  155. public function getItemData($item): array
  156. {
  157. $result = [];
  158. if (isset($item[ 'data' ]) && count($item[ 'data' ]) > 0) {
  159. foreach ($item[ 'data' ] as $k => $v) {
  160. $result[ 'data-' . str_replace('_', '-', $k) ] = $v;
  161. }
  162. }
  163. return $result;
  164. }
  165. /**
  166. * Retourne le tableau permettant la génération dynamique d'un élément en twig (wrapper, item, container)
  167. *
  168. * Les components doivent être configuré avec les éléments suivants :
  169. *
  170. * mon_component:
  171. * type: mon_type_de_component
  172. * wrapper: <== va gérer une div qui engloble le component
  173. * class: ""
  174. * class: "" <== va gérer la class du component
  175. * container: <== va gérer une div interne au component qui va contenir les sous-éléments du component
  176. * class: ""
  177. *
  178. * <div class="ma-classe-wrapper" + autres éléments dans wrapper>
  179. * <div class="ma-classe" + autres élément>
  180. * <div class="ma-classe-container" + autres éléments dans container>
  181. *
  182. * Cette configuration permet une plus grande souplesse pour organiser les éléments via les class bootstrap
  183. *
  184. * @param array|string $item tableau contenant les données de l'élément, si c'est une string, c'est pour maintenir l'ancien système
  185. * @param string $key clé du data-component-acl pour son identification
  186. * @param bool $debug
  187. *
  188. * @return array tableau contenant les informations
  189. *
  190. * id => si le component doit avoir un id, '' par défaut
  191. * class => class de l'élément, '' par défaut
  192. * data => tableau qui contient tous les éléments data de l'élément et leur valeur (data-foo="bla"), [] par défaut
  193. * tag => le tag de l'élément si c'est précisé, div par défaut
  194. * style => tableau si des éléments doivent être passé dans style (style="background:red"), défaut []
  195. * enabled => Bool pour savoir si l'élément s'affiche ou non, défaut TRUE
  196. * display => tableau qui gère l'affichage par addition ou soustraction sur des pages, défaut []
  197. * univers => uniquement si des datas sont passée dans l'item
  198. */
  199. public function generateDomOption($item, string $key = '', bool $debug = false): array
  200. {
  201. $result = [
  202. 'id' => '',
  203. 'data' => [],
  204. 'tag' => 'div',
  205. 'style' => [],
  206. 'enabled' => true,
  207. 'display' => []
  208. ];
  209. // $item n'est pas un array (ancien système → wrapper correspond à la class)
  210. if (!is_array($item)) {
  211. $result[ 'class' ] = $item;
  212. return $result;
  213. }
  214. $result[ 'class' ] = $this->getClassForItem($item);
  215. $result[ 'id' ] = $item[ 'id' ] ?? $result[ 'id' ];
  216. $result[ 'tag' ] = $item[ 'tag' ] ?? $result[ 'tag' ];
  217. if (isset($item[ 'data' ]) && $item[ 'data' ] !== []) {
  218. $result[ 'data' ] = $this->getItemData($item);
  219. }
  220. if ($key !== '') {
  221. $result[ 'data' ][ 'data-component-acl' ] = $key;
  222. $result[ 'enabled' ] = $item[ 'enabled' ] ?? TRUE;
  223. $result[ 'display' ] = $item[ 'display' ] ?? [];
  224. if (isset($item[ 'univers' ]) && $item[ 'univers' ] !== []) {
  225. $result[ 'univers' ] = $item[ 'univers' ];
  226. }
  227. }
  228. if (isset($item[ 'style' ]) && $item[ 'style' ] !== []) {
  229. foreach ($item[ 'style' ] as $rule => $value) {
  230. $result[ 'style' ][ str_replace('_', '-', $rule) ] = $value;
  231. }
  232. }
  233. return $result;
  234. }
  235. /**
  236. * Génère le tableau permettant la création dynamique d'un atom dans le twig
  237. *
  238. * Pour un atom, c'est la clef wrapper qui va prendre le data-acl-component
  239. *
  240. * @param array|null $atom tableau contenant les data de l'atom TODO gerer un toArray si on passe un objet
  241. * @param string|null $key clé identifiant l'atom pour les ACL
  242. * @param bool $debug
  243. *
  244. * @return array
  245. */
  246. public function generateAtomOptions(?array $atom, ?string $key = '', bool $debug = FALSE): array
  247. {
  248. // wrapper n'existe pas, ou est null, ou n'est pas un tableau
  249. switch (TRUE) {
  250. case !isset($atom[ 'wrapper' ]):
  251. $wrapper = [];
  252. break;
  253. case is_string($atom[ 'wrapper' ]):
  254. $wrapper = [
  255. 'class' => $atom[ 'wrapper' ],
  256. ];
  257. break;
  258. default:
  259. $wrapper = $atom[ 'wrapper' ];
  260. break;
  261. }
  262. $result = array_merge(
  263. [
  264. 'enabled' => $atom[ 'enabled' ] ?? TRUE,
  265. ],
  266. $wrapper,
  267. );
  268. return $this->generateDomOption($result, $key);
  269. }
  270. /**
  271. * @param $component
  272. * @param string|null $key
  273. *
  274. * @return array
  275. */
  276. public function generateComponentOptions($component, ?string $key = '', $debug = false): array
  277. {
  278. $cacheKey = md5(json_encode([$key, $component]));
  279. if (array_key_exists($cacheKey, $this->componentOptionsCache)) {
  280. return $this->componentOptionsCache[$cacheKey];
  281. }
  282. $key = $key ?? '';
  283. // WRAPPER
  284. $wrapperKey = 'wrapper';
  285. $wrapper = isset($component[ $wrapperKey ]) ? $this->generateDomOption($component[ $wrapperKey ], '', $debug) : $this->generateDomOption([], '', $debug);
  286. // ITEM
  287. $item = $this->generateDomOption($component, $key, $debug);
  288. // CONTAINER
  289. $containerKey = 'container';
  290. $container = isset($component[ $containerKey ]) ? $this->generateDomOption($component[ $containerKey ], '', $debug) : $this->generateDomOption([], '', $debug);
  291. return $this->componentOptionsCache[$cacheKey] = [
  292. 'wrapper' => $wrapper,
  293. 'item' => $item,
  294. 'container' => $container,
  295. ];
  296. }
  297. /**
  298. * @param string $keys
  299. * @param array|null $platform
  300. * @param string|null $lastKeyPlatform
  301. *
  302. * @return array|mixed
  303. *
  304. * @throws JsonException
  305. */
  306. public function getFrontDataFromSettingOrYaml(string $keys, ?array $platform, ?string $lastKeyPlatform = NULL)
  307. {
  308. return $this->frontService->getFrontDataFromSettingOrYaml($keys, $platform, $lastKeyPlatform);
  309. }
  310. /**
  311. * TODO Vérifier son utilisation, pour le moment uniquement sur default_progression_status_step.html.twig
  312. *
  313. * @param $atomic_component
  314. *
  315. * @return string
  316. *
  317. * @throws LoaderError
  318. * @throws RuntimeError
  319. * @throws SyntaxError
  320. */
  321. public function customAtomicContent($atomic_component): string
  322. {
  323. $folder = '/templates/platform/component';
  324. if (file_exists($this->projectDir . $folder . '/atom/' . $atomic_component . '.html.twig')) {
  325. $view = 'platform/component/atom/' . $atomic_component . '.html.twig';
  326. } elseif (file_exists($this->projectDir . $folder . '/molecule/' . $atomic_component . '.html.twig')) {
  327. $view = 'platform/component/molecule/' . $atomic_component . '.html.twig';
  328. } elseif (file_exists($this->projectDir . $folder . '/organism/' . $atomic_component . '.html.twig')) {
  329. $view = 'platform/component/organism/' . $atomic_component . '.html.twig';
  330. } else {
  331. return $atomic_component . ' not found !';
  332. }
  333. return $this->twig->render($view);
  334. }
  335. /**
  336. * @param $component
  337. * @param $keys
  338. * @param $index
  339. *
  340. * @return mixed
  341. */
  342. private function checkComponent($component, &$keys, $index)
  343. {
  344. $platform = $this->params->get('platform');
  345. $value = $platform;
  346. $keys = explode('.', $component);
  347. $i = 1;
  348. foreach ($keys as $key) {
  349. if (!isset($value[ $key ]) &&
  350. !isset($value[ 'global' ][ $key ]) &&
  351. !isset($value[ 'front' ][ $key ]) &&
  352. !isset($value[ 'back_office' ][ $key ])) {
  353. // On affiche l'erreur de key non trouvée que pour les développeurs.
  354. // En prod et pour les autres utilisateurs, on n'affiche rien (une erreur log est générée néanmoins).
  355. /** @var User $currentUser */
  356. $currentUser = $this->security->getUser();
  357. if ($currentUser !== NULL && $currentUser->isDeveloper()) {
  358. return "key '$key' of '$component' does not exist";
  359. }
  360. $this->logger->error("key '$key' of '$component' does not exist");
  361. return '';
  362. }
  363. if (isset($value[ 'global' ][ $key ])) {
  364. $value = $value[ 'global' ][ $key ];
  365. } elseif (isset($value[ 'front' ][ $key ])) {
  366. $value = $value[ 'front' ][ $key ];
  367. } elseif (isset($value[ 'back_office' ][ $key ])) {
  368. $value = $value[ 'back_office' ][ $key ];
  369. } else {
  370. $value = $value[ $key ];
  371. }
  372. // si un index est passé et qu'on est à la dernière clé, on va chercher l'objet à l'index donné.
  373. if (NULL !== $index && $i === count($keys)) {
  374. $value = $value[ $index ];
  375. }
  376. $i++;
  377. }
  378. return $value;
  379. }
  380. /**
  381. * @throws SyntaxError
  382. * @throws RuntimeError
  383. * @throws LoaderError
  384. */
  385. private function buildComponent($value, $keys, $data, $debug = FALSE): string
  386. {
  387. $response = '<div class="text-danger">' . $value[ 'type' ] . ' not found in components !</div>';
  388. /** @var User $currentUser */
  389. $currentUser = $this->security->getUser();
  390. $componentAclFullKey = is_array($keys) ? implode('.', $keys) : $keys;
  391. $folder = '/templates/platform/component';
  392. if (!(isset($value[ 'disabled' ]) && $value[ 'disabled' ] === TRUE)) {
  393. if (file_exists($this->projectDir . $folder . '/atom/' . $value[ 'type' ] . '.html.twig')) {
  394. $view = 'platform/component/atom/' . $value[ 'type' ] . '.html.twig';
  395. } elseif (file_exists($this->projectDir . $folder . '/molecule/' . $value[ 'type' ] . '.html.twig')) {
  396. $view = 'platform/component/molecule/' . $value[ 'type' ] . '.html.twig';
  397. } elseif (file_exists($this->projectDir . $folder . '/organism/' . $value[ 'type' ] . '.html.twig')) {
  398. $view = 'platform/component/organism/' . $value[ 'type' ] . '.html.twig';
  399. }
  400. if (isset($view)) {
  401. $response = $this->twig->render($view, [
  402. 'value' => $value,
  403. 'data' => $data,
  404. 'componentKey' => $componentAclFullKey,
  405. ]);
  406. }
  407. }
  408. if (isset($view) && $currentUser !== NULL && $currentUser->isDeveloper()) {
  409. $response = "\n<!-- ***** START component " . $value[ 'type' ] . " : " . $view . " ***** -->\n" .
  410. $response .
  411. "\n<!-- ***** END component " . $value[ 'type' ] . " ***** -->\n";
  412. }
  413. return $response;
  414. }
  415. /**
  416. * @param string $page
  417. * @param string $frontType
  418. *
  419. * @return array
  420. * @throws JsonException
  421. */
  422. private function getContentData(string $page, string $frontType): array
  423. {
  424. $cacheKey = $frontType . ':' . $page;
  425. if (array_key_exists($cacheKey, $this->contentDataCache)) {
  426. return $this->contentDataCache[$cacheKey];
  427. }
  428. if (!in_array($frontType, ['security', 'common', 'content'])) {
  429. $frontType = 'content';
  430. }
  431. // on regarde si on a des données en BDD pour cette page
  432. $fromBdd = $this->frontService->getArrayDataFromSetting('front.' . $frontType . '.' . $page);
  433. if ($fromBdd !== []) {
  434. $pageArray = $fromBdd;
  435. } else {
  436. $platform = $this->params->get('platform');
  437. $pageArray = $platform[ 'front' ][ $frontType ];
  438. }
  439. $testPage = explode('.', $page);
  440. if (count($testPage) > 1) {
  441. foreach ($testPage as $item) {
  442. $pageArray = $pageArray[ $item ];
  443. }
  444. } else {
  445. $pageArray = $pageArray[ $page ];
  446. }
  447. return $this->contentDataCache[$cacheKey] = [
  448. 'pageArray' => $pageArray,
  449. 'frontCat' => $frontType,
  450. 'contentKey' => $frontType . '.' . $page,
  451. ];
  452. }
  453. /**
  454. * Génère les div d'ouverture lorsque content() est appelé
  455. *
  456. * TODO à revoir pour verrouiller
  457. *
  458. * container peut avoir plusieurs valeurs
  459. * - TRUE => on ajoute une div class="container" au debut
  460. * - container => on ajoute une div class="container" au debut
  461. * - container-fluid => on ajoute une div class="container-fluid" au debut
  462. * - fluid => on ajoute une div class="container-fluid" au debut
  463. *
  464. * si la clef row existe et n'est pas FALSE => on rajoute une div class="row" après le container
  465. * si la clef row existe et n'est pas FALSE et que la clef row_justify existe => on rajoute une div class="row [valeur de row_justify]" après le container
  466. *
  467. * @param array $pageArray
  468. *
  469. * @return array
  470. */
  471. private function getContentStartDivs(array $pageArray): array
  472. {
  473. $divs = [];
  474. // 3 cas possibles
  475. // la clef n'existe pas ou est à false → pas de container
  476. if (isset($pageArray[ 'container' ])) {
  477. // si la clef est à true ou "container" → container
  478. if (in_array(
  479. $pageArray[ 'container' ],
  480. [TRUE, 'container'],
  481. TRUE
  482. )) {
  483. $divs[] = '<div class="container">';
  484. // si la clef est à "fluid" ou "container-fluid" => container-fluid
  485. } elseif (in_array(
  486. $pageArray[ 'container' ],
  487. ['container-fluid', 'fluid']
  488. )) {
  489. $divs[] = '<div class="container-fluid">';
  490. }
  491. }
  492. if (
  493. isset($pageArray[ 'row' ])
  494. && $pageArray[ 'row' ] !== FALSE
  495. ) {
  496. $row_justify = $pageArray[ 'row_justify' ] ?? '';
  497. $divs[] = '<div class="row ' . $row_justify . '">';
  498. }
  499. return $divs;
  500. }
  501. private function getClassForItem($item)
  502. {
  503. $allClass = $item[ 'class' ] ?? '';
  504. $allClassArray = explode(' ', $allClass);
  505. $classCatArr = [];
  506. if (isset($item[ 'class_category' ])) {
  507. foreach ($item[ 'class_category' ] as $key => $value) {
  508. $classCatArr[ $key ] = !is_array($value) ? explode(' ', $value) : $value;
  509. }
  510. }
  511. $merged = array_merge($allClassArray, ...array_values($classCatArr));
  512. $allClass = array_unique($merged);
  513. return implode(' ', $allClass);
  514. }
  515. public function urlExist($url): bool
  516. {
  517. stream_context_set_default( [
  518. 'ssl' => [
  519. 'verify_peer' => false,
  520. 'verify_peer_name' => false,
  521. ],
  522. ]);
  523. $headers = get_headers($url);
  524. return (bool)stripos($headers[ 0 ], "200 OK");
  525. }
  526. }