ajout nur/data

This commit is contained in:
Jephté Clain 2023-12-18 05:52:35 +04:00
parent 09cd738458
commit 8dc8d12ab3
46 changed files with 4415 additions and 0 deletions

15
nur_src/data/Context.php Normal file
View File

@ -0,0 +1,15 @@
<?php
namespace nur\data;
use nur\data\expr\IContext;
class Context implements IContext {
/**
* ajouter une nouvelle source de données
*
* $data contient une description de la source de données, conforme au schéma
* {@link IContext::SOURCE_SCHEMA}
*/
function addSource(array $data, $source) {
}
}

View File

@ -0,0 +1,57 @@
<?php
namespace nur\data\expr;
use nur\A;
use nur\b\coll\BaseArray;
use nur\b\ValueException;
use nur\func;
use nur\md;
/**
* Class GenericExpr: une expression générique, utilisée par défaut
*/
class GenericExpr extends BaseArray implements IExpr {
static final function with($expr, ?string $key=null): IExpr {
if ($expr instanceof IExpr) return $expr;
return new GenericExpr($expr, $key);
}
function __construct($data=null, ?string $key=null) {
md::ensure_schema($data, static::SCHEMA, null, false);
A::replace_z($data, "name", $key);
A::replace_z_indirect($data, "name", "value");
A::replace_z_indirect($data, "title", "name");
parent::__construct($data);
}
function getValue(): ?string {
return $this->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);
}
}

View File

@ -0,0 +1,60 @@
<?php
namespace nur\data\expr;
/**
* Interface IContext: un contexte à partir duquel sont accédées certaines
* données
*/
interface IContext {
/** schéma d'une représentation d'un contexte sous forme de tableau */
const SCHEMA = [
"sources" => [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);
}

View File

@ -0,0 +1,28 @@
<?php
namespace nur\data\expr;
/**
* Interface IExpr: une expression à évaluer dans un contexte
*/
interface IExpr {
/** schéma d'une représentation d'une expression sous forme de tableau */
const SCHEMA = [
"value" => [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);
}

View File

@ -0,0 +1,75 @@
<?php
namespace nur\data\expr;
use nur\A;
use nur\b\coll\BaseArray;
use nur\config;
use nur\func;
use nur\session;
class SimpleContext extends BaseArray implements IContext {
/**
* @return array une liste de sources {$name => $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); }
}

View File

@ -0,0 +1,16 @@
<?php
namespace nur\data\flow;
/**
* Interface IStateMachine: interface pour une machine à état
*/
interface IStateMachine {
/** obtenir l'état courant */
function get_current_state(): array;
/** obtenir la liste des actions possible à partir de l'état courant */
function get_next_actions(): array;
/** faire l'action spécifiée */
function perform_action(string $action, ?array $data=null): void;
}

View File

@ -0,0 +1,14 @@
<?php
namespace nur\data\template;
/**
* Interface ITemplate: un modèle de document
*/
interface ITemplate {
/**
* instancier le modèle et "retourner" les données.
*
* la façon dont les données sont "retournées" dépend de l'implémentation
*/
function apply();
}

View File

@ -0,0 +1,204 @@
<?php
namespace nur\data\template;
use nur\A;
use nur\b\io\IOException;
use nur\data\expr\SimpleContext;
use nur\func;
use nur\session;
use ZipArchive;
/**
* Class InterpTemplate: réimplémentation de l'ancienne classe interp
*/
class InterpTemplate extends SimpleContext implements ITemplate {
use TTemplate;
function __construct(?string $text=null, $data=null, $quote=true, bool $allowPattern=true) {
if ($data === null) $data = [];
parent::__construct($data);
$this->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();
}
}

View File

@ -0,0 +1,62 @@
<?php
namespace nur\data\template;
use nur\b\io\FileReader;
use nur\b\io\IReader;
use nur\b\io\IWriter;
use nur\b\ValueException;
use nur\data\expr\SimpleContext;
class StreamTemplate extends SimpleContext implements ITemplate {
use TTemplate;
/**
* retourner un nom de fichier à utiliser par défaut en entrée si l'argument
* $input n'est pas spécifié.
*/
protected function INPUT(): ?string {
return static::INPUT;
} const INPUT = null;
/** @var IReader */
private $input;
/** @var IWriter */
private $output;
/** @var bool */
private $autoClose;
function __construct(IWriter $output, ?IReader $input=null, bool $autoClose=true) {
parent::__construct();
$this->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;
}
}

View File

@ -0,0 +1,34 @@
<?php
namespace nur\data\template;
use nur\data\expr\SimpleContext;
class StringTemplate extends SimpleContext implements ITemplate {
use TTemplate;
/** @return string le texte du modèle */
protected function TEXT(): string {
$text = $this->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);
}
}

View File

@ -0,0 +1,29 @@
<?php
namespace nur\data\template;
use nur\base;
use nur\data\expr\GenericExpr;
use nur\data\expr\IContext;
trait TTemplate {
/** @var IContext */
protected $context;
function getContext(): IContext {
return $this->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;
}
}

View File

@ -0,0 +1,119 @@
<?php
namespace nur\data\types;
use nur\md;
use nur\types;
/**
* Class CompositeType: squelette d'implémentation d'un type complexe
*
* un type complexe est la composition de plusieurs types simples. il n'est pas
* prévu pour l'analyse ou le formatage, uniquement l'utilisation de la méthode
* {@link verifix()}
*/
abstract class AbstractComplexType extends AbstractSimpleType {
use _Tparametrable;
const ALLOW_NULL = true;
const COMPONENT_SCHEMA = [
"name" => ["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;
}
}

View File

@ -0,0 +1,255 @@
<?php
namespace nur\data\types;
use Exception;
use nur\A;
use nur\b\ValueException;
use nur\md;
use nur\types;
/**
* Class CompositeType: squelette d'implémentation d'un type composite
*
* un type composite permet de gérer une valeur qui est stockée dans plusieurs
* champs de types différents
*
* Par convention, les classes dérivées sont nommées C{name}Type
*/
abstract class AbstractCompositeType extends AbstractType {
use _Tparametrable;
const ALLOW_NULL = true;
const COMPONENT_SCHEMA = [
"name" => ["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;
}
}
}

View File

@ -0,0 +1,128 @@
<?php
namespace nur\data\types;
use Exception;
use nur\A;
/**
* Class SimpleType: squelette d'implémentation pour un type simple
*/
abstract class AbstractSimpleType extends AbstractType {
use _Tparametrable;
/** @var bool suffit-il de vérifier la classe pour être sûr du type? */
const CHECK_CLASS = true;
/**
* @var bool est-il autorisé que l'analyse ne consomme aucune donnée et
* retourne null?
*/
const ALLOW_PARSE_EMPTY = false;
const PARAMETRABLE_PARAMS_SCHEMA = [
"allow_parse_empty" => ["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);
}
}
}

View File

@ -0,0 +1,343 @@
<?php
namespace nur\data\types;
use Exception;
use nur\A;
use nur\b\IllegalAccessException;
use nur\b\params\IParametrable;
use nur\b\ValueException;
use nur\prop;
use nur\str;
/**
* Class AbstractType: implémentation partagée
*/
abstract class AbstractType implements IType, IParametrable {
use _Tparametrable1;
/**
* @var bool si une valeur chaine est fournie, doit-elle être trimée avant
* analyse?
*/
const TRIM = true;
/** @var bool|null la chaine vide est-elle autorisée? */
const ALLOW_EMPTY = true;
/** @var bool la valeur false est-elle autorisée? */
const ALLOW_FALSE = true;
/** @var bool la valeur null est-elle autorisée? */
const ALLOW_NULL = true;
/** @var bool si une chaine est fournie, doit-elle correspondre entièrement? */
const PARSE_ALL = true;
/** @var array messages standards */
const MESSAGES = [
"empty" => "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;
}
}

View File

@ -0,0 +1,176 @@
<?php
namespace nur\data\types;
use nur\prop;
class BoolType extends AbstractSimpleType {
use _Tparametrable;
const TITLE = "valeur booléenne";
/**
* @var bool
*
* par défaut, n'autoriser que deux état: true et false. remplacer par
* true dans une classe dérivée pour avoir les 3 états true, false et null
*/
const ALLOW_NULL = false;
const OUINON_FORMAT = ["Oui", "Non", false];
const OUINONNULL_FORMAT = ["Oui", "Non", ""];
const ON_FORMAT = ["O", "N", false];
const ONN_FORMAT = ["O", "N", ""];
const XN_FORMAT = ["X", "", false];
const OZ_FORMAT = ["1", "", false];
const FORMATS = [
"ouinon" => 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);
}
}

View File

@ -0,0 +1,23 @@
<?php
namespace nur\data\types;
class CTimeslotType extends AbstractCompositeType {
const TITLE = "plage horaire";
const COMPONENTS = [
"start" => [
"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*)?/';
}

View File

@ -0,0 +1,42 @@
<?php
namespace nur\data\types;
use nur\b\ui\IContent;
use nur\b\ui\IPrintable;
class ContentType extends AbstractSimpleType {
const TITLE = "contenu à afficher";
const TRIM = false;
/** @var string description de la valeur */
const KIND = "content";
function getClass(): string {
# on est malheureusement obligé de mettre mixed, mais en réalité, il peut
# s'agir des types suivants: scalar (string, int, etc.), iterable,
# IPrintable, IContent
return "mixed";
}
function isInstance($value, bool $strict=false): bool {
if (is_iterable($value)) {
$values = $value;
foreach ($values as $value) {
if (!$this->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 {
}
}

View File

@ -0,0 +1,51 @@
<?php
namespace nur\data\types;
/**
* Class GenericType: type de données générique, qui n'est ni formatable ni
* analysable
*/
class FileType extends AbstractType {
const TITLE = "information de fichier uploadé récupéré depuis \$_FILES";
function getClass(): string {
return "array";
}
function isInstance($value, bool $strict=false): bool {
if ($value === null) return $this->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);
}
}
}

View File

@ -0,0 +1,48 @@
<?php
namespace nur\data\types;
class FloatType extends AbstractSimpleType {
const TITLE = "nombre à virgule";
private static $instance;
static final function to_zfloat($value) {
$type = self::$instance;
if ($type === null) self::$instance = $type = new self();
return $type->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;
}
}

View File

@ -0,0 +1,91 @@
<?php
namespace nur\data\types;
use nur\A;
use nur\b\ValueException;
use nur\func;
use Traversable;
/**
* Class GenericType: type de données générique, qui n'est ni formatable ni
* analysable
*/
class GenericType extends AbstractSimpleType {
const ARRAY = "array";
const ARRAY_ARRAY = "array[]";
const ITERABLE = "iterable";
const RESOURCE = "resource";
function __construct($class, ?array $params=null) {
parent::__construct($params);
func::fix_class_args($class, $args);
$this->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);
}
}

View File

@ -0,0 +1,11 @@
<?php
namespace nur\data\types;
/**
* Interface IIncarnation: un gestionnaire de types
*/
interface IIncarnation {
function hasType($name);
function getType($name, bool $required=true): ?IType;
}

View File

@ -0,0 +1,261 @@
<?php
namespace nur\data\types;
use Exception;
use nur\b\IllegalAccessException;
use nur\b\ValueException;
/**
* Interface IType: un format d'affichage et de saisie pour un type de données
*/
interface IType {
/**
* @return string la classe des objets gérés par ce format: le type attendu
* par {@link format()} et le type retourné par {@link verifix()}
*
* Les valeurs "mixed", "bool", "float", "int", "string" et "array" peuvent
* aussi être retournées, bien qu'elles ne soient pas à proprement parler des
* classes.
*
* La valeur "mixed" signifie qu'il peut s'agir de n'importe quelle classe
* et/ou que la valeur ne peut pas être déterminée à l'avance.
*/
function getClass(): string;
/**
* comme {@link getClass()} mais peut être utilisé directement comme type dans
* une déclaration PHP. notamment, si le type est nullable et que
* $allowNullable==true, il y a "?" devant le nom.
*
* Par exemple:
* - pour un type chaine nullable, {@link getClass()} retournerait "string"
* alors que cette méthode retournerait "?string".
* - pour un type mixed, {@link getClass()} retournerait "mixed" alors que
* cette méthode retournerait null
*/
function getPhpType(bool $allowNullable=true): ?string;
/**
* vérifier si $value est de ce type
*
* si {@link getClass()} retourne "string" ou "array", on ne se contente pas
* de vérifier que la donnée est au bon type: on vérifie aussi si le format de
* la donnée est correct
*/
function isInstance($value): bool;
/**
* obtenir la valeur correspondant à "non-existant"
*
* si la valeur retournée est null, ce n'est peut-être pas la valeur undef. la
* seule façon de le savoir est d'appeler {@link isUndef()}
*/
function getUndefValue();
/**
* vérifier si la valeur existe
*
* - si $key===null, alors indiquer si $value n'a pas la valeur "non-existant"
* si cela a du sens pour ce type
* - si $key!==null, alors indiquer si $value[$key] existe ou s'il n'a pas la
* valeur "non-existant"
*
* d'une manière générale, "false" correspond à "non-existant", mais il peut
* y avoir des exceptions d'où l'existence de cette méthode
*/
function isUndef($value, $key=null): bool;
/**
* indiquer si c'est le type d'une valeur qui ne peut prendre que 2 états: une
* "vraie" et une "fausse"
*/
function is2States(): bool;
/**
* Si {@link is2States()} est vrai, retourner les deux valeurs [faux, vrai]
*/
function get2States(): array;
/**
* indiquer si c'est le type d'une valeur qui ne peut prendre que 3 états: une
* "vraie", une "fausse", et une "indéterminée"
*/
function is3States(): bool;
/**
* Si {@link is3States()} est vrai, retourner les 3 valeurs [faux, vrai, undef]
*/
function get3States(): array;
/**
* cet objet est-il capable de formatter des valeurs pour affichage? sinon,
* la méthode format() lance l'exception {@link IllegalAccessException}
*
* @return bool true si les valeurs de ce type peuvent être formattées pour
* affichage
*/
function canFormat(): bool;
/**
* formatter $value pour affichage.
*
* $value peut valoir null ou false si ces valeurs sont autorisées. sinon,
* elle doit être du bon type (c'est à dire que {@link isInstance()} doit
* retourner true). sinon le comportement n'est pas défini
*/
function format($value): string;
/**
* cet objet est-il capable d'analyser des valeurs chaines? sinon, la méthode
* {@link parse()} ne fait pas partie de l'API publique de cet objet. elle ne
* sera alors généralement pas implémentée
*
* @return bool true si cet objet est capable d'analyser une valeur chaine
* fournie par l'utilisateur
*/
function canParse(): bool;
/**
* analyser $input:
* - si elle commence par une valeur de ce type, retourner la chaine pour
* traitement par {@link verifix()}. $input est mis à jour pour pointer sur la
* suite de la chaine, après la valeur analysée
* - sinon, retourner false. dans ce cas, $input n'est pas modifié
*
* NB: une implémentation peut décider de retourner autre chose qu'une chaine
* mais dans ce cas, il faut documenter le format de la valeur de retour, et
* probablement que {@link canParse()} doit retourner false
*
* @param string $input la chaine à analyser
* @return mixed|bool la partie de la chaine à traiter avec {@link verifix()}
* @throws IllegalAccessException si {@link canParse()} retourne false
*/
function parse(string &$input);
/** avant d'analyser une chaine, faut-il la trimer? */
function isTrim(): bool;
/**
* la chaine vide est-elle autorisée?
*
* si une chaine est fournie et que la chaine vide n'est pas autorisée, la
* méthode {@link verifixReplaceEmpty()} est appelée pour la remplacer par une
* autre valeur. par défaut, elle est remplacée par null.
*/
function isAllowEmpty(): bool;
/**
* fonction de support: corriger la valeur "" le cas échéant
*
* par défaut, remplacer "" par null si null est autorisé
*/
function verifixReplaceEmpty(&$value): void;
/**
* la valeur false est-elle autorisée?
*
* si false n'est pas autorisée, la fonction {@link verifixReplaceFalse()}
* est appelée pour la remplacer par une autre valeur. par défaut, elle est
* remplacée par null.
*/
function isAllowFalse(): bool;
/**
* 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;
/**
* la valeur null est-elle autorisée?
*
* si null n'est pas autorisée, la fonction {@link verifixReplaceNull()}
* est appelée pour la remplacer par une autre valeur. par défaut, aucun
* remplacement n'est fait, ce qui provoque donc une erreur
*/
function isAllowNull(): bool;
/** fonction de support: corriger la valeur null le cas échéant */
function verifixReplaceNull(&$value): void;
const VERIFIX_RESULT_SCHEMA = [
"valid" => [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;
}

View File

@ -0,0 +1,49 @@
<?php
namespace nur\data\types;
class IntType extends AbstractSimpleType {
const TITLE = "nombre entier";
private static $instance;
static final function to_zint($value) {
$type = self::$instance;
if ($type === null) self::$instance = $type = new self();
return $type->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;
}
}

View File

@ -0,0 +1,35 @@
<?php
namespace nur\data\types;
class KeyType extends AbstractSimpleType {
const TITLE = "clé d'un tableau";
const TRIM = false;
/** @var string description de la valeur */
const KIND = "key";
function getClass(): string {
# on est malheureusement obligé de mettre mixed, mais en réalité, il ne peut
# y avoir que int et string
return "mixed";
}
function isInstance($value, bool $strict=false): bool {
return $value === null || is_string($value) || is_int($value);
}
function parse(string &$input) {
return $input;
}
function verifixConvert(&$value): bool {
if (is_string($value) && preg_match('/^-?[0-9]+$/', $value)) {
$value = intval($value);
}
return true;
}
function verifixReplaceEmpty(&$value): void {
}
}

View File

@ -0,0 +1,32 @@
<?php
namespace nur\data\types;
class MailType extends RegexpType {
const PATTERN = '/^.+/';
const PATTERN_NORMALIZED = '/^(\S+)$/';
protected function extractParsedValue(array $ms) {
$mail = $ms[0];
$mail = filter_var($mail, FILTER_SANITIZE_EMAIL);
$mail = filter_var($mail, FILTER_VALIDATE_EMAIL, FILTER_FLAG_EMAIL_UNICODE);
if ($mail === false) return false;
if (preg_match('/^([A-Za-z0-9.-]+)(\+[^@]*)?(@[A-Za-z0-9.-]+)$/', $mail, $ms)) {
return [$ms[1], $ms[2], $ms[3]];
} else {
return false;
#return $mail;
}
}
function verifixConvert(&$value): bool {
if ($value === null) return true;
elseif ($value === false) return false;
if (is_array($value)) {
[$local, $alias, $domain] = $value;
if (strpos($local, ".") !== false) $local = strtolower($local);
$domain = strtolower($domain);
$value = "$local$alias$domain";
}
return true;
}
}

View File

@ -0,0 +1,842 @@
<?php
namespace nur\data\types;
use nur\A;
use nur\b\params\IParametrable;
use nur\b\ValueException;
use nur\base;
use nur\md;
use nur\types;
/**
* Class Metadata: gestion de données conformes à un schéma
*
* un schéma est un tableau d'éléments {$key => $sfield} $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}, $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-:
* ~~~
* 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 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);
}
}

View File

@ -0,0 +1,30 @@
<?php
namespace nur\data\types;
/**
* Class MixedType: n'importe quel type: on ne fait aucun effort pour déterminer
* le type effectif ni pour chercher à convertir ou normaliser les valeurs
*/
class MixedType extends AbstractSimpleType {
const TITLE = "type indifférent";
const ALLOW_NULL = true;
function getClass(): string {
return "mixed";
}
function isInstance($value, bool $strict=false): bool {
return true;
}
function parse(string &$input) {
$value = $input;
$input = "";
return $value;
}
protected function _verifix(&$value, array &$result=null): void {
self::result_valid($result, $value, $value);
}
}

View File

@ -0,0 +1,213 @@
<?php
namespace nur\data\types;
use nur\A;
use nur\func;
use nur\ref\ref_type;
class PhpIncarnation implements IIncarnation {
function __construct() {
# non nullable
$this->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;
}
}

View File

@ -0,0 +1,142 @@
<?php
namespace nur\data\types;
use nur\b\ValueException;
use nur\str;
class RawStringType extends AbstractSimpleType {
use _Tparametrable;
const TITLE = "chaine de caractères verbatim";
const TRIM = false;
const ALLOW_EMPTY = null;
/**
* @var bool si on doit trimmer la valeur, faut-il normaliser les caractères
* de fin de ligne?
*/
const NORM_LINES = true;
/**
* @var bool si on doit trimmer la valeur, faut-il trimmer chaque ligne de
* façon indépendante?
*/
const TRIM_LINES = false;
/** @var string description de la valeur */
const KIND = "value";
const PARAMETRABLE_PARAMS_SCHEMA = [
"allowed_values" => ["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;
}
}

View File

@ -0,0 +1,94 @@
<?php
namespace nur\data\types;
use nur\A;
use nur\base;
/**
* Class PatternType: une chaine qui correspond à une regexp
*/
class RegexpType extends AbstractSimpleType {
use _Tparametrable;
const ALLOW_NULL = true;
const CHECK_CLASS = false;
/** @var array|string */
const PATTERN = ".*";
/**
* @var string un pattern qui matche *entièrement* une chaine normalisée de ce
* type. de plus, ce pattern peut définir des groupes pour isoler certaines
* parties de la valeur.
* par défaut, utiliser {@link PATTERN} si cette constante est nulle.
*/
const PATTERN_NORMALIZED = null;
const PARAMETRABLE_PARAMS_SCHEMA = [
"pattern" => ["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;
}
}

View File

@ -0,0 +1,43 @@
<?php
namespace nur\data\types;
use nur\b\date\Date;
class SDateType extends RegexpType {
const TITLE = "date";
private static $instance;
static final function to_date($value) {
$type = self::$instance;
if ($type === null) self::$instance = $type = new self();
return $type->with($value);
}
const PATTERN = [
'/^(?<d>\d{1,2})[-\/.]+(?<m>\d{1,2})(?:[-\/.]+(?<y>\d{2,4}))?(?:\s+0{1,2}[hH:.,]0{1,2}(?:[:.,]0{1,2})?)?/',
'/^(?<y>\d{4})-(?<m>\d{2})-(?<d>\d{2})(?:\s*00:00:00)?/',
'/^(?<d>\d{2})(?<m>\d{2})(?<y>\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));
}
}

View File

@ -0,0 +1,50 @@
<?php
namespace nur\data\types;
use nur\b\date\Date;
use nur\b\date\Datetime;
class SDatetimeType extends RegexpType {
const TITLE = "date et heure";
private static $instance;
static final function to_datetime($value) {
$type = self::$instance;
if ($type === null) self::$instance = $type = new self();
return $type->with($value);
}
const PATTERN = [
'/^(?<d>\d{1,2})[-\/]+(?<m>\d{1,2})(?:[-\/]+(?<y>\d{2,4}))?\s+(?<H>[0-9]{1,2})[hH:.,](?<M>[0-9]{1,2})(?:[:.,](?<S>[0-9]{1,2}))?/',
'/^(?<y>\d{4})-(?<m>\d{2})-(?<d>\d{2}) (?<H>\d{2}):(?<M>\d{2}):(?<S>\d{2})/',
'/^(?<d>\d{2})(?<m>\d{2})(?<y>\d{2,4})?\s+(?<H>[0-9]{1,2})[hH:.,](?<M>[0-9]{1,2})(?:[:.,](?<S>[0-9]{1,2}))?/',
'/^(?<d>\d{1,2})[-\/]+(?<m>\d{1,2})(?:[-\/]+(?<y>\d{2,4}))?/',
'/^(?<y>\d{4})-(?<m>\d{2})-(?<d>\d{2})/',
'/^(?<d>\d{2})(?<m>\d{2})(?<y>\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));
}
}

View File

@ -0,0 +1,21 @@
<?php
namespace nur\data\types;
use nur\b\date\Hour;
class SHourType extends STimeType {
const TITLE = "heure du jour";
private static $instance;
static final function to_hour($value) {
$type = self::$instance;
if ($type === null) self::$instance = $type = new self();
return $type->with($value);
}
function verifixConvert(&$value): bool {
if ($value === null) return true;
$value = (new Hour($value))->format();
return true;
}
}

View File

@ -0,0 +1,32 @@
<?php
namespace nur\data\types;
use nur\b\date\Time;
class STimeType extends RegexpType {
const TITLE = "heure du jour";
private static $instance;
static final function to_time($value) {
$type = self::$instance;
if ($type === null) self::$instance = $type = new self();
return $type->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;
}
}

View File

@ -0,0 +1,27 @@
<?php
namespace nur\data\types;
class STimeslotType extends AbstractComplexType {
const TITLE = "plage horaire";
const COMPONENTS = [
"start" => [
"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";
}
}

View File

@ -0,0 +1,8 @@
<?php
namespace nur\data\types;
class StringType extends RawStringType {
const TITLE = "chaine de caractères trimmée et normalisée";
const TRIM = true;
}

View File

@ -0,0 +1,354 @@
<?php
namespace nur\data\types;
class TelephoneType extends RegexpType {
const PATTERN = '/^[0-9+, .()-]+/';
const PATTERN_NORMALIZED = '/^(\+?[0-9 ,]+)$/';
const COD_PAYS = [
'1242' => ['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;
}
}

View File

@ -0,0 +1,39 @@
<?php
namespace nur\data\types;
use nur\str;
use nur\txt;
/**
* Class TextType: un texte encodé en utf-8
*/
class TextType extends StringType {
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 = 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;
}
}

View File

@ -0,0 +1,128 @@
<?php
namespace nur\data\types;
use nur\b\date\Time;
class TimeType extends AbstractSimpleType {
const TITLE = "heure du jour";
function getClass(): string {
return Time::class;
}
function isInstance($value, bool $strict=false): bool {
return $value === null || $value instanceof Time;
}
const TIME_PATTERN = '/^(\d+)\s*(?:[hH:.]\s*(?:(\d+)\s*(?:[:.]\s*(\d+)\s*)?)?)?/';
/** @return array|false un tableau de la forme [$h, $m, $s] */
function parse(string &$input) {
if (preg_match(self::TIME_PATTERN, $input, $ms)) {
$h = intval($ms[1]);
$m = isset($ms[2])? intval($ms[2]): 0;
$s = isset($ms[3])? intval($ms[3]): 0;
$input = substr($input, strlen($ms[0]));
return [$h, $m, $s];
} else {
return false;
}
}
const ALLOW_NULL = true;
const ALLOW_FALSE = false;
function verifixReplaceNull(&$value): void {
if (!$this->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);
}
}

View File

@ -0,0 +1,13 @@
<?php
namespace nur\data\types;
trait Tmd {
/** @var Metadata */
private static $md;
protected function md(): ?Metadata {
$schema = self::SCHEMA;
if ($schema === null) return null;
else return md_utils::ensure_md($md, $schema);
}
}

View File

@ -0,0 +1,34 @@
<?php
namespace nur\data\types;
class TriboolType extends BoolType {
const TITLE = "valeur triléenne";
const ALLOW_NULL = true;
const FORMAT = self::OUINONNULL_FORMAT;
static final function to_tribool($value): ?bool {
return $value !== null? self::to_bool($value): null;
}
function isInstance($value, bool $strict=false): bool {
return $value === null || is_bool($value);
}
function isUndef($value, $key=null): bool {
if ($key !== null) {
if (!is_array($value)) return $key === 0;
return array_key_exists($key, $value);
}
return true;
}
function is3States(): bool {
return true;
}
function get3States(): array {
return [false, true, null];
}
}

View File

@ -0,0 +1,25 @@
<?php
namespace nur\data\types;
use nur\b\params\parametrable_utils;
/**
* variante de {@link Tparametrable} qui n'utilise pas {@link Metadata} pour
* éviter une boucle infinie.
*/
trait _Tparametrable {
use _Tparametrable0;
function setParametrableParams(?array $params): void {
parent::setParametrableParams($params);
$parametrables = $this->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);
}
}

View File

@ -0,0 +1,24 @@
<?php
namespace nur\data\types;
use nur\b\params\parametrable_utils;
/**
* Trait _Tparametrable0: implémentation partagée entre {@link _Tparametrable1}
* et {@link _Tparametrable} qui doit toujours être écrasée dans les classes
* dérivées (i.e si les méthodes définies ici sont surchargées, elle ne seront
* pas disponibles dans les classes dérivées parce qu'écrasées par la directive
* `use _Tparametrable`)
*/
trait _Tparametrable0 {
function setParametrableParams(?array $params): void {
$parametrables = $this->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);
}
}

View File

@ -0,0 +1,20 @@
<?php
namespace nur\data\types;
use nur\b\params\parametrable_utils;
/**
* variante de {@link Tparametrable1} qui n'utilise pas {@link Metadata} pour
* éviter une boucle infinie.
*
* NB: on ne met ici que les méthodes qui doivent pourvoir être surchargées.
* toutes les autres méthodes doivent être dans {@link _Tparametrable0}
*/
trait _Tparametrable1 {
use _Tparametrable0;
/** obtenir la destination de certains paramètres si elle diffère de $this */
protected function getParametrableParamsParametrables(): ?array {
return null;
}
}

View File

@ -0,0 +1,18 @@
<?php
namespace nur\data\types;
class md_utils {
static function ensure_md(?Metadata &$md, array $schema, ?array $params=null): Metadata {
if ($md === null) $md = new Metadata($schema, $params);
return $md;
}
static function ensure_md_func(?Metadata &$md, callable $get_md): Metadata {
if ($md === null) $md = $get_md();
return $md;
}
static function ensure_schema(&$item, ?Metadata &$md, array $schema, ?array $params=null): void {
self::ensure_md($md, $schema, $params)->ensureSchema($item);
}
}