src/Twig/Runtime/PlatformComponentRuntime.php line 101

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