nur-ture/src/schema/_scalar/ScalarWrapper.php

318 lines
9.4 KiB
PHP

<?php
namespace nur\sery\wip\schema\_scalar;
use nulib\php\func;
use nulib\ref\schema\ref_analyze;
use nulib\ValueException;
use nur\sery\wip\schema\WrapperContext;
use nur\sery\wip\schema\input\Input;
use nur\sery\wip\schema\types;
use nur\sery\wip\schema\types\IType;
use nur\sery\wip\schema\Wrapper;
class ScalarWrapper extends Wrapper {
function __construct(ScalarSchema $schema, &$value=null, $valueKey=null, ?array $params=null) {
$verifix = $params["verifix"] ?? true;
$throw = $params["throw"] ?? null;
if ($value !== null && $throw === null) {
# Si $value est null, ne pas lancer d'exception, parce qu'on considère que
# c'est une initialisation sans conséquences
$throw = true;
}
$this->verifix = $verifix;
$this->throw = $throw ?? false;
$this->schema = $schema;
$this->result = new ScalarResult();
$this->reset($value, $valueKey);
$this->throw = $throw ?? true;
}
function isScalar(?ScalarWrapper &$wrapper=null): bool { $wrapper = $this; return true; }
protected bool $verifix;
protected bool $throw;
/** schéma de cette valeur */
protected ScalarSchema $schema;
/** source et destination de la valeur */
protected Input $input;
/** @var string|int|null clé de la valeur dans le tableau destination */
protected $valueKey;
/** type de la valeur après analyse */
protected ?IType $type;
/** résultat de l'analyse de la valeur */
protected ScalarResult $result;
protected function newInput(&$value): Input {
return new Input($value);
}
function reset(&$value, $valueKey=null, ?bool $verifix=null): Wrapper {
if ($value instanceof Input) $input = $value;
else $input = $this->newInput($value);
$this->input = $input;
$this->valueKey = $valueKey;
$this->type = null;
$this->analyze();
if ($verifix ?? $this->verifix) $this->verifix();
return $this;
}
function getKeys(): array {
return [null];
}
/** @param string|int|null $key */
function select($key): ScalarWrapper {
if ($key !== null) throw ValueException::invalid_key($key);
return $this;
}
/** analyser la valeur et résoudre son type */
protected function analyze0(WrapperContext $context): int {
/** @var ScalarSchema $schema */
$schema = $context->schema;
$input = $context->input;
$valueKey = $context->valueKey;
/** @var ScalarResult $result */
$result = $context->result;
$default = $schema->default;
if (!$input->isPresent($valueKey)) {
if ($default !== null) {
$input->set($default, $valueKey);
return $result->setNormalized();
} else {
return $result->setMissing($schema);
}
}
$schemaTypes = $schema->type;
if ($schemaTypes instanceof IType) {
$type = $schemaTypes;
} else {
# type union
$haveType = false;
$types = [];
$type = $firstType = null;
$value = null;
# d'abord chercher un type pour lequel c'est une valeur normalisée
$index = 0;
$haveValue = false;
foreach ($schemaTypes as $key => $name) {
if ($key === $index) {
$index++;
$args = null;
} else {
$args = $name;
$name = $key;
}
$type = types::get($schema->nullable, $name, $args, $this->schema->getDefinition());
if ($firstType === null) $firstType = $type;
$types[] = $type;
if ($type->isAvailable($input, $valueKey)) {
if (!$haveValue) {
$value = $input->get($valueKey);
$haveValue = true;
}
if ($type->isValid($value, $normalized) && $normalized) {
$haveType = true;
break;
}
}
}
# ensuite chercher un type pour lequel la valeur est valide
if (!$haveType) {
foreach ($types as $type) {
if ($type->isAvailable($input, $valueKey) && $type->isValid($value)) {
$haveType = true;
break;
}
}
}
# sinon prendre le premier type
if (!$haveType) {
$type = $firstType;
}
}
$context->type = $this->type = $type;
if (!$type->isAvailable($input, $valueKey)) {
if ($default !== null) {
$input->set($default, $valueKey);
return $result->setNormalized();
} else {
return $result->setUnavailable($schema);
}
}
$value = $input->get($valueKey);
$context->origValue = $context->value = $value;
if ($type->isNull($value)) {
return $result->setNull($schema);
} elseif (is_string($value)) {
return ref_analyze::STRING;
} elseif ($type->isValid($value, $normalized)) {
if ($normalized) return $result->setNormalized();
else return $result->setValid();
} else {
return $result->setInvalid($value, $schema);
}
}
protected function analyze(): int {
$schema = $this->schema;
$input = $this->input;
$valueKey = $this->valueKey;
$result = $this->result;
$result->reset();
$context = new WrapperContext($schema, $this, $input, $valueKey, $result);
/** @var func $analyzerFunc */
$analyzerFunc = $schema->analyzerFunc;
if ($analyzerFunc !== null) $what = $analyzerFunc->invoke([$context]);
else $what = $this->analyze0($context);
if ($what !== ref_analyze::STRING) return $what;
$value = $context->value;
try {
/** @var func $extractorFunc */
$extractorFunc = $schema->extractorFunc;
if ($extractorFunc !== null) $extracted = $extractorFunc->invoke([$value, $context]);
else $extracted = $context->type->extract($value);
$context->value = $extracted;
} catch (ValueException $e) {
return $result->setInvalid($context->origValue, $schema, $e);
}
if ($context->type->isNull($extracted)) return $result->setNull($schema);
try {
/** @var func $parserFunc */
$parserFunc = $schema->parserFunc;
if ($parserFunc !== null) $parsed = $parserFunc->invoke([$extracted, $context]);
else $parsed = $context->type->parse($extracted);
$context->value = $parsed;
} catch (ValueException $e) {
return $result->setInvalid($context->origValue, $schema, $e);
}
$normalized = $parsed === $context->origValue;
if ($normalized) {
$input->set($parsed, $valueKey);
return $result->setNormalized();
} else {
$input->set($extracted, $valueKey);
return $result->setValid($parsed);
}
}
function verifix(?bool $throw=null): bool {
$result = $this->result;
$valueKey = $this->valueKey;
$verifix = false;
$modified = false;
if ($result->resultAvailable) {
if ($result->null) {
# forcer la valeur null, parce que la valeur actuelle est peut-être une
# valeur assimilée à null
$this->input->set(null, $valueKey);
} elseif ($result->valid && !$result->normalized) {
$normalizedValue = $result->normalizedValue;
if ($normalizedValue !== null) {
# la valeur normalisée est disponible
$this->input->set($normalizedValue);
$result->normalizedValue = null;
$modified = true;
} else {
# normaliser la valeur
$verifix = true;
}
}
} else {
$verifix = true;
}
if ($verifix) {
$value = $this->input->get($valueKey);
$schema = $this->schema;
/** @var func $normalizerFunc */
$normalizerFunc = $schema->normalizerFunc;
if ($normalizerFunc !== null) {
$context = new WrapperContext($schema, $this, $this->input, $valueKey, $result);
$orig = $value;
$value = $normalizerFunc->invoke([$orig, $context]);
$modified = $value !== $orig;
} else {
$modified = $this->type->verifix($value, $result, $this->schema);
}
if ($result->valid) $this->input->set($value, $valueKey);
}
if (!$result->valid) $result->throw($throw ?? $this->throw);
return $modified;
}
function getResult(): ScalarResult {
return $this->result;
}
function isPresent(): bool {
return $this->result->present;
}
function getType(): IType {
return $this->type;
}
function isAvailable(): bool {
return $this->result->available;
}
function isValid(): bool {
return $this->result->valid;
}
function isNormalized(): bool {
return $this->result->normalized;
}
function get($default=null) {
if ($this->result->available) return $this->input->get($this->valueKey);
else return $default;
}
function set($value, ?bool $verifix=null): ScalarWrapper {
$this->input->set($value, $this->valueKey);
$this->analyze();
if ($verifix ?? $this->verifix) $this->verifix();
return $this;
}
function unset(?bool $verifix=null): ScalarWrapper {
$this->input->unset($this->valueKey);
$this->analyze();
if ($verifix ?? $this->verifix) $this->verifix();
return $this;
}
function format($format=null): string {
$value = $this->input->get($this->valueKey);
/** @var func $formatterFunc */
$formatterFunc = $this->schema->formatterFunc;
if ($formatterFunc !== null) {
# la fonction formatter n'a pas forcément accès au format de la définition
# le lui fournir ici
$format ??= $this->schema->format;
return $formatterFunc->invoke([$value, $format]);
} else {
# on assume que le type a été initialisé avec le format de la définition
# le cas échéant
return $this->type->format($value, $format);
}
}
}