diff --git a/nur_src/data/Context.php b/nur_src/data/Context.php new file mode 100644 index 0000000..f9f900d --- /dev/null +++ b/nur_src/data/Context.php @@ -0,0 +1,15 @@ +data["value"]; + } + + function getName(): string { + return $this->data["name"]; + } + + function getTitle(): string { + return $this->data["title"]; + } + + public function eval(IContext $context) { + $expr = $this->getValue(); + if (func::is_static($expr) || func::is_method($expr)) { + return $context->callMethod($expr); + } elseif (is_string($expr)) { + $prefix = substr($expr, 0, 1); + if ($prefix == "*") { + # session + return $context->getSession(substr($expr, 1)); + } elseif ($prefix == "+") { + # config + return $context->getConfig(substr($expr, 1)); + } + # valeur à récupérer du contexte + return $context->getValue($expr); + } + throw ValueException::unexpected_type(["string", "callable"], $expr); + } +} diff --git a/nur_src/data/expr/IContext.php b/nur_src/data/expr/IContext.php new file mode 100644 index 0000000..6f5faf8 --- /dev/null +++ b/nur_src/data/expr/IContext.php @@ -0,0 +1,60 @@ + [null, null, "sources des données de ce contexte", true], + "exprs" => [null, null, "liste des expressions définies dans ce contexte", true], + "conds" => [null, null, "liste des expressions conditionnelles définies dans ce contexte", true], + ]; + /** schéma d'une description de source sous forme de tableau */ + const SOURCE_SCHEMA = [ + "name" => [null, null, "identifiant de la source de donnée", true], + "title" => [null, null, "description de la source de donnée, pour affichage", false], + ]; + /** schéma d'une description d'une expression sous forme de tableau */ + const EXPR_SCHEMA = IExpr::SCHEMA; + /** schéma d'une description d'une condition sous forme de tableau */ + const COND_SCHEMA = IExpr::SCHEMA; + + /** + * @return array des informations sur ce contexte, sous la forme d'un tableau + * conforme au schéma {@link IContext::SCHEMA} + */ + function getContextInfos(): array; + + /** @return mixed obtenir la valeur correspondant au chemin */ + function getValue(string $pkey); + + /** @return mixed obtenir la valeur de la session correspondant au chemin */ + function getSession(string $pkey); + + /** @return mixed obtenir la valeur de configuration correspondant au chemin */ + function getConfig(string $pkey); + + /** + * appeler la méthode spécifiée et retourner le résultat de l'appel. + * + * La méthode peut être dans un des formats suivants: + * - "Class::method" ou ["Class", "method"] pour appeler une méthode statique + * de la classe spécifiée + * - "::method", ["method"] ou [null, "method"] pour appeler une méthode + * statique de la classe par défaut + * - "->method", ["->method"] ou [anything, "->method"] pour appeler une + * méthode de l'objet par défaut + * + * La classe et l'objet par défaut sont déterminés par le contexte. + * + * Si $method est un tableau, il peut contenir des éléments supplémentaires + * qui sont considérés comme des arguments de l'appel, e.g: + * $context->callMethod(["MyClass", "method", "hello", "world"]); + * est équivant à: + * MyClass::method("hello", "world"); + */ + function callMethod($method); +} diff --git a/nur_src/data/expr/IExpr.php b/nur_src/data/expr/IExpr.php new file mode 100644 index 0000000..0805da2 --- /dev/null +++ b/nur_src/data/expr/IExpr.php @@ -0,0 +1,28 @@ + [null, null, "définition de l'expression", true], + "name" => [null, null, "identifiant de l'expression dans le modèle", true], + "title" => [null, null, "description courte de l'expression, pour affichage", false, + "desc" => "vaut [name] par défaut", + ], + ]; + + /** obtenir la définition de l'expression, le cas échéant */ + function getValue(): ?string; + + /** obtenir l'identifiant de l'expression */ + function getName(): string; + + /** obtenir une description courte de l'expression, pour affichage */ + function getTitle(): string; + + /** évalue l'expression dans le contexte spécifié et retourne sa valeur */ + function eval(IContext $context); +} diff --git a/nur_src/data/expr/SimpleContext.php b/nur_src/data/expr/SimpleContext.php new file mode 100644 index 0000000..08ac582 --- /dev/null +++ b/nur_src/data/expr/SimpleContext.php @@ -0,0 +1,75 @@ + $source} conformes au schéma + * {@link IContext::SOURCE_SCHEMA} + */ + protected function SOURCES(): array { + return static::SOURCES; + } const SOURCES = []; + + /** + * @return array une liste d'expressions {$name => $expr} conformes au schéma + * {@link IContext::EXPR_SCHEMA} + */ + protected function EXPRS(): array { + return static::EXPRS; + } const EXPRS = []; + + /** + * @return array une liste de conditions {$key => $cond} conformes au schéma + * {@link IContext::COND_SCHEMA} + */ + protected function CONDS(): array { + return static::CONDS; + } const CONDS = []; + + /** @var mixed l'objet sur lequel sont appliquées les appels de méthode */ + protected $object; + + function __construct(?array $data=null) { + parent::__construct($data); + $this->object = $this; + } + + function getContextInfos(): array { + return [ + "sources" => $this->SOURCES(), + "exprs" => $this->EXPRS(), + "conds" => $this->CONDS(), + ]; + } + + function getValue(string $pkey) { + #XXX parcourir les sources + return A::pget($this->data, $pkey); + } + + function getSession(string $pkey) { + return session::pget($pkey); + } + + function getConfig(string $pkey) { + return config::get($pkey); + } + + function callMethod($method) { + func::ensure_func($method, $this->object, $args); + return func::call($method, ...$args); + } + + ## ArrayAccess + function has($key): bool { return $this->_has($key); } + function &get($key, $default=null) { return $this->_get($key, $default); } + function set($key, $value): self { return $this->_set($key, $value); } + function add($value): self { return $this->_set(null, $value); } + function del($key): self { return $this->_del($key); } +} diff --git a/nur_src/data/flow/IStateMachine.php b/nur_src/data/flow/IStateMachine.php new file mode 100644 index 0000000..1d1088b --- /dev/null +++ b/nur_src/data/flow/IStateMachine.php @@ -0,0 +1,16 @@ +context = $this; + $this->setText($text, $data, $quote, $allowPattern); + } + + /** @var string */ + protected $text; + /** @var bool|array */ + protected $quote; + /** @var bool */ + protected $allowPattern; + + function setText(?string $text, $data=null, $quote=true, bool $allowPattern=true): void { + $this->text = $text; + if ($data !== null) { + $this->data = A::with($data); + $this->quote = $quote; + $this->allowPattern = $allowPattern; + } + } + + /** + * Traiter la valeur $value selon la valeur de $quote + * + * D'abord la transformer en chaine, puis: + * - si $quote===true, la mettre en échappement avec htmlspecialchars() + * - si $quote===false ou null, ne pas la mettre en échappement + * - sinon, ce doit être une fonction qui met la valeur en échappement + */ + private static final function quote($value, $quote) { + if (A::is_array($value)) $value = print_r(A::with($value), true); + elseif (!is_string($value)) $value = strval($value); + if ($quote === true) return htmlspecialchars($value); + else if ($quote === false || $quote === null) return $value; + else return func::call($quote, $value); + } + + /** + * Traiter la valeur $value selon la valeur de $quote. Le traitement final est + * fait avec la méthode quote() + * + * - Si $quote est un tableau, alors $quote[$name] est la valeur utilisée pour + * décider comment traiter la valeur et sa valeur par défaut est true. + * - Sinon prendre la valeur $quote telle quelle + */ + private static final function quote_nv(string $name, $value, $quote) { + if (is_array($quote)) { + if (isset($quote[$name])) { + $value = self::quote($value, $quote[$name]); + } else { + # quoter par défaut quand on fournit un tableau + $value = self::quote($value, true); + } + } else { + $value = self::quote($value, $quote); + } + return $value; + } + + /** + * Résoudre la valeur du nom $name + * - Si $name est de la forme '+keyp', prendre la valeur dans la configuration + * - Si $name est de la forme '*keyp', prendre la valeur dans la session + * - Sinon, $name est le chemin de clé dans le tableau $values + */ + private function resolve(string $name, ?array $values, $quote) { + switch (substr($name, 0, 1)) { + case "+": + return $this->getConfig(substr($name, 1)); + case "*": + if (!session::started()) return ""; + return $this->getSession(substr($name, 1)); + default: + $value = A::pget_s($values, $name, ""); + $value = self::quote_nv($name, $value, $quote); + return $value; + } + } + + function apply() { + $text = $this->text; + $data = $this->data; + $quote = $this->quote; + ## d'abord les remplacements complexes + if ($this->allowPattern) { + if ($data !== null) { + $names = array_keys($data); + $name_keys_pattern = '('.implode("|", $names).')((?:\.[a-zA-Z0-9_]+)*)'; + $or_name_pattern = '|(?:(?:'.implode("|", $names).')(?:\.[a-zA-Z0-9_]+)*)'; + } else { + $name_keys_pattern = null; + $or_name_pattern = ''; + } + # patterns conditionnels + # autoriser un niveau de variable complexe à l'intérieur de la condition, i.e {{if(cond){{var.value}}}} + $pattern = '/\{\{(if|unless)\(((?:[+*][a-zA-Z0-9_]+(?:\.[a-zA-Z0-9_]+)*)'.$or_name_pattern.')\)((?:[^{}]*(?:\{\{[^{}]*\}\})?(?:\{[^{}]*\})?)*)\}\}/s'; + $text = preg_replace_callback($pattern, function($matches) use($data, $quote) { + $cond = $this->resolve($matches[2], $data, $quote); + if ($matches[1] == "if") { + if ($cond) $value = $matches[3]; + else return ""; + } elseif ($matches[1] == "unless") { + if (!$cond) $value = $matches[3]; + else return ""; + } else { + return $matches[0]; + } + $value = self::xml($value, $data, $quote); + return $value; + }, $text); + # valeurs de config + $pattern = '/\{\{(\+[a-zA-Z0-9_]+(?:\.[a-zA-Z0-9_]+)*)\}\}/s'; + $text = preg_replace_callback($pattern, function($matches) use($data, $quote) { + $name = $matches[1]; + $value = $this->resolve($name, $data, $quote); + return $value; + }, $text); + # valeurs de la session + $pattern = '/\{\{(\*[a-zA-Z0-9_]+(?:\.[a-zA-Z0-9_]+)*)\}\}/s'; + $text = preg_replace_callback($pattern, function($matches) use($data, $quote) { + $name = $matches[1]; + $value = $this->resolve($name, $data, $quote); + return $value; + }, $text); + if ($data !== null) { + # indirections + $pattern = '/\{\{'.$name_keys_pattern.'\}\}/s'; + $text = preg_replace_callback($pattern, function($matches) use($data, $quote) { + $name = $matches[1]; + $value = $this->resolve("$name$matches[2]", $data, $quote); + return $value; + }, $text); + } + } + if ($data !== null) { + ## ensuite les remplacements simples + $froms = array(); + $tos = array(); + foreach ($data as $name => $value) { + $froms[] = "{".$name."}"; + $tos[] = self::quote_nv($name, $value, $quote); + } + $text = str_replace($froms, $tos, $text); + } + return $text; + } + + function xml(?string $text, $data=null, $quote=true, bool $allowPattern=true): string { + $this->setText($text, $data, $quote, $allowPattern); + return $this->apply(); + } + + /** + * Comme {@link xml()} mais les valeurs ne sont pas mises en échappement par + * défaut. + */ + function string(?string $text, $data=null): string { + return $this->xml($text, $data, false); + } + + /** Comme {@link xml()} pour un fichier au format LibreOffice Writer */ + function odt(string $file, $data=null, $quote=true): void { + $zip = new ZipArchive(); + $error = $zip->open($file); + if ($error !== true) { + throw new IOException("$file: error $error occured on open"); + } + $oldContent = $zip->getFromName("content.xml"); + if ($oldContent !== false) { + $newContent = $this->xml($oldContent, $data, $quote); + if ($newContent !== $oldContent) { + $zip->deleteName("content.xml"); + $zip->addFromString("content.xml", $newContent); + } + } + $oldStyles = $zip->getFromName("styles.xml"); + if ($oldStyles !== false) { + $newStyles = $this->xml($oldStyles, null, $quote); + if ($newStyles != $oldStyles) { + $zip->deleteName("styles.xml"); + $zip->addFromString("styles.xml", $newStyles); + } + } + $zip->close(); + } +} diff --git a/nur_src/data/template/StreamTemplate.php b/nur_src/data/template/StreamTemplate.php new file mode 100644 index 0000000..2b4a962 --- /dev/null +++ b/nur_src/data/template/StreamTemplate.php @@ -0,0 +1,62 @@ +input = $input; + $this->output = $output; + $this->autoClose = $autoClose; + $this->context = $this; + } + + protected function ensureInput(): IReader { + $input = $this->input; + if ($input === null) { + $input = $this->INPUT(); + if ($input === null) { + throw new ValueException("input is required"); + } + $input = new FileReader($input); + } + return $input; + } + + /** retourner true */ + function apply() { + $context = $this->getContext(); + $input = $this->ensureInput(); + $output = $this->output; + foreach ($input as $line) { + $line = $this->applyRules($line, $context); + $output->wnl($line); + } + $output->close($this->autoClose); + return true; + } +} diff --git a/nur_src/data/template/StringTemplate.php b/nur_src/data/template/StringTemplate.php new file mode 100644 index 0000000..f461ce5 --- /dev/null +++ b/nur_src/data/template/StringTemplate.php @@ -0,0 +1,34 @@ +text; + if ($text === null) $text = static::TEXT; + return $text; + } const TEXT = ""; + + function __construct(?array $data=null) { + parent::__construct($data); + $this->context = $this; + } + + /** @var string */ + protected $text; + + function setText(string $text): void { + $this->text = $text; + } + + /** retourner le texte avec les variables renseignées */ + function apply() { + $text = $this->TEXT(); + $context = $this->getContext(); + return $this->applyRules($text, $context); + } +} diff --git a/nur_src/data/template/TTemplate.php b/nur_src/data/template/TTemplate.php new file mode 100644 index 0000000..9038758 --- /dev/null +++ b/nur_src/data/template/TTemplate.php @@ -0,0 +1,29 @@ +context; + } + + function setContext(IContext $context): void { + $this->context = $context; + } + + function applyRules(string $text, IContext $context) { + foreach ($this->EXPRS() as $name => $expr) { + $value = GenericExpr::with($expr, $name)->eval($context); + if (!base::is_undef($value)) $text = str_replace($name, $value, $text); + } + #XXX ajouter le support des expressions conditionnelles. traiter ligne par + # ligne s'il y a des expressions conditionnelles + return $text; + } +} diff --git a/nur_src/data/types/AbstractComplexType.php b/nur_src/data/types/AbstractComplexType.php new file mode 100644 index 0000000..7c66f80 --- /dev/null +++ b/nur_src/data/types/AbstractComplexType.php @@ -0,0 +1,119 @@ + ["string", null, "nom de la composante", "required" => true], + "type" => ["mixed", null, "type de la composante", "required" => true], + "title" => ["?string", null, "libellé de la composante"], + ]; + + /** @var array la définition des composantes */ + const COMPONENTS = []; + + /** @var string le séparateur entre chaque composant utilisé lors du formatage */ + const SEPARATOR = " "; + + /** @var string le pattern qui matche le séparateur entre les composants */ + const SEPARATOR_PATTERN = '/^\s+/'; + + ############################################################################# + + /** @var array */ + protected $components; + + function setComponents(array $components): self { + foreach ($components as $key => &$component) { + md::ensure_schema($component, self::COMPONENT_SCHEMA, $key, false); + $type = $component["type"]; + if (is_array($type) && count($type) == 1) { + $type[] = $component; + } + $component["type"] = types::get($type); + }; unset($component); + $this->components = $components; + return $this; + } + + const PARAMETRABLE_PARAMS_SCHEMA = [ + "separator" => ["string", null, "séparateur pour le format"], + "separator_pattern" => ["string", null, "séparateur pour l'analyse"], + ]; + + /** @var string */ + protected $ppSeparator; + + /** @var string */ + protected $ppSeparatorPattern; + + ############################################################################# + + function __construct(?array $params=null) { + $this->setComponents(static::COMPONENTS); + $this->ppSeparator = static::SEPARATOR; + $this->ppSeparatorPattern = static::SEPARATOR_PATTERN; + parent::__construct($params); + } + + function getClass(): string { + return "string"; + } + + function canFormat(): bool { + return false; + } + + function canParse(): bool { + return false; + } + + function parse(string &$input, ?array $params=null) { + $data = $input; + $values = []; + foreach ($this->components as $component) { + $cname = $component["name"]; + # cette fonction sert uniquement à avoir la valeur dans le bon type + /** @var IType $ctype */ + $ctype = $component["type"]; + if ($data !== "") { + $value = $ctype->parse($data); + } elseif ($ctype->isAllowEmpty()) { + $value = ""; + $ctype->verifixReplaceEmpty($value); + } else { + $value = false; + } + if ($value === false) return false; + if ($ctype instanceof AbstractSimpleType) $ctype->verifixConvert($value); + $values[$cname] = $value; + $data = preg_replace(static::SEPARATOR_PATTERN, "", $data); + } + $input = $data; + return $values; + } + + function verifixConvert(&$value): bool { + $parts = []; + foreach ($this->components as $component) { + $cname = $component["name"]; + $ctype = $component["type"]; + $parts[] = $ctype->format($value[$cname]); + } + $value = implode($this->ppSeparator, $parts); + return true; + } +} diff --git a/nur_src/data/types/AbstractCompositeType.php b/nur_src/data/types/AbstractCompositeType.php new file mode 100644 index 0000000..7876b3b --- /dev/null +++ b/nur_src/data/types/AbstractCompositeType.php @@ -0,0 +1,255 @@ + ["string", null, "nom de la composante", "required" => true], + "type" => ["mixed", null, "type de la composante", "required" => true], + "key" => ["?string", null, "nom de la clé de la composante dans l'objet destination"], + "title" => ["?string", null, "libellé de la composante"], + ]; + + /** @var array messages standards */ + const CMESSAGES = [ + "empty" => "{cname}: cette valeur ne doit pas être vide", + "false" => "{cname}: cette valeur ne doit pas être false", + "null" => "{cname}: cette valeur ne doit pas être null", + "invalid" => "{cname}: {value}: cette valeur est invalide", + ]; + + /** @var array la définition des composantes */ + const COMPONENTS = []; + + /** @var string le séparateur entre chaque composant utilisé lors du formatage */ + const SEPARATOR = " "; + + /** @var string le pattern qui matche le séparateur entre les composants */ + const SEPARATOR_PATTERN = '/^\s+/'; + + private static function itype($type): IType { return $type; } + + ############################################################################# + + /** @var array */ + protected $components; + + function setComponents(array $components): self { + foreach ($components as $key => &$component) { + md::ensure_schema($component, self::COMPONENT_SCHEMA, $key, false); + A::replace_n_indirect($component, "key", "name"); + $type = $component["type"]; + if (is_array($type) && count($type) == 1) { + $type[] = $component; + } + $component["type"] = types::get($type); + }; unset($component); + $this->components = $components; + return $this; + } + + const PARAMETRABLE_PARAMS_SCHEMA = [ + "ckeys" => ["array", null, "clés à utiliser pour les composantes"], + "separator" => ["string", null, "séparateur pour le format"], + "separator_pattern" => ["string", null, "séparateur pour l'analyse"], + "cmessages" => ["array", null, "messages à retourner en cas d'erreur pour les composantes"], + ]; + + function pp_setCkeys(array $ckeys): self { + if (A::is_seq($ckeys)) { + $index = 0; + foreach ($this->components as &$component) { + $component["key"] = $ckeys[$index++]; + }; unset($component); + } else { + foreach ($ckeys as $cname => $ckey) { + $this->components[$cname]["key"] = $ckey; + } + } + return $this; + } + + /** @var string */ + protected $ppSeparator; + + /** @var string */ + protected $ppSeparatorPattern; + + /** @var array messages à retourner en cas d'erreur */ + protected $ppCmessages; + + ############################################################################# + + function __construct(?array $params=null) { + $this->setComponents(static::COMPONENTS); + $this->ppSeparator = static::SEPARATOR; + $this->ppSeparatorPattern = static::SEPARATOR_PATTERN; + parent::__construct($params); + } + + /** + * retourner une liste {cname => component} des composantes, qui sont chacune + * conformes au schéma {@link COMPONENT_SCHEMA} + */ + function getComponents(): array { + return $this->components; + } + + protected function _getComponent(string $cname) { + return ValueException::check_nn(A::get($this->components, $cname), "$cname: invalid component"); + } + + /** obtenir le type d'une composante en particulier */ + function getCtype(string $cname): IType { + return $this->_getComponent($cname)["type"]; + } + + protected function _getCvalue(array $component, $value) { + return $value[$component["key"]]; + } + + protected function _setCvalue(array $component, &$value, $cvalue): void { + $value[$component["key"]] = $cvalue; + } + + /** obtenir la valeur d'une composante en particulier */ + function getCvalue(string $cname, $value) { + return $this->_getCvalue($this->_getComponent($cname), $value); + } + + /** spécifier la valeur d'une composante en particulier */ + function setCvalue(string $cname, &$value, $cvalue): void { + $this->_setCvalue($this->_getComponent($cname), $value, $cvalue); + } + + function getClass(): string { + return "array"; + } + + function format($value, ?array $params=null): string { + $parts = []; + foreach ($this->components as $component) { + $ctype = self::itype($component["type"]); + $parts[] = $ctype->format($this->_getCvalue($component, $value)); + } + return implode(static::SEPARATOR, $parts); + } + + function parse(string &$input, ?array $params=null) { + $data = $input; + $values = []; + foreach ($this->components as $component) { + $ckey = $component["key"]; + $ctype = self::itype($component["type"]); + $isaSimpleType = $ctype instanceof AbstractSimpleType; + if ($data !== "") { + $value = $ctype->parse($data); + } elseif ($isaSimpleType && $ctype->isAllowParseEmpty()) { + $value = null; + } elseif (!$isaSimpleType && $ctype->isAllowEmpty()) { + $value = ""; + $ctype->verifixReplaceEmpty($value); + } else { + $value = false; + } + if ($value === false) return false; + if ($isaSimpleType) $ctype->verifixConvert($value); + $values[$ckey] = $value; + $data = preg_replace(static::SEPARATOR_PATTERN, "", $data); + } + $input = $data; + return $values; + } + + ############################################################################# + + protected static function _update_error(array &$cresult, string $error_code, array $component, ?array $cmessages): void { + $cresult["cname"] = $component["name"]; + $cresult["ckey"] = $component["key"]; + if ($cmessages === null) $cmessages = static::CMESSAGES; + self::_set_error($cresult, $error_code, $cmessages); + } + + /** + * fonction de support: convertir le résultat de l'analyse de la chaine dans + * le type approprié. retourner true si la valeur est valide, false si la + * valeur est invalide (bien que peut-être syntaxiquement correcte) + */ + function verifixConvert(&$value): bool { + return true; + } + + /** + * retourner une description de la valeur, utilisable le cas échéant dans les + * messages d'erreur + */ + protected function getValueDesc($orig, $parsed, $value): ?string { + if (is_array($value)) { + $parts = []; + foreach ($this->components as $component) { + $parts[] = A::get($value, $component["key"]); + } + return implode("-", $parts); + } + return null; + } + + protected function _verifix(&$value, array &$result=null): void { + $orig = $value; + if ($this->verifixNoParse($value, $result)) return; + if (is_string($value)) { + try { + $input = $unparsed = $value; + $cvalues = $this->parse($input); + $parsed = substr($unparsed, 0, strlen($unparsed) - strlen($input)); + if (($input === "" || !$this->ppParseAll) && $this->verifixConvert($cvalues)) { + $value = $cvalues; + self::result_valid($result, $value, $orig, $parsed, $input); + } else { + $orig_desc = $this->getValueDesc($orig, $parsed, $cvalues); + $value = self::result_invalid($result, "invalid", $orig, $orig_desc, $value, $input, $this->ppMessages, null); + } + } catch (Exception $e) { + $orig_desc = $this->getValueDesc($orig, false, $value); + $value = self::result_invalid($result, "invalid", $orig, $orig_desc, false, false, $this->ppMessages, $e); + } + } else { + $cresults = []; + $cvalid = true; + foreach ($this->components as $component) { + $ckey = $component["key"]; + $ctype = self::itype($component["type"]); + $ctype->verifix($value[$ckey], $cresult); + if (!$cresult["valid"]) { + $cvalid = false; + self::_update_error($cresult, $cresult["error_code"], $component, $this->ppCmessages); + } + $cresults[$ckey] = $cresult; + } + if ($cvalid && $this->verifixConvert($value)) { + self::result_valid($result, $value, $orig, false, false); + } else { + $orig_desc = $this->getValueDesc($orig, false, $value); + $value = self::result_invalid($result, "invalid", $orig, $orig_desc, false, false, $this->ppMessages, null); + } + $result["cresults"] = $cresults; + } + } +} diff --git a/nur_src/data/types/AbstractSimpleType.php b/nur_src/data/types/AbstractSimpleType.php new file mode 100644 index 0000000..a61449b --- /dev/null +++ b/nur_src/data/types/AbstractSimpleType.php @@ -0,0 +1,128 @@ + ["bool", null, "une analyse qui ne consomme aucun caractère est-elle autorisée?"], + ]; + + /** @var bool */ + protected $ppAllowParseEmpty; + + function isAllowParseEmpty(): bool { + return $this->ppAllowParseEmpty; + } + + ############################################################################# + + function __construct(?array $params=null) { + A::replace_n($params, "allow_parse_empty", static::ALLOW_PARSE_EMPTY); + parent::__construct($params); + } + + ############################################################################# + + function canFormat(): bool { + return true; + } + + function format($value): string { + return strval($value); + } + + ############################################################################# + + /** + * fonction de support: mettre en forme la valeur, qui est déjà dans le bon + * type. @return false s'il n'est pas possible de mettre en forme la valeur. + */ + function beforeCheckInstance(&$value): bool { + return true; + } + + /** + * fonction de support: mettre en forme la valeur, qui est déjà dans le bon type + */ + function afterCheckInstance(&$value): void { + } + + /** fonction de support: traiter les valeurs qui sont déjà dans le bon type */ + protected function verifixCheckClass(&$value, $orig, array &$result=null): bool { + if (!static::CHECK_CLASS) return false; + if (!$this->beforeCheckInstance($value)) return false; + if (!$this->isInstance($value)) return false; + $this->afterCheckInstance($value); + self::result_valid($result, $value, $orig); + return true; + } + + /** + * fonction de support: convertir le résultat de l'analyse de la chaine dans + * le type approprié. retourner true si la valeur est valide, false si la + * valeur est invalide (bien que peut-être syntaxiquement correcte) + * + * NB: si $this->allowParseEmpty==true, alors $value peut être null. il faut + * donc toujours tester ce cas, parce que la classe peut-être instanciée avec + * ce paramètre + */ + function verifixConvert(&$value): bool { + return true; + } + + /** + * retourner une description de la valeur, utilisable le cas échéant dans les + * messages d'erreur + */ + protected function getValueDesc($orig, $parsed, $value): ?string { + return null; + } + + /** + * fonction de support: analyser la chaine et retourner la valeur dans le type + * approprié + */ + protected function verifixParse(string $value, $orig, ?array &$result=null) { + try { + $input = $unparsed = $value; + $value = $this->parse($input); + $parsed = substr($unparsed, 0, strlen($unparsed) - strlen($input)); + if (($input === "" || !$this->ppParseAll) && $this->verifixConvert($value)) { + return self::result_valid($result, $value, $orig, $parsed, $input); + } + $orig_desc = $this->getValueDesc($orig, $parsed, $value); + return self::result_invalid($result, "invalid", $orig, $orig_desc, $parsed, $input, $this->ppMessages, null); + } catch (Exception $e) { + $orig_desc = $this->getValueDesc($orig, false, $value); + return self::result_invalid($result, "invalid", $orig, $orig_desc, false, false, $this->ppMessages, $e); + } + } + + protected function _verifix(&$value, array &$result=null): void { + $orig = $value; + if ($this->verifixNoParse($value, $result)) return; + if ($this->verifixCheckClass($value, $orig, $result)) return; + if (is_string($value)) { + $value = $this->verifixParse($value, $orig, $result); + } else { + $orig_desc = $this->getValueDesc($orig, false, $value); + $value = self::result_invalid($result, "invalid", $orig, $orig_desc, false, false, $this->ppMessages, null); + } + } +} diff --git a/nur_src/data/types/AbstractType.php b/nur_src/data/types/AbstractType.php new file mode 100644 index 0000000..a1b8ae7 --- /dev/null +++ b/nur_src/data/types/AbstractType.php @@ -0,0 +1,343 @@ + "cette valeur ne doit pas être vide", + "false" => "cette valeur ne doit pas être false", + "null" => "cette valeur ne doit pas être null", + "invalid" => "{value_desc}: cette valeur est invalide", + ]; + + ############################################################################# + + const PARAMETRABLE_PARAMS_SCHEMA = [ + "trim" => ["bool", null, "faut-il trimmer la valeur chaine avant analyse"], + "allow_empty" => ["?bool", null, "la chaine vide est-elle autorisée?"], + "allow_false" => ["bool", null, "la valeur false est-elle autorisée?"], + "allow_null" => ["bool", null, "la valeur null est-elle autorisée?"], + "parse_all" => ["bool", null, "la chaine fournie doit-elle correspondre entièrement?"], + "messages" => ["array", null, "messages à retourner en cas d'erreur"], + ]; + + /** @var bool */ + protected $ppTrim; + + function isTrim(): bool { + return $this->ppTrim; + } + + /** @var bool */ + protected $ppAllowEmpty; + + function isAllowEmpty(): bool { + $allowEmpty = $this->ppAllowEmpty; + if ($allowEmpty !== null) return $allowEmpty; + else return $this->ppAllowNull; + } + + /** @var bool */ + protected $ppAllowFalse; + + function isAllowFalse(): bool { + return $this->ppAllowFalse; + } + + /** @var bool */ + protected $ppAllowNull; + + function isAllowNull(): bool { + return $this->ppAllowNull; + } + + /** @var bool */ + protected $ppParseAll; + + /** @var array messages à retourner en cas d'erreur */ + protected $ppMessages; + + function setMessages(array $messages): void { + $this->ppMessages = $messages; + } + + ############################################################################# + + function __construct(?array $params=null) { + A::replace_n($params, "trim", static::TRIM); + A::replace_n($params, "allow_empty", static::ALLOW_EMPTY); + A::replace_n($params, "allow_false", static::ALLOW_FALSE); + A::replace_n($params, "allow_null", static::ALLOW_NULL); + A::replace_n($params, "parse_all", static::PARSE_ALL); + $this->initParametrableParams($params); + } + + abstract function getClass(): string; + + function getPhpType(bool $allowNullable=true): ?string { + $phpType = $this->getClass(); + if ($phpType === "mixed") return null; + if ($this->ppAllowNull && $allowNullable) $phpType = "?$phpType"; + return $phpType; + } + + function isInstance($value, bool $strict=false): bool { + if ($value === null) return $this->ppAllowNull; + $class = $this->getClass(); + switch ($class) { + case "mixed": return true; + case "bool": return is_bool($value); + case "int": return is_int($value) || (!$strict && is_float($value)); + case "float": return is_float($value) || (!$strict && is_int($value)); + case "string": return is_string($value); + case "array": return is_array($value); + case "iterable": return is_iterable($value); + case "resource": return is_resource($value); + default: return $value instanceof $class; + } + } + + function getUndefValue() { + return false; + } + + function isUndef($value, $key=null): bool { + if ($key !== null) { + if (!is_array($value)) return $key !== 0; + if (!array_key_exists($key, $value)) return true; + $value = $value[$key]; + } + return $value === false; + } + + function is2States(): bool { + return false; + } + + function get2States(): array { + throw IllegalAccessException::not_implemented(); + } + + function is3States(): bool { + return false; + } + + function get3States(): array { + throw IllegalAccessException::not_implemented(); + } + + function canFormat(): bool { + return false; + } + + function format($value): string { + throw IllegalAccessException::not_implemented(); + } + + function canParse(): bool { + return false; + } + + function parse(string &$input) { + throw IllegalAccessException::not_implemented(); + } + + ############################################################################# + + protected static function _set_error(array &$result, string $error_code, ?array $messages): void { + $error = A::get($messages, $error_code); + if ($error === null) $error = A::get(static::MESSAGES, $error_code); + if ($error === null) $error = $error_code; + foreach ($result as $key => $value) { + switch ($key) { + case "value": + case "orig": + $value = var_export($value, true); + break; + case "value_desc": + if ($value === null) $value = $result["value"]; + $value = var_export($value, true); + break; + case "orig_desc": + if ($value === null) $value = $result["orig"]; + $value = var_export($value, true); + break; + case "exception": + $value = null; + break; + } + $error = str_replace("{".$key."}", $value, $error); + } + $result["error"] = $error; + } + + protected static function result_invalid(?array &$result, string $error_code, $orig, $orig_desc, $parsed, $remains, ?array $messages, ?Exception $exception=null) { + $result = [ + "valid" => false, + "value" => $orig, + "value_desc" => $orig_desc, + "error_code" => $error_code, + "error" => null, + "exception" => $exception, + "orig" => $orig, + "orig_desc" => $orig_desc, + "parsed" => $parsed, + "remains" => $remains, + ]; + self::_set_error($result, $error_code, $messages); + return $orig; + } + + protected static function result_valid(?array &$result, $value, $orig, $parsed=false, $remains=false) { + $result = [ + "valid" => true, + "value" => $value, + "value_desc" => null, + "error_code" => null, + "error" => false, + "exception" => null, + "orig" => $orig, + "orig_desc" => null, + "parsed" => $parsed, + "remains" => $remains, + ]; + return $value; + } + + /** fonction de support: trimmer la valeur */ + function verifixTrim(string $value): string { + return trim($value); + } + + /** + * fonction de support: corriger la valeur "" le cas échéant + * + * par défaut, remplacer "" par null si null est autorisé + */ + function verifixReplaceEmpty(&$value): void { + if ($this->ppAllowNull) $value = null; + } + + /** + * fonction de support: corriger la valeur false le cas échéant + * + * par défaut, remplacer false par null si false n'est pas autorisé. + */ + function verifixReplaceFalse(&$value): void { + if (!$this->ppAllowFalse) $value = null; + } + + /** fonction de support: corriger la valeur null le cas échéant */ + function verifixReplaceNull(&$value): void { + } + + /** + * fonction de support qui implémente la vérification des valeurs non chaine + */ + protected function verifixNoParse(&$value, ?array &$result): bool { + $orig = $value; + if (is_string($value)) { + if ($this->ppTrim) $value = $this->verifixTrim($value); + if ($value === "") $this->verifixReplaceEmpty($value); + } + if ($value === false) $this->verifixReplaceFalse($value); + if ($value === null) $this->verifixReplaceNull($value); + + if ($value === null) { + if ($this->ppAllowNull) { + self::result_valid($result, $value, $orig); + } else { + $value = self::result_invalid($result, "null", $orig, null, false, false, $this->ppMessages, null); + } + return true; + } + if ($value === false) { + if ($this->ppAllowFalse) { + self::result_valid($result, $value, $orig); + } else { + $value = self::result_invalid($result, "false", $orig, null, false, false, $this->ppMessages, null); + } + return true; + } + if ($value === "" && !$this->ppAllowEmpty) { + $value = self::result_invalid($result, "empty", $orig, null, false, "", $this->ppMessages, null); + return true; + } + return false; + } + + protected abstract function _verifix(&$value, array &$result=null): void; + + function verifix(&$value, array &$result=null, bool $throw=false): bool { + $this->_verifix($value, $result); + $valid = $result["valid"]; + if (!$valid && $throw) { + $exception = $result["exception"]; + if ($exception !== null) throw $exception; + else throw new ValueException($result["error"]); + } + return $valid; + } + + function with($value) { + $this->verifix($value, $result, true); + return $value; + } + + ############################################################################# + + function getGetterName(string $name): string { + return prop::get_getter_name($name); + } + + function getSetterName(string $name): string { + return prop::get_setter_name($name); + } + + function getDeleterName(string $name): string { + return prop::get_deletter_name($name); + } + + function getClassConstName(string $name): string { + return strtoupper($name); + } + + function getObjectPropertyName(string $name): string { + return str::us2camel($name); + } + + function getArrayKeyName(string $name): string { + return $name; + } +} diff --git a/nur_src/data/types/BoolType.php b/nur_src/data/types/BoolType.php new file mode 100644 index 0000000..95f319f --- /dev/null +++ b/nur_src/data/types/BoolType.php @@ -0,0 +1,176 @@ + self::OUINON_FORMAT, + "ouinonnull" => self::OUINONNULL_FORMAT, + "on" => self::ON_FORMAT, + "onn" => self::ONN_FORMAT, + "xn" => self::XN_FORMAT, + "oz" => self::OZ_FORMAT, + ]; + + const FORMAT = self::OUINON_FORMAT; + + /** liste de valeurs chaines à considérer comme 'OUI' */ + const YES_VALUES = array( + # IMPORTANT: ordonner par taille décroissante pour compatibilité avec parse() + "true", "vrai", "yes", "oui", + "t", "v", "y", "o", "1", + ); + + /** liste de valeurs chaines à considérer comme 'NON' */ + const NO_VALUES = array( + # IMPORTANT: ordonner par taille décroissante pour compatibilité avec parse() + "false", "faux", "non", "no", + "f", "n", "0", + ); + + /** Vérifier si $value est 'OUI' */ + static final function is_yes($value): bool { + if ($value === null) return false; + if (is_bool($value)) return $value; + $value = strtolower(trim(strval($value))); + if (in_array($value, self::YES_VALUES, true)) return true; + // n'importe quelle valeur numérique + if (preg_match('/^[0-9]+$/', $value)) return $value != 0; + return false; + } + + /** Vérifier si $value est 'NON' */ + static final function is_no($value): bool { + if ($value === null) return true; + if (is_bool($value)) return !$value; + $value = strtolower(trim(strval($value))); + return in_array($value, self::NO_VALUES, true); + } + + static final function to_bool($value): bool { + if (self::is_yes($value)) return true; + elseif (self::is_no($value)) return false; + else return boolval($value); + } + + const PARAMETRABLE_PARAMS_SCHEMA = [ + "format" => [null, null, "format à appliquer"], + ]; + + /** @var array */ + protected $ppFormat; + + function pp_setFormat($format): void { + if ($format === null) $format = static::FORMAT; + if (!is_array($format)) $format = static::FORMATS[$format]; + $this->ppFormat = $format; + } + + function __construct(?array $params=null) { + parent::__construct($params); + if ($this->ppFormat === null) $this->pp_setFormat(null); + } + + function getClass(): string { + return "bool"; + } + + function isInstance($value, bool $strict=false): bool { + return is_bool($value); + } + + function getUndefValue() { + return null; + } + + function isUndef($value, $key=null): bool { + if ($key !== null) { + if (!is_array($value)) return $key !== 0; + if (!array_key_exists($key, $value)) return true; + return $value[$key] === null; + } + return $value === null; + } + + function is2States(): bool { + return true; + } + + function get2States(): array { + return [false, true]; + } + + function format($value): string { + $format = $this->ppFormat; + if ($value === null) { + $fvalue = $format[2]; + if ($fvalue !== false) return $fvalue; + } + return !self::is_no($value)? $format[0]: $format[1]; + } + + function parse(string &$input) { + if (preg_match('/^[0-9]+/', $input, $ms)) { + $value = $ms[0]; + $length = strlen($value); + $input = substr($input, $length); + return $value; + } + foreach (self::YES_VALUES as $value) { + $length = strlen($value); + if (substr($input, 0, $length) === $value) { + $input = substr($input, $length); + return $value; + } + } + foreach (self::NO_VALUES as $value) { + $length = strlen($value); + if (substr($input, 0, $length) === $value) { + $input = substr($input, $length); + return $value; + } + } + return false; + } + + function verifixConvert(&$value): bool { + if ($value === null && $this->ppAllowNull) return true; + $value = self::to_bool($value); + return true; + } + + protected function _verifix(&$value, array &$result=null): void { + $orig = $value; + if (!is_bool($value)) { + if ($value === null || $value === "") $value = null; + elseif (self::is_yes($value)) $value = true; + elseif (self::is_no($value)) $value = false; + else $value = boolval($value); + } + if ($value === null && !$this->ppAllowNull) $value = false; + self::result_valid($result, $value, $orig); + } + + function getGetterName(string $name): string { + return prop::get_getter_name($name, !$this->ppAllowNull); + } +} diff --git a/nur_src/data/types/CTimeslotType.php b/nur_src/data/types/CTimeslotType.php new file mode 100644 index 0000000..4580fb0 --- /dev/null +++ b/nur_src/data/types/CTimeslotType.php @@ -0,0 +1,23 @@ + [ + "type" => [SHourType::class], + "key" => "ts_start", + "title" => "heure de début (inclue)", + "allow_parse_empty" => true, + ], + "end" => [ + "type" => [SHourType::class], + "key" => "ts_end", + "title" => "heure de fin (non inclue)", + "allow_parse_empty" => true, + ], + ]; + const SEPARATOR = "-"; + const SEPARATOR_PATTERN = '/^\s*(?:-\s*)?/'; +} diff --git a/nur_src/data/types/ContentType.php b/nur_src/data/types/ContentType.php new file mode 100644 index 0000000..e181a36 --- /dev/null +++ b/nur_src/data/types/ContentType.php @@ -0,0 +1,42 @@ +isInstance($value)) return false; + } + return true; + } + return $value === null || + is_scalar($value) || + $value instanceof IPrintable || + $value instanceof IContent; + } + + function parse(string &$input) { + return $input; + } + + function verifixReplaceEmpty(&$value): void { + } +} diff --git a/nur_src/data/types/FileType.php b/nur_src/data/types/FileType.php new file mode 100644 index 0000000..6d9935f --- /dev/null +++ b/nur_src/data/types/FileType.php @@ -0,0 +1,51 @@ +ppAllowNull; + return is_array($value); + } + + function canFormat(): bool { + return false; + } + + protected function _verifix(&$value, array &$result=null): void { + $orig = $value; + if ($value === false) $this->verifixReplaceFalse($value); + if ($value === null) $this->verifixReplaceNull($value); + if ($value === null) { + if ($this->ppAllowNull) { + self::result_valid($result, $value, $orig); + } else { + $value = self::result_invalid($result, "null", $orig, null, false, false, $this->ppMessages); + } + return; + } + if ($value === false) { + if ($this->ppAllowFalse) { + self::result_valid($result, $value, $orig); + } else { + $value = self::result_invalid($result, "false", $orig, null, false, false, $this->ppMessages); + } + return; + } + + if (is_array($value)) { + self::result_valid($result, $value, $orig); + } else { + $value = self::result_invalid($result, "invalid", $orig, null, false, false, $this->ppMessages); + } + } +} diff --git a/nur_src/data/types/FloatType.php b/nur_src/data/types/FloatType.php new file mode 100644 index 0000000..43add81 --- /dev/null +++ b/nur_src/data/types/FloatType.php @@ -0,0 +1,48 @@ +with($value); + } + static final function to_float($value): float { + return floatval(self::to_zfloat($value)); + } + + function getClass(): string { + return "float"; + } + + function isInstance($value, bool $strict=false): bool { + return $value === null || is_float($value) || (!$strict && is_int($value)); + } + + function parse(string &$input) { + if (preg_match('/^[0-9]+(?:\.[0-9]*)?/', $input, $ms)) { + $value = $ms[0]; + $length = strlen($value); + $input = substr($input, $length); + return $value; + } + return false; + } + + protected function verifixCheckClass(&$value, $orig, array &$result=null): bool { + # accepter float et int + if (is_float($value)) $value = self::result_valid($result, $value, $orig); + elseif (is_int($value)) $value = self::result_valid($result, floatval($value), $orig); + else return false; + return true; + } + + function verifixConvert(&$value): bool { + if ($value === null && $this->ppAllowNull) return true; + $value = floatval($value); + return true; + } +} diff --git a/nur_src/data/types/GenericType.php b/nur_src/data/types/GenericType.php new file mode 100644 index 0000000..48ef4d6 --- /dev/null +++ b/nur_src/data/types/GenericType.php @@ -0,0 +1,91 @@ +class = $class; + $this->args = $args; + #XXX faut-il vérifier que la classe est valide? + } + + /** @var string */ + protected $class; + + /** @var array */ + protected $args; + + function getClass(): string { + $class = $this->class; + switch ($class) { + case self::ARRAY_ARRAY: $class = self::ARRAY; break; + } + return $class; + } + + function isInstance($value, bool $strict=false): bool { + if ($value === null) return $this->ppAllowNull; + $class = $this->getClass(); + switch ($class) { + case self::ARRAY: + case self::ARRAY_ARRAY: + return is_array($value); + case self::ITERABLE: return is_iterable($value); + case self::RESOURCE: return is_resource($value); + default: return $value instanceof $class; + } + } + + function canFormat(): bool { + return false; + } + + protected function _verifix(&$value, array &$result=null): void { + $orig = $value; + if ($this->verifixNoParse($value, $result)) return; + + $class = $this->getClass(); + switch ($class) { + case self::ARRAY: + A::ensure_array($value); + break; + case self::ARRAY_ARRAY: + A::ensure_array($value); + foreach ($value as &$item) { + A::ensure_array($item); + }; unset($item); + break; + case self::ITERABLE: + if (!($value instanceof Traversable)) A::ensure_array($value); + break; + case self::RESOURCE: + if (!is_resource($value)) { + $value = self::result_invalid($result, "invalid", $orig, null + , false, false + , $this->ppMessages, ValueException::unexpected_type(self::RESOURCE, $value)); + return; + } + break; + default: + $args = $this->args; + A::merge($args, A::with($value)); + $value = func::cons($class, ...$args); + } + self::result_valid($result, $value, $orig); + } +} diff --git a/nur_src/data/types/IIncarnation.php b/nur_src/data/types/IIncarnation.php new file mode 100644 index 0000000..b79192a --- /dev/null +++ b/nur_src/data/types/IIncarnation.php @@ -0,0 +1,11 @@ + [null, false, "est-ce que la valeur fournie est valide?"], + "value" => [null, null, "valeur corrigée si elle est valide, valeur originale si elle est invalide"], + "error" => [null, false, "message d'erreur si la valeur fournie est invalide"], + "exception" => [null, null, "exception associée à l'erreur le cas échaéant"], + "orig" => [null, null, "valeur originale avant correction"], + "parsed" => [null, false, "partie de la chaine originale qui a pu être analysée si valid==false"], + "remains" => [null, false, "partie de la chaine originale non analysée"], + ]; + + /** + * vérifier $value qui a été fourni par l'utilisateur: + * - si elle déjà du bon type et au bon format, la laisser en l'état + * - si elle est du bon type mais pas au bon format, elle est normalisée + * - sinon, ce doit être une chaine de caractère, qui est analysée et doit + * correspondre entièrement (sauf si l'implémentation autorise que l'analyse + * soit incomplète) + * + * si la valeur est invalide elle est laissée inchangée. dans tous les cas, + * $result est mis à jour avec les clés suivantes: + * - "valid" => true ou false, indique si la valeur est valide + * - "value" => valeur normalisée si elle est valide + * - "error" => message d'erreur si la valeur est invalide, ou false si la + * valeur est valide + * - "exception" => si la valeur est invalide, exception éventuellement + * associée à l'erreur + * - "orig" => valeur originale de $value + * - "parsed" => partie de la chaine originale qui a pu être analysée si + * la valeur est invalide + * - "remains" => le reste non analysé de la chaine si l'implémentation + * autorise que la chaine en entrée ne soit pas analysée entièrement ou si + * la valeur est invalide + * + * si plusieurs messages d'erreur sont possibles, $params["messages"] permet + * de spécifier des messages différents des valeurs par défaut. les clés + * valides sont documentées dans l'implémentation + * + * @param mixed &$value la valeur à analyser et normaliser + * @param array $result le résultat de l'analyse + * @param bool $throw faut-il lancer une exception si la valeur est invalide? + * @return bool si la valeur est valide + * @throws IllegalAccessException si {@link canParse()} retourne false + * @throws Exception si la valeur est invalide, que $throw==true et que le + * champ exception de $result a été renseigné + * @throws ValueException si la valeur est invalide, que $throw==true et que + * le champ exception de $result n'a pas été renseigné + */ + function verifix(&$value, array &$result=null, bool $throw=false): bool; + + /** + * Méthode de convenance pour retourner la valeur normalisée et lancer une + * exception en cas d'erreur + */ + function with($value); + + ############################################################################# + + /** @return string le nom d'un getter pour une valeur de ce type */ + function getGetterName(string $name): string; + + /** @return string le nom d'un setter pour une valeur de ce type */ + function getSetterName(string $name): string; + + /** @return string le nom d'un deleter pour une valeur de ce type */ + function getDeleterName(string $name): string; + + /** + * @return string le nom d'une constante de classe pour une valeur de ce type + */ + function getClassConstName(string $name): string; + + /** + * @return string le nom d'une propriété d'une classe pour une valeur de ce + * type + */ + function getObjectPropertyName(string $name): string; + + /** @return string le nom d'une clé d'un tableau pour une valeur de ce type */ + function getArrayKeyName(string $name): string; +} diff --git a/nur_src/data/types/IntType.php b/nur_src/data/types/IntType.php new file mode 100644 index 0000000..5d4ca7c --- /dev/null +++ b/nur_src/data/types/IntType.php @@ -0,0 +1,49 @@ +with($value); + } + static final function to_int($value): int { + return intval(self::to_zint($value)); + } + + function getClass(): string { + return "int"; + } + + function isInstance($value, bool $strict=false): bool { + return $value === null || is_int($value) + || (!$strict && (is_float($value) || is_bool($value))); + } + + function parse(string &$input) { + if (preg_match('/^-?[0-9]+/', $input, $ms)) { + $value = $ms[0]; + $input = substr($input, strlen($value)); + return $value; + } + return false; + } + + protected function verifixCheckClass(&$value, $orig, array &$result=null): bool { + # accepter int, float et true + if (is_int($value)) $value = self::result_valid($result, $value, $orig); + elseif (is_float($value)) $value = self::result_valid($result, intval($value), $orig); + elseif ($value === true) $value = self::result_valid($result, intval($value), $orig); + else return false; + return true; + } + + function verifixConvert(&$value): bool { + if ($value === null && $this->ppAllowNull) return true; + $value = intval($value); + return true; + } +} diff --git a/nur_src/data/types/KeyType.php b/nur_src/data/types/KeyType.php new file mode 100644 index 0000000..113f264 --- /dev/null +++ b/nur_src/data/types/KeyType.php @@ -0,0 +1,35 @@ + $sfield} où $sfield est un + * tableau conforme au schéma {@link md::MSFIELD_SCHEMA}. Si $sfield n'est pas + * un tableau, alors c'est la valeur par défaut + * + * l'élément {"" => $sinfos}, où $sinfos est un tableau conforme au schéma + * {@link MSINFOS_SCHEMA}, est particulier: il définit des caractéristiques qui + * concernent l'objet tout entier. + * + * --- ancienne doc à migrer -------------------------------------------------- + * Un méta-schéma sert à décrire les champs d'un schéma de données, e.g + * ~~~ + * const FIELD_METASCHEMA = [ + * "name" => [null, null, "nom de la colonne", true], + * "title" => [null, null, "libellé de la colonne", false], + * ]; + * ~~~ + * Ce méta-schéma permettrait de décrire les champs d'un schéma comme celui-là: + * ~~~ + * const DATA_SCHEMA = [ + * "nom" => ["name" => "sn", "title" => "nom"], + * "prenom" => ["name" => "givenName", "title" => "prénom"], + * "pass" => ["name" => "password", "title" => "mot de passe"], + * ] + * ~~~ + * + * Bien entendu, un méta-schéma peut servir aussi de schéma simple pour un + * tableau de données + */ +class Metadata implements IParametrable { + use _Tparametrable1; + + private static function itype($type): IType { + return $type; + } + private static function citype($type): AbstractCompositeType { + return $type; + } + + static final function with($schema): Metadata { + if ($schema instanceof Metadata) return $schema; + elseif (is_array($schema)) return new static($schema); + else throw ValueException::unexpected_type([Metadata::class, "array"], $schema); + } + + const ENSURE_KEYS = true; + const ORDER_KEYS = true; + const ENSURE_TYPES = true; + const CHECK_REQUIRED = true; + + const PARAMETRABLE_PARAMS_SCHEMA = [ + "ensure_keys" => ["bool", null, "toutes les clés doivent-elles être présentes?"], + "order_keys" => ["bool", null, "les clés doivent-elle être dans l'ordre du schéma?"], + "ensure_types" => ["bool", null, "les valeurs doivent-elles être du bon type?", + "desc" => "cela ne concerne que les types simples standard", + ], + "check_required" => ["bool", null, "vérifier que les valeurs requises sont présentes?"], + ]; + + function __construct(array $schema, ?array $params=null, ?IIncarnation $incarnation=null) { + if ($incarnation === null) $incarnation = types::manager(); + $this->incarnation = $incarnation; + $this->schema = self::_normalize_schema($schema); + + $this->initParametrableParams($params); + base::update_n($this->ppEnsureKeys, static::ENSURE_KEYS); + base::update_n($this->ppOrderKeys, static::ORDER_KEYS); + base::update_n($this->ppEnsureTypes, static::ENSURE_TYPES); + base::update_n($this->ppCheckRequired, static::CHECK_REQUIRED); + } + + /** @var bool */ + protected $ppEnsureKeys; + + /** @var bool */ + protected $ppOrderKeys; + + /** @var bool */ + protected $ppEnsureTypes; + + /** @var bool */ + protected $ppCheckRequired; + + const SINFOS_SCHEMA = [ + "ctypes" => [ + "type" => "?array", + "default" => null, + "title" => "liste de types composites à appliquer à l'objet", + "required" => false, + "key" => "ctypes", + "header" => "ctypes", + "desc" => null, + "schema" => null, + "composite" => false, + ], + "apply2items" => [ + "type" => "bool", + "default" => false, + "title" => "le schéma s'applique-t-il aux éléments d'une liste séquentielle", + "required" => false, + "key" => "apply2items", + "header" => "apply2items", + "desc" => null, + "schema" => null, + "composite" => false, + ], + ]; + const SINFOS_INDEXES = ["ctypes" => 0, "apply2items" => 1]; + + protected static function _normalize_schema(array $schema): array { + if (count($schema) == 1 && array_key_exists(0, $schema)) { + $apply2items = true; + $schema = $schema[0]; + } else { + $apply2items = false; + } + $sfields = []; + $sinfos = ["ctypes" => null, "apply2items" => $apply2items]; # doit être conforme au schéma ci-dessus + $cokeys = []; + $cikeys = []; + foreach ($schema as $key => $sfield) { + if ($key !== "") { + md::_ensure_msfield($sfield, $key); + + $type = $sfield["type"]; + $is_array_type = in_array($type, md::ARRAY_TYPES); + if ($sfield["schema"] !== null) { + if ($is_array_type) { + $sfield["schema"] = self::_normalize_schema($sfield["schema"]); + if (in_array($type, md::APPLY2ITEMS_TYPES)) { + $sfield["schema"]["sinfos"]["apply2items"] = true; + } + } else { + $sfield["schema"] = null; + } + } + if ($sfield["composite"]) $cokeys[] = $key; + $sfields[$key] = $sfield; + } else { + $sinfos = $sfield; + md::_ensure_schema($sinfos, self::SINFOS_SCHEMA, self::SINFOS_INDEXES); + $sinfos["apply2items"] = $apply2items; + $citypes = $sinfos["ctypes"]; + if ($citypes !== null) $cikeys = array_keys($citypes); + } + }; unset($sfield); + return [ + "sfields" => $sfields, + "sinfos" => $sinfos, + "indexes" => array_flip(array_keys($sfields)), + "cokeys" => $cokeys, + "cikeys" => $cikeys, + "types" => null, + ]; + } + + /** @var IIncarnation */ + protected $incarnation; + + const schema_SCHEMA = [ + "sfields" => ["array", null, "liste des schémas des champs"], + "sinfos" => ["array", null, "informations sur l'objet entier", + "schema" => self::SINFOS_SCHEMA, + ], + "indexes" => ["array", null, "liste des indexes des clés, de la forme {key => index}"], + "cokeys" => ["array", null, "liste des clés de champs composantes"], + "cikeys" => ["array", null, "liste des clés de champs composites"], + "types" => ["?array", null, "liste des types pour chaque élément de [skeys]"], + ]; + + /** @var array */ + protected $schema; + + /** obtenir le schéma normalisé */ + function getSchema(): array { + $schema = ["" => $this->schema["sinfos"]]; + $schema = array_merge($schema, $this->schema["sfields"]); + if ($this->schema["sinfos"]["apply2items"]) $schema = [$schema]; + return $schema; + } + + /** retourner la définition des champs */ + function getSfields(): array { + return $this->schema["sfields"]; + } + + /** retourner les clés des champs non composantes */ + function getSikeys(): array { + $allKeys = array_keys($this->schema["sfields"]); + return array_diff($allKeys, $this->schema["cokeys"]); + } + + /** retourner les clés des champs composantes */ + function getCokeys(): array { + return $this->schema["cokeys"]; + } + + /** retourner toutes les clés simples */ + function getKeys(): array { + return array_keys($this->schema["sfields"]); + } + + /** tester si $key est une clé simple valide */ + function isKey(string $key): bool { + return in_array($key, $this->getKeys()); + } + + /** retourner les clés des champs composites */ + function getCikeys(): array { + return $this->schema["cikeys"]; + } + + /** retourner toutes les clés (les clés simples et les clés composites) */ + function getAllKeys(): array { + $schema = $this->schema; + return array_merge(array_keys($schema["sfields"]), $schema["cikeys"]); + } + + /** obtenir l'instance de type correspondant au champ spécifié */ + function getType(string $key, bool $required=true): ?IType { + $types = $this->_getTypes($this->schema); + $type = A::get($types, $key); + if ($type === null && $required) { + throw ValueException::invalid_value($key, "type"); + } + return $type; + } + + protected function _getTypes(array &$schema): array { + if ($schema["types"] === null) { + $types = []; + foreach ($schema["sfields"] as $key => &$sfield) { + $types[$key] = $this->_buildType($sfield["type"], $sfield); + if ($sfield["schema"] !== null) { + $this->_getTypes($sfield["schema"]); + } + } + $ctypes = $schema["sinfos"]["ctypes"]; + if ($ctypes !== null) { + foreach ($ctypes as $key => $type) { + $types[$key] = $this->_buildType($type); + } + } + $schema["types"] = $types; + } + return $schema["types"]; + } + + private function _buildType($type, $sfield=null): IType { + if (!$this->incarnation->hasType($type) && is_array($type)) { + $tkey = array_key_first($type); + if (count($type) == 1) { + # syntaxe [tkey => Type], dans ce cas c'est la définition du + # champ qui est passé en paramètres au type + $type = [$tkey => $type[$tkey], $sfield]; + } + } + return $this->incarnation->getType($type); + } + + ############################################################################# + + /** + * s'assurer que $item est conforme à la structure du schéma, c'est à dire que + * tous les champs sont présents dans l'ordre. + * + * de plus, par défaut, si les champs ont des types connus, ils sont honorés. + * les autres champs sont laissés en l'état. + * + * $item peut être un tableau séquentiel ou associatif (ou un mix des deux). + * s'assurer que toutes les clés définies dans le schéma existent dans $item + * en les créant avec la valeur par défaut le cas échéant. + * + * soit une clé $key du schéma: + * - avec le type par défaut, si $item[$key] === false alors on considère que + * cette clé n'existe pas et elle est remplacée par la valeur par défaut. + * Si un type est défini, le comportement peut être différent. cf la doc de + * {@link md::MSFIELD_SCHEMA} pour les détails + * - si la clé n'existe pas dans $item (même pas avec la valeur false), + * alors chercher la première valeur séquentielle qui n'a pas encore été + * analysée, et prendre cela comme valeur. bien entendu, si la valeur est + * false, les mêmes règles que ci-dessus s'appliquent + * + * après l'appel de cette fonction, $item est un tableau associatif. les + * clés séquentielles qui n'ont pas été utilisées sont gardées mais + * renumérotées + * ~~~ + * $item = [1, 2, 3]; + * $md = new Metadata(["a" => "x", "b" => "y"]); + * $md->ensureSchema($item); + * // maintenant, $item vaut ["a" => 1, "b" => 2, 3]; + * ~~~ + * + * si $recursive==true, alors les éléments ayant un sous-schéma sont examinés + * et corrigés aussi le cas échéant. + * + * Comme cas particulier, si $schema contient un unique élément de type array + * avec l'index 0, alors $sfield est transformé en tableau et chacun de ses + * éléments est corrigé pour être conforme au schema $schema[0]. Pour cela, + * il faut que $recursive==true + * + * $key est utilisé dans le cas où les données proviennent d'un tableau de la + * forme [..., $key => $sfield, ...] + * si $key n'est pas null, alors on considère que c'est la première valeur de + * du tableau $sfield. Par exemple, les deux ensembles de commandes suivants + * sont équivalents: + * ~~~ + * $schema = ["a" => "x", "b" => "y"]; + * # sans $key + * $sfield = [1, 2]; + * metaschema::ensure_schema($sfield, $schema); + * // $sfield vaut maintenant ["a" => 1, "b" => 2] + * # avec $key + * $sfield = [2]; + * metaschema::ensure_schema($sfield, $schema, 1); + * // $sfield vaut maintenant ["a" => 1, "b" => 2] + * ~~~ + */ + function ensureSchema(&$item, $key=null, ?array $params=null): void { + $ensureKeys = A::get($params, "ensure_keys", $this->ppEnsureKeys); + $orderKeys = A::get($params, "order_keys", $this->ppOrderKeys); + $ensureTypes = A::get($params, "ensure_types", $this->ppEnsureTypes); + $checkRequired = A::get($params, "check_required", $this->ppCheckRequired); + $recursive = A::get($params, "recursive", true); + + $schema = $this->schema; + $this->_ensureArrayItem($item, $schema, $key); + $this->_ensureSchema($item, $schema, $ensureKeys, $ensureTypes, $recursive); + if ($orderKeys) $this->orderKeys($item, $recursive); + if ($checkRequired) $this->checkRequired($item, $recursive); + } + + private function _ensureArrayItem(&$item, array $schema, $itemKey) { + if ($itemKey !== null) { + if (is_array($item)) { + # n'utiliser key que si la première clé du schéma n'existe pas + $sfields = $schema["sfields"]; + $first_key = array_key_first($sfields); + if (!array_key_exists($first_key, $item)) { + $tmp = [$itemKey]; + A::merge3($tmp, $item); + $item = $tmp; + } + } else { + $item = [$itemKey, $item]; + } + } elseif ($item !== null && !is_array($item)) { + $item = [$item]; + } + } + + private function _ensureSchema(?array &$item, array $schema, bool $ensureKeys, bool $ensureTypes, bool $recursive, ?bool $apply2items=null): void { + [ + "sfields" => $sfields, + "sinfos" => $sinfos, + "indexes" => $indexes, + #"cokeys" => $cokeys, + #"cikeys" => $cikeys, + #"types" => $types, + ] = $schema; + if ($apply2items === null) $apply2items = $sinfos["apply2items"]; + if ($apply2items) { + $items =& $item; unset($item); + $index = 0; + foreach ($items as $key => &$item) { + if ($key === $index) { + $index++; + $key = null; + } + $this->_ensureArrayItem($item, $schema, $key); + $this->_ensureSchema($item, $schema, $ensureKeys, $ensureTypes, $recursive, false); + }; unset($item); + return; + } + + if ($item === null) $item = []; + $src = $item; + $keys = array_keys($sfields); + $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, $sfields); + if ($ensureTypes && $is_schema_key) { + self::_ensure_type($sfields[$key], $value, 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]) { + if ($ensureTypes) { + self::_ensure_type($sfields[$key], $value, 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 + if ($ensureKeys) { + foreach ($dones as $dindex => $done) { + if (!$done) { + $key = $keys[$dindex]; + $sfield = $sfields[$key]; + $value = $sfield["default"]; + if ($ensureTypes) { + self::_ensure_type($sfield, $value, false); + } + $item[$key] = $value; + } + } + } + + if ($recursive) { + foreach ($sfields as $key => $sfield) { + $schema2 = $sfield["schema"]; + if ($schema2 === null) continue; + switch ($sfield["type"]) { + case "?array": + if ($item[$key] === null) continue 2; + case "array": + $this->_ensureArrayItem($item[$key], $schema2, null); + $this->_ensureSchema($item[$key], $schema2, $ensureKeys, $ensureTypes, true, false); + break; + case "?array[]": + if ($item[$key] === null) continue 2; + case "array[]": + $this->_ensureSchema($item[$key], $schema2, $ensureKeys, $ensureTypes, true, true); + break; + } + } + } + } + + private static function _ensure_type($sfield, &$value, bool $exists): void { + $type = $sfield["type"]; + $default = $sfield["default"]; + if (md::_check_known_type($type, $value, $default, $exists)) { + md::_convert_value($type, $value); + } + } + + ############################################################################# + + /** ordonner les champs de item selon l'ordre du schéma */ + function orderKeys(array &$item, bool $recursive=true): void { + $sfields = $this->schema["sfields"]; + $indexes = $this->schema["indexes"]; + if ($this->schema["sinfos"]["apply2items"]) { + $this->_eachOrderKeys($sfields, $indexes, $item, $recursive); + } else { + $this->_orderKeys($sfields, $indexes, $item, $recursive); + } + } + + function _eachOrderKeys(array $sfields, array $indexes, array &$items, bool $recursive) { + foreach ($items as &$item) { + $this->_orderKeys($sfields, $indexes, $item, $recursive); + }; unset($item); + } + + function _orderKeys(array $sfields, array $indexes, array &$item, bool $recursive) { + $maxIndex = count($sfields); + $index = 0; + $ordered = true; + foreach (array_keys($item) as $key) { + if (!key_exists($key, $sfields) || $indexes[$key] !== $index) { + $ordered = false; + break; + } + $index++; + if ($index == $maxIndex) break; + } + if ($ordered) return; + + $remaining = $item; + $ordered = []; + foreach (array_keys($sfields) as $key) { + if (array_key_exists($key, $remaining)) { + $ordered[$key] = $remaining[$key]; + unset($remaining[$key]); + } + } + $item = []; + A::merge2($item, $ordered, $remaining); + + if ($recursive) { + foreach ($sfields as $key => $sfield) { + $schema2 = $sfield["schema"]; + if ($schema2 === null || $item[$key] === null) continue; + $sfields2 = $schema2["sfields"]; + $indexes2 = $schema2["indexes"]; + if ($schema2["sinfos"]["apply2items"]) { + $this->_eachOrderKeys($sfields2, $indexes2, $item[$key], $recursive); + } else { + $this->_orderKeys($sfields2, $indexes2, $item[$key], $recursive); + } + } + } + } + + /** + * 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} + * + * $item n'a pas besoin d'être conforme au schéma au préalable + */ + function checkRequired(array $item, bool $recursive=true): void { + $sfields = $this->schema["sfields"]; + if ($this->schema["sinfos"]["apply2items"]) { + $this->_eachCheckRequired($sfields, "", $item, $recursive); + } else { + $this->_checkRequired($sfields, "", $item, $recursive); + } + } + + private function _eachCheckRequired(array $sfields, $keyPrefix, array $items, bool $recursive): void { + foreach ($items as $key => $item) { + $this->_checkRequired($sfields, "$keyPrefix$key.", $item, $recursive); + } + } + + private function _checkRequired(array $sfields, $keyPrefix, array $item, bool $recursive, ?bool $apply2items=null): void { + foreach ($sfields as $key => $sfield) { + if ($sfield["required"]) { + if (!array_key_exists($key, $item) || $item[$key] === null) { + throw new ValueException("$keyPrefix$key is required"); + } + } + $schema2 = $sfield["schema"]; + if ($schema2 !== null && $recursive) { + A::ensure_array($item[$key]); + $sfields2 = $schema2["sfields"]; + $keyPrefix = "$keyPrefix$key."; + if ($schema2["sinfos"]["apply2items"]) { + $this->_eachCheckRequired($sfields2, $keyPrefix, $item[$key], $recursive); + } else { + $this->_checkRequired($sfields2, $keyPrefix, $item[$key], $recursive); + } + } + } + } + + ############################################################################# + + /** + * Vérifier si la clé $key existe dans $item, en tenant compte des + * informations 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 séquentiel + * + * les clés composites ne sont pas supportées (le résultat est toujours false + * même si le tableau contient une clé du nom de la clé composite) + * + * XXX si le type n'est pas bool, interpréter false comme "non présent" + */ + function has($item, $key): bool { + $cikeys = $this->schema["cikeys"]; + $sfields = $this->schema["sfields"]; + if (in_array($key, $cikeys)) { + # valeur composite + return false; + } elseif (!array_key_exists($key, $sfields)) { + return is_array($item) && array_key_exists($key, $item); + } else { + if (!is_array($item)) $item = [$item]; + if (array_key_exists($key, $item)) return true; + $index = $this->schema["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 séquentiel + * + * les clés composites sont supportées et sont retournées sous forme de + * tableau + */ + function get($item, $key, $default=null, bool $ensure_type=true) { + $cikeys = $this->schema["cikeys"]; + $sfields = $this->schema["sfields"]; + if (in_array($key, $cikeys)) { + # valeur composite + if (!is_array($item)) $item = [$item]; + $values = []; + $type = self::citype($this->getType($key)); + foreach ($type->getComponents() as $cvalue) { + $key = $cvalue["key"]; + $sfield = $sfields[$key]; + $values[$key] = $this->_get($sfield, $item, $key, null, $ensure_type); + } + return $values; + } elseif (!array_key_exists($key, $sfields)) { + if (is_array($item) && array_key_exists($key, $item)) { + return $item[$key]; + } else { + return $default; + } + } else { + if (!is_array($item)) $item = [$item]; + $sfield = $sfields[$key]; + return $this->_get($sfield, $item, $key, $default, $ensure_type); + } + } + + private function _get(array $sfield, ?array $item, $key, $default=null, bool $ensureType=true) { + if (array_key_exists($key, $item)) { + $exists = true; + $value = $item[$key]; + } else { + $index = $this->schema["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 ($ensureType) { + self::_ensure_type($sfield, $value, $exists); + $schema2 = $sfield["schema"]; + if ($schema2 !== null) $this->_ensureSchema($value, $schema2, $this->ppEnsureKeys, $this->ppEnsureTypes, true); + } + return $value; + } + + /** + * spécifier la valeur de la clé $key dans $item en tenant compte des + * informations du schéma. + * + * les clés composites sont supportées et doivent être fournies sous forme de + * tableau + */ + function set(?array &$item, $key, $value): void { + $cikeys = $this->schema["cikeys"]; + $sfields = $this->schema["sfields"]; + if (in_array($key, $cikeys)) { + # valeur composite + if ($value === null) return; + $type = self::citype($this->getType($key)); + if (!is_array($value)) { + throw ValueException::unexpected_type("array", $value); + } + foreach ($type->getComponents() as $cname => $cvalue) { + $ctype = $type->getCtype($cname); + $key = $cvalue["key"]; + if (array_key_exists($key, $value)) { + $item[$key] = $ctype->with($this->get($value, $key)); + } + } + } elseif (!array_key_exists($key, $sfields)) { + $item[$key] = $value; + } else { + $type = $this->getType($key); + $item[$key] = $type->with($value); + } + } + + /** + * sélectionner dans $items les valeurs du schéma, et les retourner dans + * l'ordre du schéma sous forme de tableau associatif. + * + * $item doit aussi ê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, $ensureType==true permet + * de s'assurer aussi que les valeurs sont dans le bon type + */ + function getValues(?array $item, bool $ensureType=false): array { + if ($item === null) return []; + $values = []; + foreach ($this->schema["sfields"] as $key => $sfield) { + if (!array_key_exists($key, $item)) continue; + $value = $item[$key]; + if ($ensureType) { + self::_ensure_type($sfield, $value, true); + } + $values[$key] = $value; + } + return $values; + } + + /** + * complément de {@link getValues()}: retourner les clés qui n'ont pas été + * sélectionnées + */ + function getOthers(?array $item): array { + if ($item === null) return []; + $sfields = $this->schema["sfields"]; + $others = []; + foreach ($item as $key => $value) { + if (array_key_exists($key, $sfields)) continue; + $others[$key] = $value; + } + return $others; + } + + /** + * formatter chacun des champs de $item. on assume que $item est déjà conforme + * au schéma + */ + function format($item): array { + $cikeys = $this->schema["cikeys"]; + $types = $this->_getTypes($this->schema); + $result = []; + foreach ($this->getAllKeys() as $key) { + $type = self::itype($types[$key]); + if (in_array($key, $cikeys)) { + $value = $type->format($item); + } else { + $value = $type->format($item[$key]); + } + $result[$key] = $value; + } + return $result; + } + + /** + * vérifier et corriger chaque champ de $item. on assume que $item est déjà + * conforme au moins à la structure du schéma + */ + function verifix(&$item, ?array &$results=null, bool $throw=false, bool $recursive=false): void { + $this->_getTypes($this->schema); + $this->_verifix($item, $results, $throw, $this->schema, $recursive); + } + + function _verifix(&$item, ?array &$results, bool $throw, array $schema, bool $recursive): void { + foreach ($schema["sfields"] as $key => $sfield) { + if (!$sfield["composite"]) { + $type = self::itype($schema["types"][$key]); + $type->verifix($item[$key], $results[$key], $throw); + } + if ($recursive && $sfield["schema"] !== null) { + $this->_verifix($item[$key], $results[$key], $throw, $sfield["schema"], true); + } + } + foreach ($schema["cikeys"] as $key) { + $type = self::itype($schema["types"][$key]); + $type->verifix($item, $results[$key], $throw); + } + } + + /** + * méthode de convenance pour corriger de façon récursive tous les types, et + * lancer une exception en cas d'erreur + */ + function ensureTypes(&$item): void { + $this->verifix($item, $results, true, true); + } + + ############################################################################# + + function eachEnsureSchema(?iterable &$items, ?array $params=null): void { + if ($items === null) return; + $index = 0; + foreach ($items as $key => &$item) { + if ($key === $index++) $key = null; + $this->ensureSchema($item, $key, $params); + }; unset($item); + } + + function eachEnsureTypes(?iterable &$items): void { + if ($items === null) return; + foreach ($items as &$item) { + $this->ensureTypes($item); + }; unset($item); + } + + function eachCheckRequired(?iterable $items, bool $recursive=true): void { + if ($items === null) return; + foreach ($items as $item) { + $this->checkRequired($item, $recursive); + } + } + + function eachFormat(?iterable $items): ?array { + if ($items === null) return null; + $result = []; + foreach ($items as $item) { + $result[] = $this->format($item); + } + return $result; + } + + function eachVerifix(?iterable &$items, ?array &$itemResults=null, bool $throw=false, bool $recursive=false): void { + if ($items === null) return; + $itemResults = []; + foreach ($items as $key => &$item) { + $this->verifix($item, $results, $throw, $recursive); + $itemResults[$key] = $results; + }; unset($item); + } +} diff --git a/nur_src/data/types/MixedType.php b/nur_src/data/types/MixedType.php new file mode 100644 index 0000000..a604cc1 --- /dev/null +++ b/nur_src/data/types/MixedType.php @@ -0,0 +1,30 @@ +types[ref_type::MIXED] = new MixedType(["allow_null" => false]); + $this->types[ref_type::BOOL] = new BoolType(["allow_null" => false]); + $this->types[ref_type::INT] = new IntType(["allow_null" => false]); + $this->types[ref_type::FLOAT] = new FloatType(["allow_null" => false]); + $this->types[ref_type::RAWSTRING] = new RawStringType(["allow_null" => false]); + $this->types[ref_type::STRING] = new StringType(["allow_null" => false]); + $this->types[ref_type::TEXT] = new TextType(["allow_null" => false]); + $this->types[ref_type::KEY] = new KeyType(["allow_null" => false]); + $this->types[ref_type::CONTENT] = new ContentType(["allow_null" => false]); + $this->types[ref_type::FILE] = new FileType(["allow_null" => false]); + $this->types[ref_type::DATETIME] = new SDatetimeType(["allow_null" => false]); + $this->types[ref_type::DATE] = new SDateType(["allow_null" => false]); + $this->types[ref_type::TIME] = new STimeType(["allow_null" => false]); + $this->types[ref_type::HOUR] = new SHourType(["allow_null" => false]); + # nullable + $this->types[ref_type::NMIXED] = new MixedType(["allow_null" => true]); + $this->types[ref_type::NBOOL] = new BoolType(["allow_null" => true]); + $this->types[ref_type::TRIBOOL] = new TriboolType(["allow_null" => true]); + $this->types[ref_type::NTRIBOOL] = new TriboolType(["allow_null" => true]); + $this->types[ref_type::NINT] = new IntType(["allow_null" => true]); + $this->types[ref_type::NFLOAT] = new FloatType(["allow_null" => true]); + $this->types[ref_type::NRAWSTRING] = new RawStringType(["allow_null" => true]); + $this->types[ref_type::NSTRING] = new StringType(["allow_null" => true]); + $this->types[ref_type::NTEXT] = new TextType(["allow_null" => true]); + $this->types[ref_type::NKEY] = new KeyType(["allow_null" => true]); + $this->types[ref_type::NCONTENT] = new ContentType(["allow_null" => true]); + $this->types[ref_type::NFILE] = new FileType(["allow_null" => true]); + $this->types[ref_type::NDATETIME] = new SDatetimeType(["allow_null" => true]); + $this->types[ref_type::NDATE] = new SDateType(["allow_null" => true]); + $this->types[ref_type::NTIME] = new STimeType(["allow_null" => true]); + $this->types[ref_type::NHOUR] = new SHourType(["allow_null" => true]); + # generic + $this->types[ref_type::NARRAY] = new GenericType(GenericType::ARRAY, ["allow_null" => true]); + $this->types[ref_type::ARRAY] = new GenericType(GenericType::ARRAY, ["allow_null" => false]); + $this->types[ref_type::NARRAY_ARRAY] = new GenericType(GenericType::ARRAY_ARRAY, ["allow_null" => true]); + $this->types[ref_type::ARRAY_ARRAY] = new GenericType(GenericType::ARRAY_ARRAY, ["allow_null" => false]); + $this->types[ref_type::NITERABLE] = new GenericType(GenericType::ITERABLE, ["allow_null" => true]); + $this->types[ref_type::ITERABLE] = new GenericType(GenericType::ITERABLE, ["allow_null" => false]); + $this->types[ref_type::NRESOURCE] = new GenericType(GenericType::RESOURCE, ["allow_null" => true]); + $this->types[ref_type::RESOURCE] = new GenericType(GenericType::RESOURCE, ["allow_null" => false]); + } + + const CLASS_ALIASES = [ + ref_type::BOOL => BoolType::class, + ref_type::TRIBOOL => TriboolType::class, + ref_type::INT => IntType::class, + ref_type::FLOAT => FloatType::class, + ref_type::RAWSTRING => RawStringType::class, + ref_type::STRING => StringType::class, + ref_type::TEXT => TextType::class, + ref_type::KEY => KeyType::class, + ref_type::CONTENT => ContentType::class, + ref_type::FILE => FileType::class, + ref_type::DATETIME => SDatetimeType::class, + ref_type::DATE => SDateType::class, + ref_type::TIME => STimeType::class, + ref_type::HOUR => SHourType::class, + ]; + + /** @var array liste d'associations {nom type => classe} */ + protected $typeClasses = []; + + /** @var array liste d'associations {nom type => instance de types} */ + protected $types = []; + + /** @var array liste de définitions de types avec leurs arguments */ + protected $dynamicTypes = []; + + private static function fix_name(&$name) { + if ($name === null) $name = "mixed"; + } + + function hasType($name): bool { + self::fix_name($name); + + if (is_array($name)) { + $key = array_key_first($name); + if ($key !== 0) { + # type dynamique nommé + $name = $key; + } else { + # type dynamique + foreach ($this->dynamicTypes as $dtype) { + if ($name === $dtype) return true; + } + return false; + } + } elseif (!array_key_exists($name, $this->types)) { + $name = A::get(ref_type::ALIASES, $name, $name); + } + # type statique ou dynamique nommé + $type = A::get($this->types, $name); + if ($type !== null) return true; + return A::get($this->typeClasses, $name) !== null; + } + + function getType($name, bool $required=true): ?IType { + self::fix_name($name); + + ## vérifier si le type est déjà défini + $dclass = null; + $dtype = null; + if (is_array($name)) { + $dtype = $name; + $key = array_key_first($dtype); + if ($key !== 0) { + # type dynamique nommé + $name = $key; + $dclass = $dtype[$name]; + unset($dtype[$name]); + array_unshift($dtype, $dclass); + } else { + # type dynamique anonyme + $name = null; + } + } elseif (!array_key_exists($name, $this->types)) { + $name = A::get(ref_type::ALIASES, $name, $name); + } + if ($name === null) { + ## type dynamique anonyme + foreach ($this->dynamicTypes as $dname => $dynamicType) { + if ($dtype === $dynamicType) { + $name = $dname; + break; + } + } + } + if ($name !== null) { + $type = A::get($this->types, $name); + if ($type !== null) return $type; + } + + ## il faut créer le type + if ($name === null) { + # type dynamique + $name = count($this->dynamicTypes) + 1; + $this->dynamicTypes[$name] = $dtype; + } + + if ($dtype !== null) { + $class = $dtype[0]; + $class = A::get(ref_type::ALIASES, $class, $class); + $class = A::get(self::CLASS_ALIASES, $class, $class); + $args = array_slice($dtype, 1); + if (!is_subclass_of($class, IType::class)) { + # si la classe n'implémente pas IType, prendre le type générique + $class = [GenericType::class, $class]; + } + } else { + $class = A::get($this->typeClasses, $name); + $args = []; + if ($class !== null) func::fix_class_args($class, $args); + if ($class === null) { + # assumer que $name est une classe + if (is_subclass_of($name, IType::class)) $class = $name; + elseif ($required) $class = [GenericType::class, $name]; + else return null; + } elseif (!is_subclass_of($class, IType::class)) { + # si la classe n'implémente pas IType, prendre le type générique + $class = [GenericType::class, $class]; + } + } + func::fix_class_args($class, $args); + $type = func::cons($class, ...$args); + + return $this->types[$name] = $type; + } + + function addType($typeOrClass, ?string $name=null): void { + if ($typeOrClass instanceof IType) { + if ($name === null) $name = get_class($typeOrClass); + $this->types[$name] = $typeOrClass; + return; + } + if (is_array($typeOrClass)) { + # normaliser si nécessaire + $key = array_key_first($typeOrClass); + if ($key === 0) { + if ($name === null) { + foreach ($this->dynamicTypes as $dname => $dynamicType) { + if ($typeOrClass === $dynamicType) { + # type dynamique existant + # c'est le seul cas où le type n'écrase pas celui existant + return; + } + } + # nouveau type dynamique + $name = count($this->dynamicTypes) + 1; + $this->dynamicTypes[$name] = $typeOrClass; + } + } else { + # type dynamique nommé + if ($name === null) $name = $key; + $class = $typeOrClass[$key]; + unset($typeOrClass[$key]); + array_unshift($typeOrClass, $class); + } + } elseif ($name === null) { + $name = $typeOrClass; + } + $this->typeClasses[$name] = $typeOrClass; + } +} diff --git a/nur_src/data/types/RawStringType.php b/nur_src/data/types/RawStringType.php new file mode 100644 index 0000000..709e7a3 --- /dev/null +++ b/nur_src/data/types/RawStringType.php @@ -0,0 +1,142 @@ + ["array", null, "valeurs autorisées"], + "change_case" => ["?string", null, "faut-il transformer la chaine: upper, upper1, lower"], + ]; + + /** @var ?array */ + protected $ppAllowedValues; + + function pp_setAllowedValues(array $allowedValues): self { + # classer les chaines par ordre inverse de taille + $sizes = []; + foreach ($allowedValues as $value) { + $sizes[$value] = strlen($value); + } + arsort($sizes); + $this->ppAllowedValues = array_keys($sizes); + return $this; + } + + /** @var ?string */ + protected $ppChangeCase; + + function getClass(): string { + return "string"; + } + + function beforeCheckInstance(&$value): bool { + if (is_array($value)) $value = str::join3($value); + if ($this->ppChangeCase !== null && is_string($value)) { + switch ($this->ppChangeCase) { + case "upper": + case "uc": + $value = str::upper($value); + break; + case "upper1": + case "u1": + $value = str::upper1($value); + break; + case "upperw": + case "uw": + $value = str::upperw($value); + break; + case "lower": + case "lc": + $value = str::lower($value); + break; + case "lower1": + case "l1": + $value = str::lower1($value); + break; + } + } + return true; + } + + function isInstance($value, bool $strict=false): bool { + if ($value !== null && !is_string($value)) return false; + if ($value !== null && $this->ppAllowedValues !== null) { + if (!in_array($value, $this->ppAllowedValues)) return false; + } + return true; + } + + static function verifix_trim(string $value, bool $norm_lines, bool $trim_lines): string { + if ($trim_lines) { + $lines = []; + foreach (str::split_nl($value) as $line) { + $lines[] = trim($line); + } + return implode("\n", $lines); + } elseif ($norm_lines) { + return str::norm_nl(trim($value)); + } else { + return trim($value); + } + } + + /** trim normalise aussi les caractères de fin de ligne */ + function verifixTrim(string $value): string { + return self::verifix_trim($value, static::NORM_LINES, static::TRIM_LINES); + } + + function parse(string &$input) { + $allowedValues = $this->ppAllowedValues; + if ($allowedValues) { + $found = false; + foreach ($allowedValues as $allowedValue) { + if ($input === $allowedValue) { + $found = true; + $value = $allowedValue; + $input = ""; + break; + } elseif (str::_starts_with($allowedValue, $input)) { + $found = true; + $value = $allowedValue; + $input = substr($input, strlen($value) + 1); + break; + } + } + if (!$found) { + throw ValueException::unexpected_value($input, $allowedValues, static::KIND); + } + } else { + $value = $input; + $input = ""; + } + return $value; + } + + function verifixReplaceEmpty(&$value): void { + if (!$this->ppAllowEmpty && $this->ppAllowNull) $value = null; + } +} diff --git a/nur_src/data/types/RegexpType.php b/nur_src/data/types/RegexpType.php new file mode 100644 index 0000000..5efe0b3 --- /dev/null +++ b/nur_src/data/types/RegexpType.php @@ -0,0 +1,94 @@ + ["array", null, "patterns valides"] + ]; + + /** @var array */ + protected $patterns; + + function pp_setPattern(array $patterns): self { + $this->patterns = $patterns; + return $this; + } + + function __construct(?array $params=null) { + parent::__construct($params); + base::update_n($this->patterns, A::with(static::PATTERN)); + } + + function getClass(): string { + return "string"; + } + + function isInstance($value, bool $strict=true): bool { + if ($value === null) return true; + if (!is_string($value)) return false; + $patterns = null; + if ($strict) { + $patterns = static::PATTERN_NORMALIZED; + if ($patterns !== null) $patterns = [$patterns]; + } + if ($patterns === null) $patterns = $this->patterns; + foreach ($patterns as $pattern) { + if (preg_match($pattern, $value)) { + return true; + } + } + return false; + } + + protected function extractParsedValue(array $ms) { + return $ms[0]; + } + + function parse(string &$input) { + foreach ($this->patterns as $pattern) { + if (preg_match($pattern, $input, $ms)) { + $value = $this->extractParsedValue($ms); + $input = substr($input, strlen($ms[0])); + return $value; + } + } + return $this->ppAllowParseEmpty? null: false; + } + + /** + * normaliser la valeur qui a été analysée par {@link parse()} + * + * NB: si $this->allowParseEmpty==true, alors $value peut être null. il faut + * donc toujours tester ce cas, parce que la classe peut-être instanciée avec + * ce paramètre + * + * En fonction de l'implémentation, il est possible aussi que {@link parse()} + * aie analysé une chaine syntaxiquement correcte mais invalide. dans ce cas, + * cette fonction doit retourner false + */ + function verifixConvert(&$value): bool { + return true; + } +} diff --git a/nur_src/data/types/SDateType.php b/nur_src/data/types/SDateType.php new file mode 100644 index 0000000..9d886a0 --- /dev/null +++ b/nur_src/data/types/SDateType.php @@ -0,0 +1,43 @@ +with($value); + } + + const PATTERN = [ + '/^(?\d{1,2})[-\/.]+(?\d{1,2})(?:[-\/.]+(?\d{2,4}))?(?:\s+0{1,2}[hH:.,]0{1,2}(?:[:.,]0{1,2})?)?/', + '/^(?\d{4})-(?\d{2})-(?\d{2})(?:\s*00:00:00)?/', + '/^(?\d{2})(?\d{2})(?\d{2,4})?/', + ]; + const PATTERN_NORMALIZED = Date::PATTERN; + + /** @return array un tableau de la forme [$y, $m, $d] */ + protected function extractParsedValue(array $ms) { + $d = intval($ms["d"]); + $m = intval($ms["m"]); + $y = isset($ms["y"])? intval($ms["y"]): null; + if ($y === null) $y = intval(date("Y")); + else $y = Date::fix_any_year($y); + return [$y, $m, $d]; + } + + function verifixConvert(&$value): bool { + if ($value === null) return true; + $value = (new Date($value))->format(); + return true; + } + + function formatDate(?string $date, string $format="Y-m-d"): ?string { + if ($date === null) return null; + return date($format, Date::parse_date($date)); + } +} diff --git a/nur_src/data/types/SDatetimeType.php b/nur_src/data/types/SDatetimeType.php new file mode 100644 index 0000000..e2ae5af --- /dev/null +++ b/nur_src/data/types/SDatetimeType.php @@ -0,0 +1,50 @@ +with($value); + } + + const PATTERN = [ + '/^(?\d{1,2})[-\/]+(?\d{1,2})(?:[-\/]+(?\d{2,4}))?\s+(?[0-9]{1,2})[hH:.,](?[0-9]{1,2})(?:[:.,](?[0-9]{1,2}))?/', + '/^(?\d{4})-(?\d{2})-(?\d{2}) (?\d{2}):(?\d{2}):(?\d{2})/', + '/^(?\d{2})(?\d{2})(?\d{2,4})?\s+(?[0-9]{1,2})[hH:.,](?[0-9]{1,2})(?:[:.,](?[0-9]{1,2}))?/', + '/^(?\d{1,2})[-\/]+(?\d{1,2})(?:[-\/]+(?\d{2,4}))?/', + '/^(?\d{4})-(?\d{2})-(?\d{2})/', + '/^(?\d{2})(?\d{2})(?\d{2,4})?/', + ]; + const PATTERN_NORMALIZED = Datetime::PATTERN; + + /** @return array un tableau de la forme [$y, $m, $d, $H, $M, $S] */ + protected function extractParsedValue(array $ms) { + $d = intval($ms["d"]); + $m = intval($ms["m"]); + $y = isset($ms["y"]) && $ms["y"]? intval($ms["y"]): null; + if ($y === null) $y = intval(date("Y")); + else $y = Date::fix_any_year($y); + $H = isset($ms["H"])? intval($ms["H"]): 0; + $M = isset($ms["M"])? intval($ms["M"]): 0; + $S = isset($ms["S"])? intval($ms["S"]): 0; + return [$y, $m, $d, $H, $M, $S]; + } + + function verifixConvert(&$value): bool { + if ($value === null) return true; + $value = (new Datetime($value))->format(); + return true; + } + + function formatDatetime(?string $datetime, string $format="Y-m-d H:i:s"): ?string { + if ($datetime === null) return null; + return date($format, Datetime::parse_datetime($datetime)); + } +} diff --git a/nur_src/data/types/SHourType.php b/nur_src/data/types/SHourType.php new file mode 100644 index 0000000..cc41464 --- /dev/null +++ b/nur_src/data/types/SHourType.php @@ -0,0 +1,21 @@ +with($value); + } + + function verifixConvert(&$value): bool { + if ($value === null) return true; + $value = (new Hour($value))->format(); + return true; + } +} diff --git a/nur_src/data/types/STimeType.php b/nur_src/data/types/STimeType.php new file mode 100644 index 0000000..6c66dde --- /dev/null +++ b/nur_src/data/types/STimeType.php @@ -0,0 +1,32 @@ +with($value); + } + + const PATTERN = '/^(\d+)\s*(?:[hH:.,]\s*(?:(\d+)\s*(?:[:.,]\s*(\d+)\s*)?)?)?/'; + const PATTERN_NORMALIZED = Time::PATTERN; + + /** @return array un tableau de la forme [$H, $M, $S] */ + protected function extractParsedValue(array $ms) { + $h = intval($ms[1]); + $m = isset($ms[2])? intval($ms[2]): 0; + $s = isset($ms[3])? intval($ms[3]): 0; + return [$h, $m, $s]; + } + + function verifixConvert(&$value): bool { + if ($value === null) return true; + $value = (new Time($value))->format(); + return true; + } +} diff --git a/nur_src/data/types/STimeslotType.php b/nur_src/data/types/STimeslotType.php new file mode 100644 index 0000000..a58183d --- /dev/null +++ b/nur_src/data/types/STimeslotType.php @@ -0,0 +1,27 @@ + [ + "type" => [SHourType::class], + "key" => "ts_start", + "title" => "heure de début (inclue)", + "allow_parse_empty" => true, + ], + "end" => [ + "type" => [SHourType::class], + "key" => "ts_end", + "title" => "heure de fin (non inclue)", + "allow_parse_empty" => true, + ], + ]; + const SEPARATOR = "-"; + const SEPARATOR_PATTERN = '/^\s*(?:-\s*)?/'; + + function getClass(): string { + return "array"; + } +} diff --git a/nur_src/data/types/StringType.php b/nur_src/data/types/StringType.php new file mode 100644 index 0000000..642d6a5 --- /dev/null +++ b/nur_src/data/types/StringType.php @@ -0,0 +1,8 @@ + ['1242', null, 'Bahamas'], + '1246' => ['1246', null, 'Barbade'], + '1264' => ['1264', null, 'Anguilla'], + '1268' => ['1268', null, 'Antigua-et-Barbuda'], + '1284' => ['1284', null, 'Îles Vierges britanniques'], + '1340' => ['1340', null, 'Îles Vierges des États-Unis'], + '1345' => ['1345', null, 'îles Caïmans'], + '1441' => ['1441', null, 'Bermudes'], + '1473' => ['1473', null, 'Grenade'], + '1649' => ['1649', null, 'Îles Turques-et-Caïques'], + '1664' => ['1664', null, 'Montserrat'], + '1670' => ['1670', null, 'Îles Mariannes du Nord'], + '1671' => ['1671', null, 'Guam'], + '1684' => ['1684', null, 'Samoa américaines'], + '1758' => ['1758', null, 'Sainte-Lucie'], + '1767' => ['1767', null, 'Dominique'], + '1784' => ['1784', null, 'Saint-Vincent-et-les-Grenadines'], + '1868' => ['1868', null, 'Trinité-et-Tobago'], + '1869' => ['1869', null, 'Saint-Christophe-et-Niévès'], + '1876' => ['1876', null, 'Jamaïque'], + '1' => ['1', null, 'États-Unis'], + ['1', null, 'Canada'], + ['1', null, 'Porto Rico'], + ['1', null, 'République dominicaine'], + '20' => ['20', ['1'], 'Égypte'], + '211' => ['211', null, 'Soudan du Sud'], + '212' => ['212', '6' => ['6', '7'], 'Maroc'], + '213' => ['213', '5' => ['5', '6', '7', '9'], 'Algérie'], + '216' => ['216', '9' => ['9', '2', '5', '3', '4'], 'Tunisie'], + '218' => ['218', null, 'Libye'], + '220' => ['220', null, 'Gambie'], + '221' => ['221', null, 'Sénégal'], + '222' => ['222', null, 'Mauritanie'], + '223' => ['223', null, 'Mali'], + '224' => ['224', null, 'Guinée'], + '225' => ['225', null, 'Côte d\'Ivoire'], + '226' => ['226', null, 'Burkina Faso'], + '227' => ['227', null, 'Niger'], + '228' => ['228', null, 'Togo'], + '229' => ['229', null, 'Bénin'], + '230' => ['230', ['5'], 'Maurice'], + '231' => ['231', null, 'Libéria Liberia'], + '232' => ['232', null, 'Sierra Leone'], + '233' => ['233', null, 'Ghana'], + '234' => ['234', null, 'Nigeria'], + '235' => ['235', null, 'Tchad'], + '236' => ['236', null, 'République centrafricaine'], + '237' => ['237', ['2 ou 6'], 'Cameroun'], + '238' => ['238', null, 'Cap-Vert'], + '239' => ['239', null, 'Sao Tomé-et-Principe'], + '240' => ['240', null, 'Guinée équatoriale'], + '241' => ['241', null, 'Gabon'], + '242' => ['242', null, 'république du Congo'], + '243' => ['243', null, 'république démocratique du Congo'], + '244' => ['244', ['9'], 'Angola'], + '245' => ['245', null, 'Guinée-Bissau'], + '246' => ['246', null, 'Diego Garcia'], + '247' => ['247', null, 'Ascension'], + '248' => ['248', null, 'Seychelles'], + '249' => ['249', null, 'Soudan'], + '250' => ['250', null, 'Rwanda'], + '251' => ['251', null, 'Éthiopie'], + '252' => ['252', null, 'Somalie'], + '253' => ['253', null, 'Djibouti'], + '254' => ['254', null, 'Kenya'], + '255' => ['255', null, 'Tanzanie'], + '256' => ['256', null, 'Ouganda'], + '257' => ['257', null, 'Burundi'], + '258' => ['258', null, 'Mozambique'], + '260' => ['260', null, 'Zambie'], + '261' => ['261', '32' => ['32', '33', '34', '39'], 'Madagascar'], + '262' => ['262', '692' => ['692', '693'], 'La Réunion'], + ['262', ['639'], 'Mayotte'], + '263' => ['263', null, 'Zimbabwe'], + '264' => ['264', null, 'Namibie'], + '265' => ['265', null, 'Malawi'], + '266' => ['266', null, 'Lesotho'], + '267' => ['267', null, 'Botswana'], + '268' => ['268', null, 'Eswatini'], + '269' => ['269', null, 'Comores'], + '27' => ['27', null, 'Afrique du Sud'], + '290' => ['290', null, 'Sainte-Hélène, Ascension et Tristan da Cunha, île'], + '291' => ['291', null, 'Érythrée'], + '297' => ['297', null, 'Aruba'], + '298' => ['298', null, 'Îles Féroé'], + '299' => ['299', null, 'Groenland'], + '30' => ['30', ['6'], 'Grèce'], + '31' => ['31', ['6'], 'Pays-Bas'], + '32' => ['32', '46' => ['46', '47', '48', '49'], 'Belgique'], + '33' => ['33', '6' => ['6', '7'], 'France'], + '34' => ['34', '6' => ['6', '7'], 'Espagne'], + '350' => ['350', null, 'Gibraltar'], + '351' => ['351', ['9'], 'Portugal'], + '352' => ['352', '621' => ['621', '661', '671', '691'], 'Luxembourg'], + '353' => ['353', '82' => ['82', '83', '84', '85', '86', '87', '88', '89'], 'Irlande'], + '354' => ['354', '6' => ['6', '7', '8'], 'Islande'], + '355' => ['355', ['6'], 'Albanie'], + '356' => ['356', null, 'Malte'], + '357' => ['357', ['9'], 'Chypre'], + '358' => ['358', '4' => ['4', '50'], 'Finlande'], + '359' => ['359', '87' => ['87', '88', '89'], 'Bulgarie'], + '36' => ['36', '20' => ['20', '30', '31', '70'], 'Hongrie'], + '370' => ['370', ['6'], 'Lituanie'], + '371' => ['371', ['2'], 'Lettonie'], + '372' => ['372', '5' => ['5', '81', '82'], 'Estonie'], + '373' => ['373', null, 'Moldavie'], + '374' => ['374', null, 'Arménie'], + '375' => ['375', null, 'Biélorussie'], + '376' => ['376', '3' => ['3', '4', '6'], 'Andorre'], + '377' => ['377', ['4'], 'Monaco'], + '378' => ['378', null, 'Saint-Marin'], + '380' => ['380', null, 'Ukraine'], + '381' => ['381', '6' => ['6', '44', '45', '43', '49'], 'Serbie'], + '382' => ['382', null, 'Monténégro'], + '383' => ['383', null, 'Kosovo'], + '385' => ['385', ['9'], 'Croatie'], + '386' => ['386', '30' => ['30', '31', '40', '41', '43', '49', '51', '64', '68', '70', '71'], 'Slovénie'], + '387' => ['387', ['6'], 'Bosnie-Herzégovine'], + '389' => ['389', ['7'], 'Macédoine du Nord'], + '39' => ['39', ['3'], 'Italie'], + ['39', ['379'], 'Vatican'], + '40' => ['40', ['7'], 'Roumanie'], + '41' => ['41', '74' => ['74', '75', '76', '77', '78', '79'], 'Suisse'], + '420' => ['420', null, 'Tchéquie République tchèque'], + '421' => ['421', ['9'], 'Slovaquie'], + '423' => ['423', null, 'Liechtenstein'], + '43' => ['43', ['6'], 'Autriche'], + '44' => ['44', '74' => ['74', '75', '7624', '77', '78', '79'], 'Royaume-Uni'], + '45' => ['45', '2' => ['2', '30', '31', '40', '41', '42', '50', '51', '52', '53', '60', '61', '71', '81', '9'], 'Danemark'], + '46' => ['46', '70' => ['70', '71', '72', '73', '76'], 'Suède'], + '47' => ['47', '4' => ['4', '9'], 'Norvège'], + '48' => ['48', '50' => ['50', '51', '53', '60', '66', '69', '72', '73', '78', '79', '88'], 'Pologne'], + '49' => ['49', '15' => ['15', '16', '17'], 'Allemagne'], + '500' => ['500', null, 'Îles Malouines'], + '501' => ['501', null, 'Belize'], + '502' => ['502', null, 'Guatemala'], + '503' => ['503', null, 'Salvador'], + '504' => ['504', null, 'Honduras'], + '505' => ['505', null, 'Nicaragua'], + '506' => ['506', '7' => ['7', '8'], 'Costa Rica'], + '507' => ['507', null, 'Panama'], + '508' => ['508', null, 'Saint-Pierre-et-Miquelon'], + '509' => ['509', null, 'Haïti'], + '51' => ['51', ['9'], 'Pérou'], + '52' => ['52', ['1'], 'Mexique'], + '53' => ['53', null, 'Cuba'], + '54' => ['54', ['9'], 'Argentine'], + '55' => ['55', ['xx6', 'xx7', 'xx8', 'xx9'], 'Brésil'], + '56' => ['56', ['9'], 'Chili'], + '57' => ['57', ['3'], 'Colombie'], + '58' => ['58', ['4'], 'Venezuela'], + '590' => ['590', ['690'], 'Guadeloupe'], + '591' => ['591', '6' => ['6', '7'], 'Bolivie'], + '592' => ['592', null, 'Guyana'], + '593' => ['593', ['9'], 'Équateur'], + '594' => ['594', ['694'], 'Guyane'], + '595' => ['595', null, 'Paraguay'], + '596' => ['596', ['696'], 'Martinique'], + '597' => ['597', ['8'], 'Suriname'], + '598' => ['598', null, 'Uruguay'], + '599' => ['599', null, 'Curaçao et Pays-Bas caribéens'], + '60' => ['60', ['1'], 'Malaisie'], + '61' => ['61', '1' => ['1', '4'], 'Australie'], + '62' => ['62', null, 'Indonésie'], + '63' => ['63', null, 'Philippines'], + '64' => ['64', ['2'], 'Nouvelle-Zélande'], + '65' => ['65', '8' => ['8', '9'], 'Singapour'], + '66' => ['66', ['8'], 'Thaïlande'], + '670' => ['670', null, 'Timor oriental'], + '672' => ['672', null, 'Christmas, Cocos, Heard-et-MacDonald, Îles'], + '673' => ['673', null, 'Brunei'], + '674' => ['674', null, 'Nauru'], + '675' => ['675', null, 'Papouasie-Nouvelle-Guinée'], + '676' => ['676', null, 'Tonga'], + '677' => ['677', null, 'Îles Salomon'], + '678' => ['678', null, 'Vanuatu'], + '679' => ['679', null, 'Fidji'], + '680' => ['680', null, 'Palaos'], + '681' => ['681', null, 'Wallis-et-Futuna'], + '682' => ['682', null, 'Îles Cook'], + '683' => ['683', null, 'Niue'], + '685' => ['685', null, 'Samoa'], + '686' => ['686', null, 'Kiribati'], + '687' => ['687', null, 'Nouvelle-Calédonie'], + '688' => ['688', null, 'Tuvalu'], + '689' => ['689', null, 'Polynésie française'], + '690' => ['690', null, 'Tokelau'], + '691' => ['691', null, 'États fédérés de Micronésie'], + '692' => ['692', null, 'Îles Marshall'], + '7' => ['7', ['9'], 'Russie'], + ['7', '70' => ['70', '77'], 'Kazakhstan'], + '81' => ['81', '070' => ['070', '080', '090'], 'Japon'], + '82' => ['82', ['1'], 'Corée du Sud'], + '84' => ['84', null, 'République socialiste du Viêt Nam'], + '850' => ['850', null, 'Corée du Nord'], + '852' => ['852', '5' => ['5', '6', '9'], 'Hong Kong'], + '853' => ['853', null, 'Macao'], + '855' => ['855', null, 'Cambodge'], + '856' => ['856', null, 'Laos'], + '86' => ['86', ['1'], 'République populaire de Chine'], + '880' => ['880', null, 'Bangladesh'], + '881' => ['881', null, 'Système mobile mondial par satellite GMSS'], + '886' => ['886', ['9'], 'Taïwan'], + '90' => ['90', ['5'], 'Turquie'], + '91' => ['91', '7' => ['7', '8', '9'], 'Inde'], + '92' => ['92', null, 'Pakistan'], + '93' => ['93', ['7'], 'Afghanistan'], + '94' => ['94', null, 'Sri Lanka'], + '95' => ['95', null, 'Birmanie'], + '960' => ['960', null, 'Maldives'], + '961' => ['961', '3' => ['3', '70', '71'], 'Liban'], + '962' => ['962', null, 'Jordanie'], + '963' => ['963', null, 'Syrie'], + '964' => ['964', null, 'Irak'], + '965' => ['965', null, 'Koweït'], + '966' => ['966', ['5'], 'Arabie saoudite'], + '967' => ['967', null, 'Yémen'], + '968' => ['968', null, 'Oman'], + '970' => ['970', null, 'Palestine'], + '971' => ['971', ['5'], 'Émirats arabes unis'], + '972' => ['972', ['5'], 'Israël'], + '973' => ['973', null, 'Bahreïn'], + '974' => ['974', null, 'Qatar'], + '975' => ['975', null, 'Bhoutan'], + '976' => ['976', null, 'Mongolie'], + '977' => ['977', null, 'Népal'], + '98' => ['98', null, 'Iran'], + '992' => ['992', null, 'Tadjikistan'], + '993' => ['993', null, 'Turkménistan'], + '994' => ['994', null, 'Azerbaïdjan'], + '995' => ['995', null, 'Géorgie'], + '996' => ['996', null, 'Kirghizistan'], + '998' => ['998', null, 'Ouzbékistan'], + ]; + + private static function format_local(string $tel): string { + return substr($tel, 0, 4) + ." ".substr($tel, 4, 2) + ." ".substr($tel, 6, 2) + ." ".substr($tel, 8, 2); + } + + private static function format_foreign(string $tel): string { + $parts = []; + while(strlen($tel) > 4) { + $parts[] = substr($tel, -3); + $tel = substr($tel, 0, -3); + } + $parts[] = $tel; + $parts = array_reverse($parts); + return implode(" ", $parts); + } + + protected function extractParsedValue(array $ms) { + $tel = $ms[0]; + $tel = preg_replace('/[ .()-]/', "", $tel); + $tel = preg_replace('/\+/', "00", $tel); + $ntel = preg_replace('/[^0-9]/', "", $tel); + # tel est le numéro avec chiffres et virgule + # ntel est le numéro avec uniquement des chiffres + $size = strlen($ntel); + if ($size == 4) { + # numéro de poste, le retourner tel quel + return $tel; + } elseif ($size == 6) { + # numéro local sans préfixe + $tel = "00262262$tel"; + } elseif ($size == 10 && preg_match('/^0[1-9]/', $ntel)) { + # numéro local avec préfixe + $tel = "00262".substr($tel, 1); + } + # erreurs courantes + if ($size == 9 && substr($ntel, 0, 1) != "0") { + $tel = "00262$tel"; + } elseif ($size == 12 && strpos($ntel, "262262") === 0) { + $tel = "00$tel"; + } elseif ($size == 12 && strpos($ntel, "262692") === 0) { + $tel = "00$tel"; + } elseif ($size == 12 && strpos($ntel, "262693") === 0) { + $tel = "00$tel"; + } elseif ($size == 12 && strpos($ntel, "000262") === 0) { + $tel = "00262".substr($tel, 3); + } elseif ($size == 12 && strpos($ntel, "000692") === 0) { + $tel = "00262".substr($tel, 3); + } elseif ($size == 12 && strpos($ntel, "000693") === 0) { + $tel = "00262".substr($tel, 3); + } elseif ($size == 11 && strpos($ntel, "00262") === 0) { + $tel = "00262".substr($tel, 2); + } elseif ($size == 11 && strpos($ntel, "00692") === 0) { + $tel = "00262".substr($tel, 2); + } elseif ($size == 11 && strpos($ntel, "00693") === 0) { + $tel = "00262".substr($tel, 2); + } + # est-ce un numéro français? + if (substr($tel, 0, 5) == "00262") { + return self::format_local("0".substr($tel, 5)); + } elseif (substr($tel, 0, 4) == "0033") { + return self::format_local("0".substr($tel, 4)); + } + # chercher la partie internationale + $prefix = false; + foreach (self::COD_PAYS as $cod_pay) { + $cod_pay = $cod_pay[0]; + $plen = strlen($cod_pay); + if (substr($tel, 0, $plen + 2) == "00$cod_pay") { + $prefix = "+$cod_pay"; + $tel = substr($tel, $plen + 2); + break; + } elseif (substr($tel, 0, $plen) == $cod_pay) { + $prefix = "+$cod_pay"; + $tel = substr($tel, $plen); + break; + } + } + if ($prefix === false) return $tel; + return "$prefix ".self::format_foreign($tel); + } + + /** + * formatter le numéro pour qu'il soit toujours au format international. + * le numéro doit avoir déjà été formatté au préalable + */ + function ensureInternational(?string $tel): ?string { + if ($tel === null) return null; + else if ($tel[0] == "+") return $tel; + switch (substr($tel, 0, 4)) { + case "0262": + case "0692": + case "0693": + return "+262 ".substr($tel, 1); + default: + return "+33 ".substr($tel, 1); + } + } + + /** + * formatter le numéro pour qu'il soit toujours au format local si possible. + * c'est le "contraire" de {@link ensureInternational()} + */ + function ensureLocal(?string $tel): ?string { + if ($tel === null) return null; + elseif (substr($tel, 0, 5) == "+262 ") return "0".substr($tel, 5); + elseif (substr($tel, 0, 4) == "+33 ") return "0".substr($tel, 4); + else return $tel; + } +} diff --git a/nur_src/data/types/TextType.php b/nur_src/data/types/TextType.php new file mode 100644 index 0000000..61d324d --- /dev/null +++ b/nur_src/data/types/TextType.php @@ -0,0 +1,39 @@ +ppChangeCase !== null && is_string($value)) { + switch ($this->ppChangeCase) { + case "upper": + case "uc": + $value = txt::upper($value); + break; + case "upper1": + case "u1": + $value = txt::upper1($value); + break; + case "upperw": + case "uw": + $value = txt::upperw($value); + break; + case "lower": + case "lc": + $value = txt::lower($value); + break; + case "lower1": + case "l1": + $value = txt::lower1($value); + break; + } + } + return true; + } +} diff --git a/nur_src/data/types/TimeType.php b/nur_src/data/types/TimeType.php new file mode 100644 index 0000000..510802c --- /dev/null +++ b/nur_src/data/types/TimeType.php @@ -0,0 +1,128 @@ +ppAllowNull) $value = Time::null(); + } + + function verifixReplaceFalse(&$value): void { + if (!$this->ppAllowFalse) $value = Time::undef(); + } + + function verifixReplaceEmpty(&$value): void { + if (!$this->ppAllowFalse) $value = Time::undef(); + else $value = false; + } + + function verifixConvert(&$value): bool { + if ($value === null) return true; + $value = new Time($value[0] * 3600 + $value[1] * 60 + $value[2]); + return true; + } + + function dump($value) { + if ($value instanceof Time) { + if (!$this->ppAllowNull && $value->isNull()) $value = null; + if (!$this->ppAllowFalse && $value->isUndef()) $value = false; + } + return $value; + } + + function load($value) { + if ($value === null && !$this->ppAllowNull) $value = Time::null(); + elseif ($value === false && !$this->ppAllowFalse) $value = Time::undef(); + return $value; + } + + ############################################################################# + # Méthodes utilitaires + + function isNone($time): bool { + if ($time instanceof Time) return $time->isNull(); + else return $time === null; + } + + function isUndef($time, $key=null): bool { + if ($key !== null) { + if (!is_array($time)) return $key !== 0; + if (!array_key_exists($key, $time)) return true; + $time = $time[$key]; + } + if ($time instanceof Time) return $time->isUndef(); + else return $time === false; + } + + function add($time, $add): ?Time { + $time = $this->with($time); + $add = $this->with($add); + if ($add === null) return $time; + elseif ($time === null) return $add; + return $time->add($add); + } + + function sub($time, $sub): ?Time { + $time = $this->with($time); + $sub = $this->with($sub); + if ($sub === null) return $time; + elseif ($time === null) $time = $sub->newu(0); + return $time->sub($sub); + } + + function cmp($time, $other): int { + $time = $this->with($time); + $other = $this->with($other); + if ($time === null && $other === null) return 0; + elseif ($time === null) return -1; + elseif ($other === null) return 1; + return $time->cmp($other); + } + + function before($time, $other): bool { + $time = $this->with($time); + $other = $this->with($other); + if ($time === null && $other === null) return true; + elseif ($time === null) return true; + elseif ($other === null) return false; + return $time->before($other); + } + + function after($time, $other): bool { + $time = $this->with($time); + $other = $this->with($other); + if ($time === null && $other === null) return true; + elseif ($time === null) return false; + elseif ($other === null) return true; + return $time->after($other); + } +} diff --git a/nur_src/data/types/Tmd.php b/nur_src/data/types/Tmd.php new file mode 100644 index 0000000..556dde3 --- /dev/null +++ b/nur_src/data/types/Tmd.php @@ -0,0 +1,13 @@ +getParametrableParamsParametrables(); + parametrable_utils::set_params($parametrables, $this, $params, self::PARAMETRABLE_PARAMS_SCHEMA); + } + + function initParametrableParams(?array $params, bool $setParametrableParams=true): void { + parent::initParametrableParams(null); + $parametrables = $this->getParametrableParamsParametrables(); + parametrable_utils::set_defaults($parametrables, $this, self::PARAMETRABLE_PARAMS_SCHEMA); + if ($setParametrableParams) $this->setParametrableParams($params); + } +} diff --git a/nur_src/data/types/_Tparametrable0.php b/nur_src/data/types/_Tparametrable0.php new file mode 100644 index 0000000..a1af898 --- /dev/null +++ b/nur_src/data/types/_Tparametrable0.php @@ -0,0 +1,24 @@ +getParametrableParamsParametrables(); + parametrable_utils::set_params($parametrables, $this, $params, self::PARAMETRABLE_PARAMS_SCHEMA); + } + + function initParametrableParams(?array $params, bool $setParametrableParams=true): void { + $parametrables = $this->getParametrableParamsParametrables(); + parametrable_utils::set_defaults($parametrables, $this, self::PARAMETRABLE_PARAMS_SCHEMA); + if ($setParametrableParams) $this->setParametrableParams($params); + } +} diff --git a/nur_src/data/types/_Tparametrable1.php b/nur_src/data/types/_Tparametrable1.php new file mode 100644 index 0000000..4389553 --- /dev/null +++ b/nur_src/data/types/_Tparametrable1.php @@ -0,0 +1,20 @@ +ensureSchema($item); + } +}