source: spip-zone/_plugins_/prix/prix_fonctions.php

Last change on this file was 124351, checked in by tcharlss, 20 months ago

On rétablit le markup du prix (encapsulation dans un <span>), cette fois-ci ça marche pour de bon sur tous les prix testés, quelque soit la combinaison de format / langue / devise ou symbole. À l'intérieur on n'encapsule que la devise, car celle-ci peut parfois être au milieu du nombre : -AED123 par exemple. On ajoute aussi des attributs data pour avoir le nombre et le code alphabétique de la devise, utilise si on a besoin de manipuler les prix en js.
Un tout petit peu de ménage aussi : à la trappe la fonction qui permettait d'avoir des alias de certains paramètres (devise ou currency par ex.). Il y a un nom de paramètre possible et point barre.

File size: 14.8 KB
Line 
1<?php
2
3// Sécurité
4if (!defined('_ECRIRE_INC_VERSION')) {
5        return;
6}
7
8/**
9 * La balise qui va avec le prix TTC
10 *
11 * @param Object $p
12 * @return Float
13 */
14function balise_PRIX_dist($p) {
15        $b = $p->nom_boucle ? $p->nom_boucle : $p->descr['id_mere'];
16        if (!$_type = interprete_argument_balise(1,$p)){
17                $_type = sql_quote($p->boucles[$b]->type_requete);
18                $_id = champ_sql($p->boucles[$b]->primary,$p);
19        }
20        else
21                $_id = interprete_argument_balise(2,$p);
22        $connect = $p->boucles[$b]->sql_serveur;
23        $p->code = "prix_objet(intval(".$_id."),".$_type.','.sql_quote($connect).")";
24        $p->interdire_scripts = false;
25        return $p;
26}
27
28/**
29 * La balise qui va avec le prix HT
30 *
31 * @param Object $p
32 * @return Float
33 */
34function balise_PRIX_HT_dist($p) {
35        $b = $p->nom_boucle ? $p->nom_boucle : $p->descr['id_mere'];
36        if (!$_type = interprete_argument_balise(1,$p)){
37                $_type = sql_quote($p->boucles[$b]->type_requete);
38                $_id = champ_sql($p->boucles[$b]->primary,$p);
39        }
40        else
41                $_id = interprete_argument_balise(2,$p);
42        $connect = $p->boucles[$b]->sql_serveur;
43        $p->code = "prix_ht_objet(intval(".$_id."),".$_type.','.sql_quote($connect).")";
44        $p->interdire_scripts = false;
45        return $p;
46}
47
48/**
49 * Obtenir le prix TTC d'un objet
50 *
51 * @param Integer $id_objet
52 * @param String $type_objet
53 * @return Float
54 */
55function prix_objet($id_objet, $objet, $serveur = '') {
56        $fonction = charger_fonction('prix', 'inc/');
57        return $fonction($objet, $id_objet, array(), $serveur);
58}
59
60/**
61 * Obtenir le prix HT d'un objet
62 *
63 * @param Integer $id_objet
64 * @param String $type_objet
65 * @return Float
66 */
67function prix_ht_objet($id_objet, $objet) {
68        $fonction = charger_fonction('ht', 'inc/prix');
69        return $fonction($objet, $id_objet);
70}
71
72/**
73 * Compatibilité avec la balise #INFO_PRIX
74 *
75 * @uses prix_objet
76 *
77 * @param Integer $id_objet
78 * @param String $type_objet
79 * @param Array $ligne
80 * @return Float
81 */
82function generer_prix_entite($id_objet, $objet, $ligne) {
83        return prix_objet($id_objet, $objet);
84}
85
86/**
87 * Compatibilité avec la balise #INFO_PRIX_HT
88 *
89 * @uses prix_ht_objet
90 *
91 * @param Integer $id_objet
92 * @param String $type_objet
93 * @param Array $ligne
94 * @return Float
95 */
96function generer_prix_ht_entite($id_objet, $objet, $ligne) {
97        return prix_ht_objet($id_objet, $objet);
98}
99
100/**
101 * Formater un nombre pour l'afficher comme un prix selon une devise
102 *
103 * @note
104 * Fonction déportée dans la fonction surchargeable `filtre_prix_formater`.
105 *
106 * @uses filtres_prix_formater_dist
107 */
108function prix_formater($prix, $options = array()) {
109        $fonction_formater = charger_fonction('prix_formater', 'filtres/');
110        return $fonction_formater($prix, $options);
111}
112
113/**
114 * Formater un nombre pour l'afficher comme un prix.
115 *
116 * Le prix retourné respecte les règles d'affichages propres à chaque langue et devise :
117 * nombre de décimales, virgules et/ou points, emplacement de la devise, etc.
118 *
119 * L'option `currency_display` permet d'avoir un format spécifique aux factures.
120 * L'option `float_only` permet d'avoir le nombre flottant arrondi selon la devise.
121 *
122 * @note
123 * Nécessite soit l'extension bcmath, soit l'extension intl.
124 *
125 * @example prix_formater($prix, array('currency'=>'EUR', 'locale'=>'fr-CA'))
126 *
127 * @see https://github.com/commerceguys/intl/blob/master/src/Formatter/CurrencyFormatterInterface.php#L8
128 * @see https://www.php.net/manual/fr/numberformatter.formatcurrency.php
129 *
130 * @uses prix_devise_defaut
131 * @uses prix_locale_defaut
132 * @uses prix_devise_info
133 * @uses prix_langue_vers_locale
134 * @uses prix_filtrer_options_formater
135 * @uses prix_alias_options_formater
136 *
137 * @param float $prix
138 *     Valeur du prix à formater
139 * @param array $options
140 *     Tableau d'options :
141 *     - markup :                  (String|Boolean) pour encapsuler ou pas dans un <span>
142 *                                 Défaut: true
143 *     - class :                   (String) nom de la classe parente pour encapsuler
144 *                                 Défaut : montant
145 *     - currency :                (String) devise, code alphabétique à 3 lettres.
146 *                                 Défaut : celle configurée
147 *     - locale :                  (String) identifiant d'une locale (fr-CA) ou code de langue SPIP (fr_tu)
148 *     - style :                   (String) standard | accounting.
149 *                                 Défaut : standard
150 *     - use_grouping :            (Bool) grouper les séparateurs.
151 *                                 Défaut : true
152 *     - rounding_mode :           constante PHP_ROUND_ ou `none`.
153 *                                 Défaut : PHP_ROUND_HALF UP
154 *     - minimum_fraction_digits : (Int)
155 *                                 Défaut : fraction de la devise.
156 *     - maximum_fraction_digits : (Int)
157 *                                 Défaut : fraction de la devise.
158 *     - currency_display :        (String) symbol | code | none.
159 *                                 Défaut : symbol
160 * @return string|float
161 *     Retourne une chaine contenant le prix formaté avec une devise, encapsulée dans un <span>
162 */
163function filtres_prix_formater_dist($prix, $options = array()) {
164        prix_loader();
165
166        // S'assurer d'avoir un nombre flottant
167        $prix = floatval(str_replace(array(',', ' '), array('.', ''), $prix));
168
169        // Devise à utiliser
170        $devise = (!empty($options['currency']) ? $options['currency'] : prix_devise_defaut());
171
172        // Locale à utiliser
173        $locale = (!empty($options['locale']) ? $options['locale'] : prix_locale_defaut());
174        $locale = prix_langue_vers_locale($locale);
175
176        // Options (celles propres au formatter + diverses)
177        $options_defaut = array(
178                'locale'           => $locale,
179                'currency_display' => 'code', // pour l'accessibilité
180                'markup'           => true, // encapsuler
181        );
182        $options = array_merge($options_defaut, is_array($options) ? $options : array());
183
184        // 1) De préférence, on utilise la librairie Intl de Commerceguys
185        if (extension_loaded('bcmath')) {
186
187                // Éviter les exceptions invalid argument.
188                $options_formatter = prix_normaliser_options_formatter($options);
189                $numberFormatRepository = new CommerceGuys\Intl\NumberFormat\NumberFormatRepository;
190                $currencyRepository = new CommerceGuys\Intl\Currency\CurrencyRepository;
191                $currencyFormatter = new CommerceGuys\Intl\Formatter\CurrencyFormatter($numberFormatRepository, $currencyRepository);
192                $prix_formate = $currencyFormatter->format($prix, $devise, $options_formatter);
193
194        // 2) Sinon on se rabat sur la librairie Intl PECL
195        } elseif (extension_loaded('intl')) {
196                $currencyFormatter = new NumberFormatter($locale, NumberFormatter::CURRENCY);
197                $prix_formate = $currencyFormatter->formatCurrency($prix, $devise);
198
199        // 3) En dernier recours on fait le formatage du pauvre
200        } else {
201                $prix_nombre = str_replace('.', ',', $prix);
202                $prix_formate = $prix_nombre . '&nbsp;' . $devise;
203        }
204
205        // Enfin, on encapsule le tout
206        if (!empty($options['markup'])) {
207                $classe = (!empty($options['class']) ? $options['class'] : null);
208                // S'assurer que le montant soit arrondi selon la devise, même si c'est en principe fait en amont
209                $arrondi_devise = intval(prix_devise_info($devise, 'fraction'));
210                $montant = round($prix, $arrondi_devise);
211                $prix_formate = prix_ajouter_markup($prix_formate, $montant, $devise, $classe);
212        }
213
214        return $prix_formate;
215}
216
217/**
218 * Encapsule un prix dans des <span> en isolant la devise, en mode BEM.
219 *
220 * @note
221 * On n'encapsule pas le nombre dans un span car la devise peut être incluse dedans,
222 * ex. : -AED123
223 *
224 * @example
225 *     (sans retour ligne, là c'est pour lisibilité)
226 *     ````
227 *     <span class="montant" data-montant-nombre="3.14" data-montant-devise="EUR">
228 *       3,14 <span class="montant__devise">EUR</span>
229 *     </span>
230 *    ````
231 *
232 * @param string $prix_formate
233 *     Prix formaté avec éventuellement la devise ou le symbole
234 * @param float $montant
235 *     Prix sans formatage
236 * @param string $devise
237 *     Code alphabétique à 3 lettres
238 * @param string $classe
239 *     Classe parente à utiliser, défaut = 'montant'
240 * @return string
241 */
242function prix_ajouter_markup($prix_formate, $montant, $devise, $classe = '') {
243
244        // Les classes BEM
245        $classe = (!empty($classe) ? $classe : 'montant');
246        $classe_devise = "${classe}__devise";
247
248        // Markupisons
249        $cherche_devise = '/[^\d\-\.\,\(\)\s x{00a0}]+/u';
250        $remplace_devise = "<span class=\"$classe_devise\">$0</span>";
251        $prix_formate = preg_replace($cherche_devise, $remplace_devise, $prix_formate);
252        $prix_formate = "<span class=\"$classe\" data-montant-nombre=\"$montant\" data-montant-devise=\"$devise\">" . $prix_formate . '</span>';
253
254        return $prix_formate;
255}
256
257/**
258 * Liste les devises et les informations associées
259 *
260 * @uses prix_devise_info()
261 *
262 * @return Array
263 *     Tableau associatif avec les codes alphabétiques en clés et les infos en sous-tableaux
264 */
265function prix_lister_devises() {
266
267        prix_loader();
268        $devises = array();
269
270        $currencyRepository = new CommerceGuys\Intl\Currency\CurrencyRepository;
271        $codes_devises = $currencyRepository->getList();
272
273        foreach ($codes_devises as $code => $nom) {
274                $devises[$code] = prix_devise_info($code);
275        }
276
277        return $devises;
278}
279
280/**
281 * Liste les langues avec leur identifiant de locale.
282 *
283 * @see https://www.php.net/manual/fr/class.locale.php
284 *
285 * @return Array
286 *     Tableau associatif : locale => nom
287 */
288function prix_lister_langues() {
289
290        prix_loader();
291        $langues = array();
292
293        $languageRepository = new CommerceGuys\Intl\Language\LanguageRepository;
294        $repo_locales = $languageRepository->getlist();
295
296        // Prendre la langue du visiteur pour les noms
297        $langue_spip = $GLOBALS['spip_lang'];
298        $locale_visiteur = prix_langue_vers_locale($langue_spip);
299
300        foreach ($repo_locales as $locale => $nom) {
301                $language = $languageRepository->get($locale, $locale_visiteur);
302                $langues[$locale] = $language->getName();
303        }
304
305        return $langues;
306}
307
308/**
309 * Renvoie une ou toutes les infos sur une devise
310 *
311 * @param string $code
312 *    Code alphabétique à 3 lettres de la devise
313 * @param string $info
314 *    Info précise éventuelle :
315 *    - nom : nom de la devise
316 *    - code : code alphabétique (remis au cas où)
317 *    - code_num : code numérique
318 *    - symbole : symbole associé
319 *    - fraction : fraction pour passer à l'unité inférieure (centimes et cie)
320 *    - langue : code de langue utilisée
321 * @return string|array
322 */
323function prix_devise_info($code, $info = '') {
324        prix_loader();
325
326        // Langue du visiteur pour les noms
327        $langue_spip = $GLOBALS['spip_lang'];
328        $locale_visiteur = prix_langue_vers_locale($langue_spip);
329
330        $currencyRepository = new CommerceGuys\Intl\Currency\CurrencyRepository;
331        $devise = $currencyRepository->get($code, $locale_visiteur);
332        $infos = array(
333                'code'     => $code,
334                'code_num' => $devise->getNumericCode(),
335                'nom'      => $devise->getName(),
336                'fraction' => $devise->getFractionDigits(),
337                'symbole'  => $devise->getSymbol(),
338                'locale'   => $devise->getLocale(),
339        );
340
341        $retour = (isset($infos[$info]) ? $infos[$info] : $infos);
342
343        return $retour;
344}
345
346/**
347 * Retourne la devise par défaut.
348 *
349 * Celle configurée, sinon des euros
350 *
351 * @return String
352 *     Code alphabétique à 3 lettres
353 */
354function prix_devise_defaut() {
355
356        include_spip('inc/config');
357
358        // Par défaut celle configurée
359        if ($devise_config = lire_config('prix/devise_defaut')) {
360                $devise = $devise_config;
361        // Sinon des euros
362        } else {
363                $devise = 'EUR';
364        }
365
366        return $devise;
367}
368
369/**
370 * Retourne la locale d'après la langue du contexte
371 *
372 * @return String
373 *      Identifiant de la locale
374 */
375function prix_locale_defaut() {
376
377        include_spip('inc/config');
378
379        $langue_spip = $GLOBALS['spip_lang'];
380        $locales_config = lire_config('prix/locales', array());
381
382        // Normalement l'admin a configuré la locale correspondante à chaque code langue de spip.
383        // Sinon tant pis, on donne juste le code pays tiré du code langue de spip.
384        $locale = $locales_config[$langue_spip] ?: prix_langue_vers_locale($langue_spip);
385
386        return $locale;
387}
388
389/**
390 * Retourne une locale reconnue par Intl.
391 *
392 * Si c'est un code langue de spip, on ne garde que le code du pays (norme ISO 639).
393 *
394 * @see https://github.com/commerceguys/intl/blob/master/src/Language/LanguageRepository.php#L46
395 * @see https://blog.smellup.net/106
396 *
397 * @param string $code_langue
398 * @return string
399 */
400function prix_langue_vers_locale($code_langue) {
401
402        include_spip('inc/config');
403        $locale = $code_langue;
404        $is_langue_spip = in_array($code_langue, explode(',', lire_config('langues_proposees')));
405
406        if ($is_langue_spip) {
407                // Extraire le code pays pour avoir la locale "générale" : fr_tu → fr
408                $locale = strtolower(strtok($code_langue, '_'));
409
410                // Exceptions : certains codes pays des langues de spip ne font pas partie de la liste des locales.
411                // On fait une correspondance manuellement en prenant la locale la plus proche.
412                // (ça n'indique pas que ce sont des langues identiques, mais suffisamment proches pour le formatage des prix)
413                $exceptions = array(
414                        'oc'  => 'fr', // occitan
415                        'ay'  => 'ayr', // aymara
416                        'co'  => 'fr', // corse
417                        'cpf' => 'fr', // créole et pidgins (rcf)
418                        'fon' => '', // fongbè
419                        'roa' => 'pdc', // langues romanes
420                );
421                if (!empty($exceptions[$locale])) {
422                        $locale = $exceptions[$locale];
423                }
424        }
425
426        return $locale;
427}
428
429/**
430 * Fonction privée pour filtrer le tableau d'options du formatter
431 *
432 * Retire les options inconnues et typecaste les valeurs pour éviter les exceptions invalid argument.
433 *
434 * Le tableau d'options peut être issu d'un squelette,
435 * et dans ce cas par défaut les valeurs sont des chaînes de texte à défaut de |filtre ou de #EVAL.
436 *
437 * @param array $valeurs
438 * @return array
439 */
440function prix_normaliser_options_formatter($options) {
441        $options_valides = array(
442                'locale',
443                'style',
444                'use_grouping',
445                'rounding_mode',
446                'minimum_fraction_digits',
447                'maximum_fraction_digits',
448                'currency_display',
449        );
450        if (is_array($options)) {
451                foreach ($options as $k => $v) {
452                        // option inconnue, chaine vide ou null : on retire la valeur
453                        if (!in_array($k, $options_valides) or is_null($v) or $v == '') {
454                                unset($options[$k]);
455                                // nombre flottant / entier
456                        } elseif (is_numeric($v)) {
457                                if (intval($v) == $v) {
458                                        $options[$k] = intval($v);
459                                } else {
460                                        $options[$k] = floatval($v);
461                                }
462                        // booléens
463                        } elseif (in_array($v, array('true', 'oui'))) {
464                                $options[$k] = true;
465                        } elseif (in_array($v, array('false', 'non'))) {
466                                $options[$k] = false;
467                        }
468                }
469        }
470        return $options;
471}
472
473/**
474 * Autoloader
475 * @throws Exception
476 */
477function prix_loader() {
478        static $done = false;
479        if (!$done) {
480                $done = true;
481                require_once __DIR__ . '/vendor/autoload.php';
482        }
483}
Note: See TracBrowser for help on using the repository browser.