614 lines
		
	
	
		
			21 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
			
		
		
	
	
			614 lines
		
	
	
		
			21 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
| <?php
 | |
| namespace nur;
 | |
| 
 | |
| use nur\b\ValueException;
 | |
| use nur\ref\ref_type;
 | |
| use Traversable;
 | |
| 
 | |
| /**
 | |
|  * Class md: gestion de métadonnées. cette classe est une version simplifiée de
 | |
|  * {@link Metadata}
 | |
|  */
 | |
| class md {
 | |
|   /**
 | |
|    * fonction de support pour tester si une définition de type désigne une valeur
 | |
|    * nullable. retourner true si la valeur est nullable, false sinon.
 | |
|    * $type est mis à jour pour enlever le marqueur "?" le cas échéant
 | |
|    */
 | |
|   static final function check_nullable(&$type): bool {
 | |
|     if (is_string($type) && substr($type, 0, 1) === "?") {
 | |
|       $type = substr($type, 1);
 | |
|       return true;
 | |
|     } else {
 | |
|       return $type === null;
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * @var array méta-schéma, i.e schéma d'un champ de schéma
 | |
|    *
 | |
|    * A propos des types: en temps normal, quand il s'agit de valider les valeurs
 | |
|    * d'un objet par rapport à un schéma, les valeurs suivantes sont spéciales:
 | |
|    * - false: indiquer que le champ n'est pas spécifié, il obtient donc la
 | |
|    * valeur par défaut
 | |
|    * - null: indiquer que la champ a une valeur et que cette valeur est null
 | |
|    *
 | |
|    * Cependant, certains types modifient ce comportement:
 | |
|    * - bool: la valeur false n'a pas de signification particulière. la valeur
 | |
|    * null indique que le champ n'est pas spécifié et doit recevoir la valeur
 | |
|    * par défaut
 | |
|    * - mixed|?bool: les valeurs false et null n'ont pas de signification
 | |
|    * particulières elles sont donc reprises telles quelles
 | |
|    * - array|?array: la valeur est un tableau. le schéma de la valeur est
 | |
|    * pris en compte s'il est spécifié
 | |
|    * - array[]|?array[]: la valeur est une *liste* de tableaux. le schéma de
 | |
|    * chacun des éléments du tabvleau est pris en compte s'il est spécifié.
 | |
|    *
 | |
|    * Pour les types bool, int, double, float, string, array (et les variantes
 | |
|    * ?type qui autorisent la valeur null), la valeur est transformée dans le
 | |
|    * type demandé
 | |
|    */
 | |
|   const MSFIELD_SCHEMA = [
 | |
|     "type" => [
 | |
|       "type" => "mixed",
 | |
|       "default" => null,
 | |
|       "title" => "type PHP de la valeur",
 | |
|       "required" => false,
 | |
|       "key" => "type",
 | |
|       "header" => "type",
 | |
|       "desc" => null,
 | |
|       "schema" => null,
 | |
|       "composite" => false,
 | |
|     ],
 | |
|     "default" => [
 | |
|       "type" => "mixed",
 | |
|       "default" => null,
 | |
|       "title" => "valeur par défaut du champ",
 | |
|       "required" => false,
 | |
|       "key" => "default",
 | |
|       "header" => "default",
 | |
|       "desc" => null,
 | |
|       "schema" => null,
 | |
|       "composite" => false,
 | |
|     ],
 | |
|     "title" => [
 | |
|       "type" => "?string",
 | |
|       "default" => null,
 | |
|       "title" => "libellé de la valeur utilisé par exemple dans un formulaire",
 | |
|       "required" => false,
 | |
|       "key" => "title",
 | |
|       "header" => "title",
 | |
|       "desc" => null,
 | |
|       "schema" => null,
 | |
|       "composite" => false,
 | |
|     ],
 | |
|     "required" => [
 | |
|       "type" => "bool",
 | |
|       "default" => false,
 | |
|       "title" => "cette valeur est-elle requise?",
 | |
|       "required" => false,
 | |
|       "key" => "required",
 | |
|       "header" => "required",
 | |
|       "desc" => null,
 | |
|       "schema" => null,
 | |
|       "composite" => false,
 | |
|     ],
 | |
|     "key" => [
 | |
|       "type" => "?string",
 | |
|       "default" => null,
 | |
|       "title" => "nom de la clé",
 | |
|       "required" => false,
 | |
|       "key" => "key",
 | |
|       "header" => "key",
 | |
|       "desc" => null,
 | |
|       "schema" => null,
 | |
|       "composite" => false,
 | |
|     ],
 | |
|     "header" => [
 | |
|       "type" => "?string",
 | |
|       "default" => null,
 | |
|       "title" => "nom de l'en-tête s'il fallait présenter cette donnée dans un tableau",
 | |
|       "required" => false,
 | |
|       "key" => "header",
 | |
|       "header" => "header",
 | |
|       "desc" => null,
 | |
|       "schema" => null,
 | |
|       "composite" => false,
 | |
|     ],
 | |
|     "desc" => [
 | |
|       "type" => "?string",
 | |
|       "default" => null,
 | |
|       "title" => "description de la valeur",
 | |
|       "required" => false,
 | |
|       "key" => "desc",
 | |
|       "header" => "desc",
 | |
|       "desc" => null,
 | |
|       "schema" => null,
 | |
|       "composite" => false,
 | |
|     ],
 | |
|     "schema" => [
 | |
|       "type" => "?array",
 | |
|       "default" => null,
 | |
|       "title" => "schema de la donnée",
 | |
|       "required" => false,
 | |
|       "key" => "schema",
 | |
|       "header" => "schema",
 | |
|       "desc" => null,
 | |
|       "schema" => null,
 | |
|       "composite" => false,
 | |
|     ],
 | |
|     "composite" => [
 | |
|       "type" => "bool",
 | |
|       "default" => false,
 | |
|       "title" => "ce champ fait-il partie d'une valeur composite?",
 | |
|       "required" => false,
 | |
|       "key" => "composite",
 | |
|       "header" => "composite",
 | |
|       "desc" => null,
 | |
|       "schema" => null,
 | |
|       "composite" => false,
 | |
|     ],
 | |
|   ];
 | |
|   const MSFIELD_INDEXES = ["type" => 0, "default" => 1, "title" => 2, "required" => 3, "key" => 4, "header" => 5, "desc" => 6, "schema" => 7, "composite" => 8];
 | |
|   const ARRAY_TYPES = ["array", "?array", "array[]", "?array[]"];
 | |
|   const APPLY2ITEMS_TYPES = ["array[]", "?array[]"];
 | |
| 
 | |
|   static final function normalize_schema(array &$schema, bool $recursive=true): array {
 | |
|     foreach ($schema as $key => &$msfield) {
 | |
|       if ($key === "") continue;
 | |
|       self::_ensure_msfield($msfield, $key);
 | |
| 
 | |
|       $is_array_type = in_array($msfield["type"], self::ARRAY_TYPES);
 | |
|       if ($msfield["schema"] !== null) {
 | |
|         if (!$is_array_type) $msfield["schema"] = null;
 | |
|         elseif ($recursive) self::normalize_schema($msfield["schema"], $recursive);
 | |
|       }
 | |
|     }; unset($msfield);
 | |
|     return array_flip(array_keys($schema));
 | |
|   }
 | |
| 
 | |
|   /** s'assurer que $msfield est un champ de méta-schéma conforme */
 | |
|   static final function _ensure_msfield(&$msfield, $key=null): void {
 | |
|     if (!is_array($msfield)) {
 | |
|       $msfield = [
 | |
|         "type" => $msfield, "default" => null, "title" => $key, "required" => false,
 | |
|         "key" => $key, "header" => $key,
 | |
|         "desc" => null, "schema" => null, "composite" => false,
 | |
|       ];
 | |
|       return;
 | |
|     }
 | |
|     if ($key !== null) $msfield["key"] = $key;
 | |
|     self::_ensure_schema($msfield, self::MSFIELD_SCHEMA, self::MSFIELD_INDEXES, false);
 | |
|     if ($msfield["title"] === null) $msfield["title"] = $msfield["key"];
 | |
|     if ($msfield["header"] === null) $msfield["header"] = $msfield["title"];
 | |
|     if ($msfield["schema"] !== null) {
 | |
|       foreach ($msfield["schema"] as $key2 => &$msfield2) {
 | |
|         self::_ensure_msfield($msfield2, $key2);
 | |
|       }; unset($msfield2);
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * s'assurer que
 | |
|    * - $sfield est un tableau
 | |
|    * - il contient toutes les clés du schéma, en provisionnant les valeurs par
 | |
|    *   défaut à chaque fois que c'est nécessaire
 | |
|    * - les valeurs sont du bon type. seules les types standards listés dans
 | |
|    * {@link KNOWN_TYPES} sont considérés
 | |
|    *
 | |
|    * notez que:
 | |
|    * - les clés ne sont pas réordonnées
 | |
|    * - la présence de tous les champs requis n'est pas vérifiée par défaut
 | |
|    * (utiliser {@link check_required()} pour cela)
 | |
|    * - seuls les types simples sont supportés. notamment, les types composites
 | |
|    * ne sont pas supportés
 | |
|    * - les types "éléments de liste" ne sont pas supportés
 | |
|    */
 | |
|   static final function ensure_schema(&$item, ?array $schema, $item_key=null, bool $check_required=false): void {
 | |
|     if ($schema !== null) {
 | |
|       $indexes = self::normalize_schema($schema);
 | |
|       self::_ensure_array_item($item, $schema, $item_key);
 | |
|       self::_ensure_schema_recursive($item, $schema, $indexes);
 | |
|       if ($check_required) self::check_required($item, $schema);
 | |
|     } else {
 | |
|       self::_ensure_array_item($item, $schema, $item_key);
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   private static function _ensure_array_item(&$item, ?array $schema, $item_key): void {
 | |
|     if ($item_key !== null) {
 | |
|       if (is_array($item)) {
 | |
|         if ($schema !== null) {
 | |
|           # n'utiliser item_key que si la première clé du schéma n'existe pas
 | |
|           $first_key = array_key_first($schema);
 | |
|           if (!array_key_exists($first_key, $item)) {
 | |
|             $tmp = [$item_key];
 | |
|             A::merge3($tmp, $item);
 | |
|             $item = $tmp;
 | |
|           }
 | |
|         }
 | |
|       } else {
 | |
|         $item = [$item_key, $item];
 | |
|       }
 | |
|     } elseif ($item !== null && !is_array($item)) {
 | |
|       $item = [$item];
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   private static function _ensure_schema_recursive(&$item, array $schema, ?array $indexes=null): void {
 | |
|     self::_ensure_schema($item, $schema, $indexes);
 | |
|     foreach ($schema as $key => $sfield) {
 | |
|       $schema2 = $sfield["schema"];
 | |
|       if ($schema2 === null) continue;
 | |
|       switch ($sfield["type"]) {
 | |
|       case "?array":
 | |
|         if ($item[$key] === null) continue 2;
 | |
|       case "array":
 | |
|         self::_ensure_array_item($item[$key], $schema2, null);
 | |
|         self::_ensure_schema_recursive($item[$key], $schema2);
 | |
|         break;
 | |
|       case "?array[]":
 | |
|         if ($item[$key] === null) continue 2;
 | |
|       case "array[]":
 | |
|         $index2 = 0;
 | |
|         foreach ($item[$key] as $key2 => &$item2) {
 | |
|           if ($key2 === $index2) {
 | |
|             $index2++;
 | |
|             $key2 = null;
 | |
|           }
 | |
|           self::_ensure_array_item($item2, $schema2, $key2);
 | |
|           self::_ensure_schema_recursive($item2, $schema2);
 | |
|         }; unset($item2);
 | |
|         break;
 | |
|       }
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * s'assurer que $item est conforme au schéma $schema.
 | |
|    *
 | |
|    * on assume que $schema est un schéma normalisé. $indexes est reconstruit si
 | |
|    * nécessaire
 | |
|    */
 | |
|   static final function _ensure_schema(&$item, array $schema, ?array $indexes=null, bool $ensure_type=true): void {
 | |
|     $keys = array_keys($schema);
 | |
|     if ($indexes === null) $indexes = array_flip($keys);
 | |
|     if ($item === null) $item = [];
 | |
|     elseif (!is_array($item)) $item = [$item];
 | |
| 
 | |
|     $src = $item;
 | |
|     $dones = array_fill(0, count($keys), false);
 | |
|     # d'abord les clés associatives
 | |
|     $inputIndex = 0;
 | |
|     foreach ($src as $key => $value) {
 | |
|       if ($key === $inputIndex) {
 | |
|         # clé séquentielle
 | |
|         $inputIndex++;
 | |
|       } else {
 | |
|         # clé associative
 | |
|         $is_schema_key = array_key_exists($key, $schema);
 | |
|         if ($ensure_type && $is_schema_key) {
 | |
|           $sfield = $schema[$key];
 | |
|           self::_ensure_type($sfield["type"], $value, $sfield["default"], true);
 | |
|         }
 | |
|         $item[$key] = $value;
 | |
|         if ($is_schema_key) $dones[$indexes[$key]] = true;
 | |
|       }
 | |
|     }
 | |
|     # ensuite les clés séquentielles
 | |
|     $inputIndex = 0;
 | |
|     $outputIndex = 0;
 | |
|     foreach ($src as $index => $value) {
 | |
|       if ($index === $inputIndex) {
 | |
|         # clé séquentielle
 | |
|         $inputIndex++;
 | |
|         unset($item[$index]);
 | |
|         $found = false;
 | |
|         foreach ($keys as $kindex => $key) {
 | |
|           if (!$dones[$kindex]) {
 | |
|             $sfield = $schema[$key];
 | |
|             if ($ensure_type) {
 | |
|               self::_ensure_type($sfield["type"], $value, $sfield["default"], true);
 | |
|             }
 | |
|             $item[$key] = $value;
 | |
|             $dones[$kindex] = true;
 | |
|             $found = true;
 | |
|             break;
 | |
|           }
 | |
|         }
 | |
|         if (!$found) {
 | |
|           $item[$outputIndex++] = $value;
 | |
|         }
 | |
|       }
 | |
|     }
 | |
|     # puis mettre les valeurs par défaut des clés qui restent
 | |
|     foreach ($dones as $dindex => $done) {
 | |
|       if (!$done) {
 | |
|         $key = $keys[$dindex];
 | |
|         $sfield = $schema[$key];
 | |
|         $value = $sfield["default"];
 | |
|         if ($ensure_type) {
 | |
|           self::_ensure_type($sfield["type"], $value, $sfield["default"], false);
 | |
|         }
 | |
|         $item[$key] = $value;
 | |
|       }
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * fonction de support pour s'assurer que $value est dans le bon type. seuls
 | |
|    * les types simples sont reconnus. s'il s'agit d'un type complexe, la valeur
 | |
|    * n'est pas vérifiée ni modifiée
 | |
|    */
 | |
|   private static function _ensure_type(?string $type, &$value, $default, bool $exists): void {
 | |
|     if (self::_check_known_type($type, $value, $default, $exists)) {
 | |
|       self::_convert_value($type, $value);
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * fonction de support pour vérifier que $type est un type simple dans lequel
 | |
|    * on peut convertir la valeur. s'il est possible de convertir la valeur, le
 | |
|    * faire
 | |
|    * - retourner false s'il n'est pas nécessaire de faire plus de traitement. la
 | |
|    * valeur est déjà dans le bon type (ou alors c'est un type complexe et il
 | |
|    * n'est pas possible ici de convertir la valeur)
 | |
|    * - retourner true s'il faut convertir la valeur avec {@link _convert_value()}
 | |
|    */
 | |
|   static final function _check_known_type(&$type, &$value, $default, bool $exists): bool {
 | |
|     $nullable = true;
 | |
|     if (!is_array($type)) $nullable = self::check_nullable($type);
 | |
|     if ($type === ref_type::ANY) {
 | |
|       # Le type null est particulier: false correspondant à non existant
 | |
|       if (!$exists || $value === false) $value = $default;
 | |
|       return false;
 | |
|     } elseif ($type === ref_type::MIXED) {
 | |
|       # Le type mixed est particulier: la valeur est prise en l'état sans aucune
 | |
|       # modification
 | |
|       if (!$exists) $value = $default;
 | |
|       return false;
 | |
|     }
 | |
|     if (!is_array($type)) $type = A::get(ref_type::ALIASES, $type, $type);
 | |
|     if (is_array($type) || !in_array($type, ref_type::KNOWN_TYPES)) {
 | |
|       if (!$exists) $value = $default;
 | |
|       return false;
 | |
|     }
 | |
| 
 | |
|     if ($type === ref_type::BOOL) {
 | |
|       # avec bool, null fonctionne comme "non présent"
 | |
|       if (!$exists || (!$nullable && $value === null)) $value = $default;
 | |
|     } elseif (!$exists || $value === false || (!$nullable && $value === null)) {
 | |
|       $value = $default;
 | |
|     }
 | |
|     if ($value === null && $nullable) return false;
 | |
| 
 | |
|     return true;
 | |
|   }
 | |
| 
 | |
|   const FALSE_VALUES = ["false", "faux", "non", "no", "f", "n", "0", ""];
 | |
| 
 | |
|   /**
 | |
|    * fonction de support pour convertir la valeur dans le type simple spécifié
 | |
|    */
 | |
|   static final function _convert_value(string $type, &$value): void {
 | |
|     switch ($type) {
 | |
|     case ref_type::BOOL:
 | |
|       if (!is_string($value)) $value = boolval($value);
 | |
|       else $value = !in_array(strtolower(trim($value)), self::FALSE_VALUES);
 | |
|       return;
 | |
|     case ref_type::INT:
 | |
|       $value = intval($value);
 | |
|       return;
 | |
|     case ref_type::FLOAT:
 | |
|       $value = floatval($value);
 | |
|       return;
 | |
|     case ref_type::RAWSTRING:
 | |
|       if (is_array($value)) $value = str::join3($value);
 | |
|       elseif (!is_string($value)) $value = strval($value);
 | |
|       return;
 | |
|     case ref_type::STRING:
 | |
|     case ref_type::TEXT:
 | |
|       if (is_array($value)) $value = str::join3($value);
 | |
|       elseif (!is_string($value)) $value = strval($value);
 | |
|       $value = str::norm_nl(trim($value));
 | |
|       return;
 | |
|     case ref_type::KEY:
 | |
|       if (!is_int($value)) {
 | |
|         if (is_array($value)) $value = str::join3($value, ".");
 | |
|         $value = strval($value);
 | |
|         if (preg_match('/^[0-9]+$/', $value)) {
 | |
|           $value = intval($value);
 | |
|         }
 | |
|       }
 | |
|       return;
 | |
|     case ref_type::CONTENT:
 | |
|       $value = c::qnz($value);
 | |
|       return;
 | |
|     case ref_type::FILE:
 | |
|     case ref_type::ARRAY:
 | |
|       A::ensure_array($value);
 | |
|       return;
 | |
|     case ref_type::ARRAY_ARRAY:
 | |
|       A::ensure_array($value);
 | |
|       foreach ($value as &$item) {
 | |
|         A::ensure_array($item);
 | |
|       }; unset($item);
 | |
|       return;
 | |
|     case ref_type::ITERABLE:
 | |
|       if (!($value instanceof Traversable)) {
 | |
|         A::ensure_array($value);
 | |
|       }
 | |
|       return;
 | |
|     case ref_type::RESOURCE:
 | |
|       if (!is_resource($value)) {
 | |
|         throw ValueException::invalid_value($value, "resource");
 | |
|       }
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * vérifier que tous les champs marqués comme requis dans le schéma n'ont pas
 | |
|    * une valeur null. sinon lancer une exception {@link ValueException}
 | |
|    *
 | |
|    * on assume que $item est conforme au schéma
 | |
|    */
 | |
|   static final function check_required(array $item, ?array $schema): void {
 | |
|     if ($schema === null) return;
 | |
|     self::normalize_schema($schema);
 | |
|     self::_check_required($item, $schema);
 | |
|   }
 | |
| 
 | |
|   /** comme {@link check_required()} mais $schema doit être déjà normalisé */
 | |
|   static final function _check_required(array $item, array $schema, string $key_prefix=""): void {
 | |
|     foreach ($schema as $key => $sfield) {
 | |
|       $exists = array_key_exists($key, $item) && $item[$key] !== null;
 | |
|       if ($sfield["required"] && !$exists) {
 | |
|         throw new ValueException("$key_prefix$key is required");
 | |
|       }
 | |
|       $schema2 = $sfield["schema"];
 | |
|       if ($schema2 !== null && $exists) {
 | |
|         #XXX si le type est array[], il faut vérifier sur chacun des éléments!
 | |
|         self::_check_required($item[$key], $schema2, "$key_prefix$key.");
 | |
|       }
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * obtenir la valeur de la clé $key depuis $item en tenant compte des
 | |
|    * informations du schéma. si la clé n'existe pas, retourner la valeur par
 | |
|    * défaut: soit $default s'il n'est pas null, soit la valeur par défaut du
 | |
|    * schéma.
 | |
|    *
 | |
|    * $item n'a pas besoin d'être conforme au schéma: il n'est pas nécessaire
 | |
|    * que toutes les clés soient présentes, et $item peut être un tableau
 | |
|    * séquentiel
 | |
|    *
 | |
|    * XXX si le type n'est pas bool, interpréter false comme "non présent"
 | |
|    */
 | |
|   static final function has($item, $key, ?array $schema): bool {
 | |
|     if ($schema === null) return $key === 0;
 | |
|     $indexes = self::normalize_schema($schema);
 | |
|     return self::_has($item, $key, $schema, $indexes);
 | |
|   }
 | |
| 
 | |
|   /** comme {@link has()} mais $schema doit être déjà normalisé */
 | |
|   static final function _has($item, $key, array $schema, ?array $indexes=null) {
 | |
|     if (!array_key_exists($key, $schema)) {
 | |
|       return is_array($item) && array_key_exists($key, $item);
 | |
|     }
 | |
|     if (!is_array($item)) $item = [$item];
 | |
|     if (array_key_exists($key, $item)) return true;
 | |
|     if ($indexes === null) $indexes = array_flip(array_keys($schema));
 | |
|     $index = $indexes[$key];
 | |
|     return array_key_exists($index, $item);
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * obtenir la valeur de la clé $key depuis $item en tenant compte des
 | |
|    * informations du schéma. si la clé n'existe pas, retourner la valeur par
 | |
|    * défaut: soit $default s'il n'est pas null, soit la valeur par défaut du
 | |
|    * schéma.
 | |
|    *
 | |
|    * $item n'a pas besoin d'être conforme au schéma: il n'est pas nécessaire
 | |
|    * que toutes les clés soient présentes, et $item peut être un tableau
 | |
|    * séquentiel
 | |
|    */
 | |
|   static final function get($item, $key, ?array $schema, $default=null, bool $ensure_type=true) {
 | |
|     if ($schema === null) return $key === 0? $item: null;
 | |
|     $indexes = self::normalize_schema($schema);
 | |
|     return self::_get($item, $key, $schema, $default, $ensure_type, $indexes);
 | |
|   }
 | |
| 
 | |
|   /** comme {@link get()} mais $schema doit être déjà normalisé */
 | |
|   static final function _get($item, $key, array $schema, $default=null, bool $ensure_type=true, ?array $indexes=null) {
 | |
|     if (!array_key_exists($key, $schema)) {
 | |
|       if (is_array($item) && array_key_exists($key, $item)) {
 | |
|         return $item[$key];
 | |
|       } else {
 | |
|         return $default;
 | |
|       }
 | |
|     }
 | |
|     $sfield = $schema[$key];
 | |
| 
 | |
|     if (!is_array($item)) $item = [$item];
 | |
|     if (array_key_exists($key, $item)) {
 | |
|       $exists = true;
 | |
|       $value = $item[$key];
 | |
|     } else {
 | |
|       if ($indexes === null) $indexes = array_flip(array_keys($schema));
 | |
|       $index = $indexes[$key];
 | |
|       if (array_key_exists($index, $item)) {
 | |
|         $exists = true;
 | |
|         $value = $item[$index];
 | |
|       } elseif ($default !== null) {
 | |
|         $exists = true;
 | |
|         $value = $default;
 | |
|       } else {
 | |
|         $exists = false;
 | |
|         $value = $sfield["default"];
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     if ($ensure_type) {
 | |
|       self::_ensure_type($sfield["type"], $value, $sfield["default"], $exists);
 | |
|       $schema2 = $sfield["schema"];
 | |
|       if ($schema2 !== null) self::_ensure_schema_recursive($value, $schema2);
 | |
|     }
 | |
|     return $value;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * sélectionner dans $items les valeurs du schéma, et les retourner dans
 | |
|    * l'ordre du schéma
 | |
|    *
 | |
|    * $item doit être un tableau associatif. idéalement, il a été au préalable
 | |
|    * rendu conforme au schéma
 | |
|    *
 | |
|    * si $item n'est pas conforme au schéma, les champs ne sont reconnus que si
 | |
|    * l'on utilise les clés associatives. il n'est pas nécessaire que toutes les
 | |
|    * clés du schéma soient présentes. dans ce cas, seuls les clés présentes sont
 | |
|    * dans le tableau résultat. dans ce cas de figure, $ensure_type==true permet
 | |
|    * de s'assurer aussi que les valeurs sont dans le bon type
 | |
|    */
 | |
|   static final function get_values(?array $item, ?array $schema, bool $ensure_type=false): array {
 | |
|     if ($item === null || $schema === null) return [];
 | |
|     self::normalize_schema($schema);
 | |
|     return self::_get_values($item, $schema, $ensure_type);
 | |
|   }
 | |
| 
 | |
|   /** comme {@link get_values()} mais $schema doit être déjà normalisé */
 | |
|   static final function _get_values(array $item, array $schema, bool $ensure_type=false): array {
 | |
|     $values = [];
 | |
|     foreach ($schema as $key => $sfield) {
 | |
|       if (!array_key_exists($key, $item)) continue;
 | |
|       $value = $item[$key];
 | |
|       if ($ensure_type) {
 | |
|         self::_ensure_type($sfield["type"], $value, $sfield["default"], true);
 | |
|       }
 | |
|       $values[$key] = $value;
 | |
|     }
 | |
|     return $values;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * complément de {@link get_values()}: retourner les clés qui n'ont pas été
 | |
|    * sélectionnées
 | |
|    */
 | |
|   static final function get_others(?array $item, ?array $schema): array {
 | |
|     if ($item === null) return [];
 | |
|     elseif ($schema === null) return $item;
 | |
|     self::normalize_schema($schema);
 | |
|     return self::_get_others($item, $schema);
 | |
|   }
 | |
| 
 | |
|   /** comme {@link get_others()} mais $schema doit être déjà normalisé */
 | |
|   static final function _get_others(array $item, array $schema): array {
 | |
|     $others = [];
 | |
|     foreach ($item as $key => $value) {
 | |
|       if (array_key_exists($key, $schema)) continue;
 | |
|       $others[$key] = $value;
 | |
|     }
 | |
|     return $others;
 | |
|   }
 | |
| }
 |