[ "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 final 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 final 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 final 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; } }