nur-sery/nur_src/md.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 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;
}
}