Source of file FormatExtension.php

Size: 17,079 Bytes - Last Modified: 2023-11-16T22:56:02+01:00

/home/websites/teicee/packagist/site/phpdoc/conf/../vendor/teicee/twig-bundle/src/Extension/FormatExtension.php

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435
<?php
namespace TIC\TwigBundle\Extension;

use TIC\TwigBundle\Base\TICTwigExtension as BaseExtension;
use TIC\CoreBundle\Util\ConvertHelper;
use TIC\CoreBundle\Util\StringHelper;

use Symfony\Contracts\Translation\TranslatorInterface;
use Twig\TwigFilter;
use Twig\TwigFunction;
use Twig\Environment;

/**
 * Filtres et fonctions twig de formattage et conversions.
 * https://symfony.com/doc/current/templating/twig_extension.html
 */
class FormatExtension extends BaseExtension
{

	public function getFilters(): array
	{
		return [
			new TwigFilter('boolval',   [$this, 'boolvalFilter'],     ['is_safe'=>['html']] ),
			new TwigFilter('boolean',   [$this, 'booleanFilter'],     ['is_safe'=>['html']] ),
			new TwigFilter('counter',   [$this, 'counterFilter'],                           ),
			new TwigFilter('jsvalue',   [$this, 'jsvalueFilter'],     ['is_safe'=>['html']] ),
			new TwigFilter('stringify', [$this, 'stringifyFilter'],   ['is_safe'=>['html']] ),
			new TwigFilter('slugify',   [$this, 'slugifyFilter']                            ),
			new TwigFilter('asciify',   [$this, 'asciifyFilter']                            ),
			new TwigFilter('obfuscate', [$this, 'obfuscateFilter'],   ['is_safe'=>['html']] ),
			new TwigFilter('pathEncode',[$this, 'pathEncodeFilter']                         ),
			new TwigFilter('linkurl',   [$this, 'linkurlFilter'],     ['is_safe'=>['html']] ),
			new TwigFilter('mailto',    [$this, 'mailtoFilter'],      ['is_safe'=>['html']] ),
			new TwigFilter('phone',     [$this, 'phoneFilter'],       ['is_safe'=>['html']] ),
			new TwigFilter('price',     [$this, 'priceFilter'],       ['is_safe'=>['html']] ),
			new TwigFilter('euro',      [$this, 'euroFilter'],        ['is_safe'=>['html']] ),
			new TwigFilter('hsize',     [$this, 'hsizeFilter'],       ['is_safe'=>['html']] ),
			new TwigFilter('color',     [$this, 'colorFilter'],       ['is_safe'=>['html']] ),
			new TwigFilter('civ',       [$this, 'civFilter'],         ['is_safe'=>['html']] ),
			new TwigFilter('roles',     [$this, 'rolesFilter'],       ['is_safe'=>['html']] ),
#			new TwigFilter('stars',     [$this, 'starsFilter'],       ['is_safe'=>['html'], 'needs_environment'=>true] ),
			new TwigFilter('icon',      [$this, 'iconFilter'],        ['is_safe'=>['html']] ),
			new TwigFilter('lnum',      [$this, 'lnumFilter']                               ),
			new TwigFilter('numStep',   [$this, 'numStepFilter']                            ),
			new TwigFilter('labelize',  [$this, 'labelizeFilter'],    ['is_safe'=>['html']] ),
			new TwigFilter('ereplace',  [$this, 'eReplaceFilter'],    ['is_safe'=>['html']] ),
			new TwigFilter('transList', [$this, 'transListFilter']                          ),
		];
	}

	public function getFunctions(): array
	{
		return [
#			new TwigFunction('example', [$this, 'exampleFunction'] ),
		];
	}

	public function __construct(TranslatorInterface $translator = null)
	{
		$this->translator = $translator;
	}


	/**
	 *
	 */
	public function boolvalFilter($value, $true_val, $false_val = "")
	{
		if (! \is_bool($value)) return $value;
		return ($value) ? $true_val : $false_val;
	}

	/**
	 * Retourne le libellé d'un booléen (ou null) correspondant à la variable (token traduisible).
	 *
	 * @param  mixed    $value      Variable à interpréter pour retourner un état booléen (true/false/null)
	 * @param  string   $default    Libellé à retourner si aucune correspondance booléenne trouvée pour $value
	 * @param  bool     $trans      Recherche d'une traduction pour le token $value (si c'est une chaine)
	 * @return string               Libellé d'un booléen (token traduisible ou déjà traduit)
	 */
	public function booleanFilter($value, string $default = '-', bool $trans = false): string
	{
		$bool = $this->getBool($value, $default);
		if     ($bool === null)  $text = self::$strings['null'];
		elseif ($bool === true)  $text = self::$strings['true'];
		elseif ($bool === false) $text = self::$strings['false'];
		return ($trans) ? $this->trans($text) : $text;
	}

	/**
	 * Retourne une quantité à partir d'une variable de type quelconque (intval, floatval, count...).
	 * @TODO: ajouter des options pour la présentation des nombres (number_format)
	 *
	 * @param  mixed    $value      Valeur dont une quantité est à déterminer
	 * @return int|float            Valeur numérique (ou null si indéterminé)
	 */
	public function counterFilter($value)
	{
		return ConvertHelper::counter($value);
	}

	/**
	 * Transformation d'une variable PHP quelque soit son type pour son affichage en Javascript (avec échappements).
	 */
	public function jsvalueFilter($value): string
	{
		switch (true) {
			case \is_null($value)   : return 'null';
			case \is_bool($value)   : return ($value) ? 'true' : 'false';
			case \is_string($value) : return '"' . StringHelper::jsescape($value) . '"';
			case \is_array($value)  : return json_encode($value);
			case \is_object($value) : return json_encode($value);
			default                : return $value;
		}
	}

	/**
	 * Transformation d'une variable PHP quelque soit son type dans sa forme correspondante en chaine de caractères.
	 */
	public function stringifyFilter($value)
	{
		return ConvertHelper::stringify($value);
	}

	/**
	 * Retourne une chaine ASCII en convertissant les caractères étendus.
	 *
	 * @param  string  $text  Texte en UTF-8 à transformer
	 * @return string         Chaine équivalente avec les caractères 7 bits
	 */
	public function asciifyFilter(string $text): string
	{
		return StringHelper::asciify($text);
	}

	/**
	 * Retourne une chaine canonisée à partir d'une chaine donnée.
	 *
	 * @param  string  $text  Texte en UTF-8 à slugifier
	 * @return string         Slug avec uniquement les caractères [a-z], [0-9], '.', '_' et '-'
	 */
	public function slugifyFilter(string $text): string
	{
		return StringHelper::slugify($text);
	}

	/**
	 * Transformation d'une chaine de caractère en code Javascript son camouflage dans une page HTML (utile pour emails).
	 */
	public function obfuscateFilter($string, $delta = null)
	{
		if ($delta === null) $delta = \rand(2, 22);
		$value = ''; foreach (\str_split($string) as $c) $value.= \chr(\ord($c) + $delta);
		return \sprintf('<script type="text/javascript">document.write(atob("%s").replace(/./g, function(c){ return String.fromCharCode(c.charCodeAt(0) - %d); }))</script>',
			\base64_encode($value), $delta
		);
	}

	/**
	 * Encodage pour échappements de chaques parties du chemin d'une URL (en conservant les '/').
	 */
	public function pathEncodeFilter($url)
	{
		if (! \is_string($url)) return $url;
		return \implode('/', \array_map('rawurlencode', \explode('/', $url)));
	}

	/**
	 * Affichage d'un lien HTML avec son libellé cliquable pour une URL.
	 */
	public function linkurlFilter(?string $url, ?string $label = null): ?string
	{
		if (null === $label) $label = $url;
		if (empty($url)) return $label;
		return \sprintf('<a href="%s" target="_blank">%s</a>', \htmlspecialchars($url), \htmlspecialchars($label));
	}

	/**
	 * Affichage d'un lien HTML avec son libellé cliquable pour une (ou plusieurs) adresse(s) email.
	 * 
	 * @param   string|array    $emails   Adresse(s) email(s) à afficher (séparateur virgule sur chaine)
	 * @param   string|bool     $label    Texte à afficher sur le lien, null pour laisser la valeur source
	 *                                    ou booléen pour valeur formattée (true=complet / false=adresse seul)
	 * @param   string          $glue     Séparateur pour la concaténation des adresses retournées (json_encode si null)
	 * @return  string                    Chaine HTML avec la (ou les) adresse(s) avec lien mailto: (si adresse reconnue)
	 */
	public function mailtoFilter(mixed $emails, mixed $label = false, ?string $glue = ', '): ?string
	{
		if ($emails === null) return null;
		if (! \is_array($emails)) $emails = \explode(',', $emails);
		
		$html = array();
		foreach ($emails as $email) {
			if ($email === null) continue;
			$email = \trim($email);
			if ($email === '') continue;
			
			if (\preg_match('/^(.*[\s<])?([^@<>\s"\']+@[^@<>\s\'"]+)[^@]*$/', $email, $match)) {
				$addr = $match[2];
				$full = \trim($match[1], " \n\r\t\v\x00\"<>");
				$full = \strlen($full) ? \sprintf('"%s" <%s>', $full, $addr) : $addr;
				$text = \is_string($label) ? $label : $email;
				if     ($label === true)  $text = $full;
				elseif ($label === false) $text = $addr;
				$html[] = \sprintf('<span class="mailto %4$s"><a href="mailto:%1$s" title="%2$s">%3$s</a></span>',
					\htmlspecialchars($full),
					\htmlspecialchars($this->trans('widget.mailto.link_title') . $addr),
					\htmlspecialchars($text),
					($text === $addr) ? "mailaddr" : "mailtext"
				);
			} else $html[] = \htmlspecialchars($email);
		}
		return ($glue === null) ? \json_encode($html) : \implode($glue, $html);
	}

	/**
	 * Formattage d'un numéro de téléphone pour affichage.
	 *
	 * @param  mixed    $value      Variable à formatter comme numéro de téléphone (liste possible)
	 * @param  string   $default    Libellé à retourner si la valeur est vide
	 * @param  bool     $html       Utilisation d'entités HTML dans le formatage (activé par défaut !)
	 * @param  mixed    $inter      Préfixe international à ajouter (aucun par défaut, "+33" si true)
	 * @return string               Numéro(s) de téléphone formattés (séparés par "\n" si plusieurs)
	 */
	public function phoneFilter($value, string $default='&mdash;', bool $html = true, $inter = ''): string
	{
		if ($inter === true) $inter = "+33";
		if (empty($html) && ($default == '&mdash;')) $default = '-';
		
		$phones = array();
		$values = \is_array($value) ? $value : array($value);
		foreach ($values as $value) {
			$phone = \trim($value);
			
			if (empty($phone)) $phone = $default;
			elseif (\preg_match('/^(\(?\d?\)?\d)[\s\.\-]*(\d\d)[\s\.\-]*(\d\d)[\s\.\-]*(\d\d)[\s\.\-]*(\d{2,8})$/', $phone, $m)) {
				// (0)1 23 45 67 89		01-23-45-67-89		1 23 45 67 89		0123456789		...
				$phone = \trim(\sprintf('%s %02d %02d %02d %02d %02d', $inter, $m[1], $m[2], $m[3], $m[4], $m[5]));
			}
			elseif (\preg_match('/^(\+\d{1,3})[\s\.\-]+(\(?\d?\)?\d)[\s\.\-]*(\d\d)[\s\.\-]*(\d\d)[\s\.\-]*(\d\d)[\s\.\-]*(\d{2,8})$/', $phone, $m)) {
				// +33 (0)1 23 45 67 89		+33-01-23-45-67-89		+33 1 23 45 67 89		...
				$phone = \trim(\sprintf('%s %02d %02d %02d %02d %02d', ($inter===false)?"":$m[1], $m[2], $m[3], $m[4], $m[5], $m[6]));
			}
			elseif (\preg_match('/^(\+\d{1,3})[\s\.\-]*(\(?\d\)?\d)[\s\.\-]*(\d\d)[\s\.\-]*(\d\d)[\s\.\-]*(\d\d)[\s\.\-]*(\d{2,8})$/', $phone, $m)) {
				// +330123456789		...
				$phone = \trim(\sprintf('%s %02d %02d %02d %02d %02d', ($inter===false)?"":$m[1], $m[2], $m[3], $m[4], $m[5], $m[6]));
			}
			$phones[] = ($html) ? \str_replace(' ', '&nbsp;', $phone) : $phone;
		}
		return \implode("\n", $phones);
	}

	/**
	 * Formattage d'un prix pour affichage
	 */
	public function priceFilter($number, $default = null, string $devise = '&nbsp;&euro;', string $space ='&nbsp;'): ?string
	{
#		$locale = \setlocale(LC_MONETARY, 'fr_FR');
#		return \money_format('%.2n', $number);
		if (\is_string($number)) $number = \floatval(\strtr($number, ',', '.'));
		if (($default !== null) && (\abs($number) < 0.001)) return $default;
		// NOTE: number_format applique un PHP_ROUND_HALF_UP si besoin
		return \number_format($number, 2, ',', $space) . $devise;
	}

	/**
	 * Formattage d'un prix pour affichage en euros.
	 */
	public function euroFilter($number, $default = null)
	{
		return $this->priceFilter($number, $default, ' €', '');
	}

	/**
	 * Formattage d'un quantité d'octets pour affichage "pour humain" (avec l'unité la plus proche).
	 *
	 * @param  integer  $bytes  Valeur en octets
	 * @param  array    $units  Liste des unités (liste par défaut si non array)
	 * @param  integer  $prec   Précision (nombre de décimales)
	 * @param  integer  $base   Calculs en base 2 ou en base 10
	 * @return string
	 */
	public function hsizeFilter($bytes, $units = null, int $prec = 1, int $base = 2)
	{
		return ConvertHelper::hsize($bytes, $units, $prec, $base);
	}

	/**
	 * Formattage d'un aperçu en HTML d'une couleur (avec ou sans son code).
	 */
	public function colorFilter($color, $with_label = false)
	{
		if (empty($color)) return '';
		if ($with_label === true) $with_label = 'right';
		$html = \sprintf('<span class="tic-color" style="background-color: %s;">&nbsp;', $color);
		switch (\trim(\strtolower($with_label))) {
			case 'left'   : $html = \htmlspecialchars($color) . '</span>&nbsp;' . $html . '</span>'; break;
			case 'right'  : $html.= '</span>&nbsp;' . \htmlspecialchars($color); break;
			case 'inside' : $html.= \htmlspecialchars($color) . '&nbsp;</span>'; break;
			default       : $html.= '</span>'; break;
		}
		return $html;
	}

	/**
	 * Formattage d'une civilité dans sa version traduite à partir de son code interne.
	 */
	public function civFilter($value, $abbr = false, $trans = true)
	{
		if ($value === null || $value === '') return '';
		if (\strpos($value, '.') !== FALSE) $value = \str_replace('.', '', $value);
		$text = \sprintf('ext.%s.%s', ($abbr)?'civ':'civilite', \strtolower($value));
		return ($trans) ? $this->trans($text) : $text;
	}

	/**
	 * Formattage du (ou des) rôle(s) d'un utilisateur en utilisant les traductions.
	 */
	public function rolesFilter($roles, $join = false, $trans = true)
	{
		if ($roles === null || empty($roles)) return ($join === false) ? array() : "";
		if (! \is_array($roles)) $roles = \explode(',', $roles);
		\sort($roles);
		
		$labels = array();
		foreach ($roles as $role) {
			if ($role === 'ROLE_USER') continue;
			$label = 'app.user.' . \strtolower(\str_replace('ROLE_', 'roles.', $role));
			$labels[] = ($trans) ? $this->trans($label) : $label;
		}
		
		if ($join === false) return $labels;
		if ($join === true) $join = ", ";
		return \implode($join, $labels);
	}

	/**
	 *
	public function starsFilter(\Twig_Environment $env, $value, $symbol = null)
	{
		if (empty($symbol)) {
			$asset = $env->getFunction('asset')->getCallable();
			$symbol = \call_user_func($asset, 'bundles/ticcore/images/bootstrap-star-rating/star.png');
		}
		$html = '<sup class="tic_stars" title="' . $value . '">';
		for ($n = 0; $n < $value; $n++) {
			$html.= \sprintf('<img src="%s" alt="*" />', $symbol);
		}
		$html.= '</sup>';
		return $html;
	}
	 */

	/**
	 * Génération d'un tag HTML pour afficher un icone FontAwesome ou Bootstrap (avec quelques alias utiles).
	 *
	 * @param string    $name       Classe Bootstrap ou FontAwesome (ex 'fas-user', 'fab-user', 'envelope', 'ban-circle'...)
	 * @param string    $state      Ajout d'une classe de type 'text-<state>' (ex: primary, info, success, warning, danger)
	 */
	public function iconFilter(string $name, string $state=''): string
	{
		return $this->getIcon($name, $state='');
	}

	/**
	 * Formattage d'un nombre (selon les règles d'affichage françaises).
	 */
	public function lnumFilter($number, $precision = null)
	{
		return \number_format((float)$number, $precision, ',', ' ');
#		return \sprintf('%.0'.$precision.'f', $number);
	}

	/**
	 * Validation/transformation d'un nombre pour respecter un "pas" (multiplieur).
	 */
	public function numStepFilter($number, $step = 5, $default = '')
	{
		if (! \is_numeric($number)) return $default;
		return \intval(\round($number / $step) * $step);
	}

	/**
	 * Génération automatique d'un libellé (token de traduction) à partir d'un champ de formulaire.
	 */
	public function labelizeFilter($view_or_data, $type = 'label')
	{
		$data = $view_or_data;
		if (\is_object($view_or_data)) {
			$data = array(
				'path' => $view_or_data->parent->vars['id'],
				'name' => $view_or_data->vars['name'],
			);
			if (isset($view_or_data->parent->vars['label_path'])) {
				$data['path'] = $view_or_data->parent->vars['label_path'];
			}
			if (isset($view_or_data->parent->parent->parent->parent->vars)
			 && $view_or_data->parent->parent->parent->vars['name'] == 'translations') {
				$data['path'] = $view_or_data->parent->parent->parent->parent->vars['id'];
			}
		}
		$path = \preg_replace('/__([a-zA-Z\-]+)\d\d\d__/', '__$1__', $data['path']);
		$parent = \strtr($path . '$', array(
			'__name__' => '',
#			'_form_'   => '.',
			'_form_'   => '.' . $type . '-',
#			'_form$'   => '.',
			'_form$'   => '.' . $type . '.',
			'$'        => '.',
			'_'        => '.',
		));
#		return preg_replace('/\.\d*\./', '.', $parent) . $type . '.' $data['name'];
		if (! \preg_match('/\.' . $type . '[\.\-]/', $parent)) $parent.= $type . ".";
		return \preg_replace('/\.\d*\./', '.', $parent) . $data['name'];
	}

	/**
	 * Application d'une RegExp de substitution.
	 */
	public function eReplaceFilter(string $subject, string $pattern, string $replacement): string
	{
		return \preg_replace($pattern, $replacement, $subject);
	}

	/**
	 * Application du filtre de traduction (trans) sur une liste de valeurs.
	 */
	public function transListFilter(array $values): array
	{
		\array_walk($values, function(&$text) { $text = $this->trans($text); });
		return $values;
	}

}