<?php
namespace App\Twig\Runtime;
use App\Entity\User;
use App\Services\Back\Settings\FrontService;
use Exception;
use JsonException;
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
use Symfony\Component\HttpKernel\KernelInterface;
use Symfony\Component\Security\Core\Security;
use Twig\Environment;
use Twig\Error\LoaderError;
use Twig\Error\RuntimeError;
use Twig\Error\SyntaxError;
use Twig\Extension\RuntimeExtensionInterface;
class PlatformComponentRuntime implements RuntimeExtensionInterface
{
private ParameterBagInterface $params;
private Environment $twig;
private Security $security;
private LoggerInterface $logger;
private FrontService $frontService;
private KernelInterface $kernel;
private string $projectDir;
/**
* @param ParameterBagInterface $params
* @param Environment $twig
* @param Security $security
* @param LoggerInterface $logger
* @param FrontService $frontService
* @param KernelInterface $kernel
* @param string $projectDir
*/
public function __construct(
ParameterBagInterface $params,
Environment $twig,
Security $security,
LoggerInterface $logger,
FrontService $frontService,
KernelInterface $kernel,
string $projectDir
) {
$this->params = $params;
$this->twig = $twig;
$this->security = $security;
$this->logger = $logger;
$this->frontService = $frontService;
$this->projectDir = $projectDir;
$this->kernel = $kernel;
}
/**
* Retourne le contenu d'un component
*
* @TODO Attention,si on passe le $component comme un tableau avec les valeurs du yaml !
* On ne peut pas utiliser le système d'ACL dynamique
*
* @param string|array $component
* @param string $componentKey
* @param null $data
* @param null $index
* @param bool $debug
*
* @return string
*
* @throws JsonException
*/
public function component($component, string $componentKey = '', $data = NULL, $index = NULL, bool $debug = FALSE): string
{
$keys = $componentKey;
if (is_array($component) && isset($component[ 'type' ])) {
$value = $component;
} else {
$keyArr = explode('.', $keys);
if (count($keyArr) === 2) {
$data = $this->getContentData($keyArr[ 1 ], $keyArr[ 0 ]);
$value = $data[ 'pageArray' ];
} else {
$value = $this->checkComponent($component, $keys, $index);
}
}
if (!isset($value[ 'type' ])) {
return '<div class="text-danger">The component ' . $component . ' is typeless!</div>';
}
// try {
return $this->buildComponent($value, $keys, $data, $debug);
// }
// catch (LoaderError|RuntimeError|SyntaxError $e)
// {
// if($this->kernel->getEnvironment() == 'dev') throw $e;
//
// $message = 'Le component ' . (is_array($keys) ? implode('.', $keys) : $value[ 'type' ]) . ' ne peut pas être généré.';
// $this->logger->critical($message . $e->getMessage());
//
// $error = '<div class="text-danger">' . $message;
// /** @var User $currentUser */
// $currentUser = $this->security->getUser();
//
// if (($currentUser !== NULL && $currentUser->isDeveloper()) || $this->kernel->getEnvironment() === 'dev') {
// $error .= '<pre>' . $e->getMessage() . '</pre>';
// }
// $error .= '</div>';
// return $error;
// }
}
/**
* @param string $page
* @param $data
* @param bool $isSecurity
*
* @return string
*
* @throws JsonException
*/
public function content(string $page, $data = NULL, bool $isSecurity = FALSE, string $key = null): string
{
$frontType = $isSecurity ? 'security' : 'content';
$contentData = $this->getContentData($page, $frontType);
$pageArray = $contentData[ 'pageArray' ] ?? [];
$frontCat = $contentData[ 'frontCat' ];
$divs = $this->getContentStartDivs($pageArray);
$content = '';
if($key) {
$content .= $this->component($pageArray[$key], $contentData[ 'contentKey' ] . '.sections.' . $key, $data, NULL, TRUE);
return implode('', $divs) . $content . str_repeat('</div>', count($divs));
}
$items = $pageArray[ 'sections' ];
foreach ($items as $key => $item)
{
// try {
$content .= $this->component($item, $contentData[ 'contentKey' ] . '.sections.' . $key, $data, NULL, TRUE);
// $content .= $this->component( $frontCat . '.' . $page . '.sections.' . $key, $data );
// } catch (Exception $e) {
// /** @var User $currentUser */
// $currentUser = $this->security->getUser();
// if ($currentUser !== NULL && $currentUser->isDeveloper()) {
// echo '<div style="border: 1px red solid; padding:8px; color:red; text-align:center">' .
// '<strong>' . $frontCat . '.' . $page . '.sections.' . $key . '</strong><br>' .
// $e->getMessage() .
// '</div>';
// }
// }
}
return implode('', $divs) . $content . str_repeat('</div>', count($divs));
}
/**
* @param $item
*
* @return array
*/
public function getItemData($item): array
{
$result = [];
if (isset($item[ 'data' ]) && count($item[ 'data' ]) > 0) {
foreach ($item[ 'data' ] as $k => $v) {
$result[ 'data-' . str_replace('_', '-', $k) ] = $v;
}
}
return $result;
}
/**
* Retourne le tableau permettant la génération dynamique d'un élément en twig (wrapper, item, container)
*
* Les components doivent être configuré avec les éléments suivants :
*
* mon_component:
* type: mon_type_de_component
* wrapper: <== va gérer une div qui engloble le component
* class: ""
* class: "" <== va gérer la class du component
* container: <== va gérer une div interne au component qui va contenir les sous-éléments du component
* class: ""
*
* <div class="ma-classe-wrapper" + autres éléments dans wrapper>
* <div class="ma-classe" + autres élément>
* <div class="ma-classe-container" + autres éléments dans container>
*
* Cette configuration permet une plus grande souplesse pour organiser les éléments via les class bootstrap
*
* @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
* @param string $key clé du data-component-acl pour son identification
* @param bool $debug
*
* @return array tableau contenant les informations
*
* id => si le component doit avoir un id, '' par défaut
* class => class de l'élément, '' par défaut
* data => tableau qui contient tous les éléments data de l'élément et leur valeur (data-foo="bla"), [] par défaut
* tag => le tag de l'élément si c'est précisé, div par défaut
* style => tableau si des éléments doivent être passé dans style (style="background:red"), défaut []
* enabled => Bool pour savoir si l'élément s'affiche ou non, défaut TRUE
* display => tableau qui gère l'affichage par addition ou soustraction sur des pages, défaut []
* univers => uniquement si des datas sont passée dans l'item
*/
public function generateDomOption($item, string $key = '', bool $debug = false): array
{
$result = [
'id' => '',
'data' => [],
'tag' => 'div',
'style' => [],
'enabled' => true,
'display' => []
];
// $item n'est pas un array (ancien système → wrapper correspond à la class)
if (!is_array($item)) {
$result[ 'class' ] = $item;
return $result;
}
$result[ 'class' ] = $this->getClassForItem($item);
$result[ 'id' ] = $item[ 'id' ] ?? $result[ 'id' ];
$result[ 'tag' ] = $item[ 'tag' ] ?? $result[ 'tag' ];
if (isset($item[ 'data' ]) && $item[ 'data' ] !== []) {
$result[ 'data' ] = $this->getItemData($item);
}
if ($key !== '') {
$result[ 'data' ][ 'data-component-acl' ] = $key;
$result[ 'enabled' ] = $item[ 'enabled' ] ?? TRUE;
$result[ 'display' ] = $item[ 'display' ] ?? [];
if (isset($item[ 'univers' ]) && $item[ 'univers' ] !== []) {
$result[ 'univers' ] = $item[ 'univers' ];
}
}
if (isset($item[ 'style' ]) && $item[ 'style' ] !== []) {
foreach ($item[ 'style' ] as $rule => $value) {
$result[ 'style' ][ str_replace('_', '-', $rule) ] = $value;
}
}
return $result;
}
/**
* Génère le tableau permettant la création dynamique d'un atom dans le twig
*
* Pour un atom, c'est la clef wrapper qui va prendre le data-acl-component
*
* @param array|null $atom tableau contenant les data de l'atom TODO gerer un toArray si on passe un objet
* @param string|null $key clé identifiant l'atom pour les ACL
* @param bool $debug
*
* @return array
*/
public function generateAtomOptions(?array $atom, ?string $key = '', bool $debug = FALSE): array
{
// wrapper n'existe pas, ou est null, ou n'est pas un tableau
switch (TRUE) {
case !isset($atom[ 'wrapper' ]):
$wrapper = [];
break;
case is_string($atom[ 'wrapper' ]):
$wrapper = [
'class' => $atom[ 'wrapper' ],
];
break;
default:
$wrapper = $atom[ 'wrapper' ];
break;
}
$result = array_merge(
[
'enabled' => $atom[ 'enabled' ] ?? TRUE,
],
$wrapper,
);
return $this->generateDomOption($result, $key);
}
/**
* @param $component
* @param string|null $key
*
* @return array
*/
public function generateComponentOptions($component, ?string $key = ''): array
{
$key = $key ?? '';
// WRAPPER
$wrapperKey = 'wrapper';
$wrapper = isset($component[ $wrapperKey ]) ? $this->generateDomOption($component[ $wrapperKey ]) : $this->generateDomOption([]);
// ITEM
$item = $this->generateDomOption($component, $key);
// CONTAINER
$containerKey = 'container';
$container = isset($component[ $containerKey ]) ? $this->generateDomOption($component[ $containerKey ]) : $this->generateDomOption([]);
return [
'wrapper' => $wrapper,
'item' => $item,
'container' => $container,
];
}
/**
* @param string $keys
* @param array|null $platform
* @param string|null $lastKeyPlatform
*
* @return array|mixed
*
* @throws JsonException
*/
public function getFrontDataFromSettingOrYaml(string $keys, ?array $platform, ?string $lastKeyPlatform = NULL)
{
return $this->frontService->getFrontDataFromSettingOrYaml($keys, $platform, $lastKeyPlatform);
}
/**
* TODO Vérifier son utilisation, pour le moment uniquement sur default_progression_status_step.html.twig
*
* @param $atomic_component
*
* @return string
*
* @throws LoaderError
* @throws RuntimeError
* @throws SyntaxError
*/
public function customAtomicContent($atomic_component): string
{
$folder = '/templates/platform/component';
if (file_exists($this->projectDir . $folder . '/atom/' . $atomic_component . '.html.twig')) {
$view = 'platform/component/atom/' . $atomic_component . '.html.twig';
} elseif (file_exists($this->projectDir . $folder . '/molecule/' . $atomic_component . '.html.twig')) {
$view = 'platform/component/molecule/' . $atomic_component . '.html.twig';
} elseif (file_exists($this->projectDir . $folder . '/organism/' . $atomic_component . '.html.twig')) {
$view = 'platform/component/organism/' . $atomic_component . '.html.twig';
} else {
return $atomic_component . ' not found !';
}
return $this->twig->render($view);
}
/**
* @param $component
* @param $keys
* @param $index
*
* @return mixed
*/
private function checkComponent($component, &$keys, $index)
{
$platform = $this->params->get('platform');
$value = $platform;
$keys = explode('.', $component);
$i = 1;
foreach ($keys as $key) {
if (!isset($value[ $key ]) &&
!isset($value[ 'global' ][ $key ]) &&
!isset($value[ 'front' ][ $key ]) &&
!isset($value[ 'back_office' ][ $key ])) {
// On affiche l'erreur de key non trouvée que pour les développeurs.
// En prod et pour les autres utilisateurs, on n'affiche rien (une erreur log est générée néanmoins).
/** @var User $currentUser */
$currentUser = $this->security->getUser();
if ($currentUser !== NULL && $currentUser->isDeveloper()) {
return "key '$key' of '$component' does not exist";
}
$this->logger->error("key '$key' of '$component' does not exist");
return '';
}
if (isset($value[ 'global' ][ $key ])) {
$value = $value[ 'global' ][ $key ];
} elseif (isset($value[ 'front' ][ $key ])) {
$value = $value[ 'front' ][ $key ];
} elseif (isset($value[ 'back_office' ][ $key ])) {
$value = $value[ 'back_office' ][ $key ];
} else {
$value = $value[ $key ];
}
// si un index est passé et qu'on est à la dernière clé, on va chercher l'objet à l'index donné.
if (NULL !== $index && $i === count($keys)) {
$value = $value[ $index ];
}
$i++;
}
return $value;
}
/**
* @throws SyntaxError
* @throws RuntimeError
* @throws LoaderError
*/
private function buildComponent($value, $keys, $data, $debug = FALSE): string
{
$response = '<div class="text-danger">' . $value[ 'type' ] . ' not found in components !</div>';
/** @var User $currentUser */
$currentUser = $this->security->getUser();
$componentAclFullKey = is_array($keys) ? implode('.', $keys) : $keys;
$folder = '/templates/platform/component';
if (!(isset($value[ 'disabled' ]) && $value[ 'disabled' ] === TRUE)) {
if (file_exists($this->projectDir . $folder . '/atom/' . $value[ 'type' ] . '.html.twig')) {
$view = 'platform/component/atom/' . $value[ 'type' ] . '.html.twig';
} elseif (file_exists($this->projectDir . $folder . '/molecule/' . $value[ 'type' ] . '.html.twig')) {
$view = 'platform/component/molecule/' . $value[ 'type' ] . '.html.twig';
} elseif (file_exists($this->projectDir . $folder . '/organism/' . $value[ 'type' ] . '.html.twig')) {
$view = 'platform/component/organism/' . $value[ 'type' ] . '.html.twig';
}
if (isset($view)) {
$response = $this->twig->render($view, [
'value' => $value,
'data' => $data,
'componentKey' => $componentAclFullKey,
]);
}
}
if (isset($view) && $currentUser !== NULL && $currentUser->isDeveloper()) {
$response = "\n<!-- ***** START component " . $value[ 'type' ] . " : " . $view . " ***** -->\n" .
$response .
"\n<!-- ***** END component " . $value[ 'type' ] . " ***** -->\n";
}
return $response;
}
/**
* @param string $page
* @param string $frontType
*
* @return array
* @throws JsonException
*/
private function getContentData(string $page, string $frontType): array
{
if (!in_array($frontType, ['security', 'common', 'content'])) {
$frontType = 'content';
}
// on regarde si on a des données en BDD pour cette page
$fromBdd = $this->frontService->getArrayDataFromSetting('front.' . $frontType . '.' . $page);
if ($fromBdd !== []) {
$pageArray = $fromBdd;
} else {
$platform = $this->params->get('platform');
$pageArray = $platform[ 'front' ][ $frontType ];
}
$testPage = explode('.', $page);
if (count($testPage) > 1) {
foreach ($testPage as $item) {
$pageArray = $pageArray[ $item ];
}
} else {
$pageArray = $pageArray[ $page ];
}
return [
'pageArray' => $pageArray,
'frontCat' => $frontType,
'contentKey' => $frontType . '.' . $page,
];
}
/**
* Génère les div d'ouverture lorsque content() est appelé
*
* TODO à revoir pour verrouiller
*
* container peut avoir plusieurs valeurs
* - TRUE => on ajoute une div class="container" au debut
* - container => on ajoute une div class="container" au debut
* - container-fluid => on ajoute une div class="container-fluid" au debut
* - fluid => on ajoute une div class="container-fluid" au debut
*
* si la clef row existe et n'est pas FALSE => on rajoute une div class="row" après le container
* 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
*
* @param array $pageArray
*
* @return array
*/
private function getContentStartDivs(array $pageArray): array
{
$divs = [];
// 3 cas possibles
// la clef n'existe pas ou est à false → pas de container
if (isset($pageArray[ 'container' ])) {
// si la clef est à true ou "container" → container
if (in_array(
$pageArray[ 'container' ],
[TRUE, 'container'],
TRUE
)) {
$divs[] = '<div class="container">';
// si la clef est à "fluid" ou "container-fluid" => container-fluid
} elseif (in_array(
$pageArray[ 'container' ],
['container-fluid', 'fluid']
)) {
$divs[] = '<div class="container-fluid">';
}
}
if (
isset($pageArray[ 'row' ])
&& $pageArray[ 'row' ] !== FALSE
) {
$row_justify = $pageArray[ 'row_justify' ] ?? '';
$divs[] = '<div class="row ' . $row_justify . '">';
}
return $divs;
}
private function getClassForItem($item)
{
$allClass = $item[ 'class' ] ?? '';
$allClassArray = explode(' ', $allClass);
$classCatArr = [];
if (isset($item[ 'class_category' ])) {
foreach ($item[ 'class_category' ] as $key => $value) {
$classCatArr[ $key ] = !is_array($value) ? explode(' ', $value) : $value;
}
}
$merged = array_merge($allClassArray, ...array_values($classCatArr));
$allClass = array_unique($merged);
return implode(' ', $allClass);
}
public function urlExist($url): bool
{
stream_context_set_default( [
'ssl' => [
'verify_peer' => false,
'verify_peer_name' => false,
],
]);
$headers = get_headers($url);
return (bool)stripos($headers[ 0 ], "200 OK");
}
}