maj nur-sery

This commit is contained in:
Jephté Clain 2024-05-22 20:49:05 +04:00
parent 08a1ce49c5
commit 6755323cc5
116 changed files with 5571 additions and 281 deletions

15
.idea/codeception.xml Normal file
View File

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Codeception">
<option name="configurations">
<list>
<Configuration>
<option name="path" value="$PROJECT_DIR$/tests" />
</Configuration>
<Configuration>
<option name="path" value="$PROJECT_DIR$/tests" />
</Configuration>
</list>
</option>
</component>
</project>

View File

@ -2,15 +2,10 @@
<module type="WEB_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/php/src_base" isTestSource="false" packagePrefix="nulib\" />
<sourceFolder url="file://$MODULE_DIR$/php/tests" isTestSource="true" packagePrefix="nulib\" />
<sourceFolder url="file://$MODULE_DIR$/php/src_output" isTestSource="false" packagePrefix="nulib\output\" />
<sourceFolder url="file://$MODULE_DIR$/php/src_web" isTestSource="false" packagePrefix="nulib\web\" />
<sourceFolder url="file://$MODULE_DIR$/php/src_ref" isTestSource="false" packagePrefix="nulib\ref\" />
<sourceFolder url="file://$MODULE_DIR$/php/src_php" isTestSource="false" packagePrefix="nulib\php\" />
<sourceFolder url="file://$MODULE_DIR$/php/src_file" isTestSource="false" packagePrefix="nulib\file\" />
<sourceFolder url="file://$MODULE_DIR$/php/src_os" isTestSource="false" packagePrefix="nulib\os\" />
<sourceFolder url="file://$MODULE_DIR$/php/src_values" isTestSource="false" packagePrefix="nulib\values\" />
<sourceFolder url="file://$MODULE_DIR$/php/src" isTestSource="false" packagePrefix="nulib\" />
<sourceFolder url="file://$MODULE_DIR$/spec" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/tests" isTestSource="true" />
<excludeFolder url="file://$MODULE_DIR$/vendor" />
</content>
<orderEntry type="inheritedJdk" />

13
.idea/phpspec.xml Normal file
View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="PHPSpec">
<suites>
<PhpSpecSuiteConfiguration>
<option name="myPath" value="$PROJECT_DIR$" />
</PhpSpecSuiteConfiguration>
<PhpSpecSuiteConfiguration>
<option name="myPath" value="$PROJECT_DIR$" />
</PhpSpecSuiteConfiguration>
</suites>
</component>
</project>

10
.idea/phpunit.xml Normal file
View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="PHPUnit">
<option name="directories">
<list>
<option value="$PROJECT_DIR$/tests" />
</list>
</option>
</component>
</project>

39
php/src/A.php Normal file
View File

@ -0,0 +1,39 @@
<?php
namespace nulib;
use Traversable;
/**
* Class A: gestion de tableaux ou d'instances de {@link IArrayWrapper}
*
* contrairement à {@link cl}, les méthodes de cette classes sont plutôt conçues
* pour modifier le tableau en place
*/
class A {
/**
* s'assurer que $array est un array non null. retourner true si $array n'a
* pas été modifié (s'il était déjà un array), false sinon.
*/
static final function ensure_array(&$array): bool {
if (is_array($array)) return true;
if ($array instanceof IArrayWrapper) $array = $array->wrappedArray();
if ($array === null || $array === false) $array = [];
elseif ($array instanceof Traversable) $array = iterator_to_array($array);
else $array = [$array];
return false;
}
/**
* s'assurer que $array est un array s'il est non null. retourner true si
* $array n'a pas été modifié (s'il était déjà un array ou s'il valait null).
*/
static final function ensure_narray(&$array): bool {
if ($array === null || is_array($array)) return true;
if ($array instanceof IArrayWrapper) $array = $array->wrappedArray();
if ($array === false) $array = [];
elseif ($array instanceof Traversable) $array = iterator_to_array($array);
else $array = [$array];
return false;
}
}

View File

@ -1,19 +0,0 @@
<?php
namespace nulib;
use RuntimeException;
/**
* Class DataException: exception générique concernant l'accès à des données
*/
class DataException extends RuntimeException {
static final function no_more_data(): self {
return new self("no more data");
}
static final function ensure_not_eof($data, bool $throw=true, $eof=false) {
if (!$throw) return null;
elseif ($data !== $eof) return $data;
else throw self::no_more_data();
}
}

View File

@ -8,8 +8,8 @@ use Throwable;
* quitter normalement, avec éventuellement un code d'erreur.
*/
class ExitException extends UserException {
function __construct(int $exitcode=0, $user_message=null, Throwable $previous=null) {
parent::__construct($user_message, null, $exitcode, $previous);
function __construct(int $exitcode=0, $userMessage=null, Throwable $previous=null) {
parent::__construct($userMessage, null, $exitcode, $previous);
}
function isError(): bool {

11
php/src/IArrayWrapper.php Normal file
View File

@ -0,0 +1,11 @@
<?php
namespace nulib;
/**
* Interface IArrayWrapper: un objet qui encapsule un array auquel on peut
* accéder
*/
interface IArrayWrapper {
/** retourne une référence sur l'array encapsulé */
function &wrappedArray(): ?array;
}

View File

@ -75,12 +75,13 @@ class UserException extends RuntimeException {
return implode("\n", $tbs);
}
function __construct($user_message, $tech_message=null, $code=0, ?Throwable $previous=null) {
$this->userMessage = $user_message;
if ($tech_message === null) $tech_message = $user_message;
parent::__construct($tech_message, $code, $previous);
function __construct($userMessage, $techMessage=null, $code=0, ?Throwable $previous=null) {
$this->userMessage = $userMessage;
if ($techMessage === null) $techMessage = $userMessage;
parent::__construct($techMessage, $code, $previous);
}
/** @var ?string */
protected $userMessage;
function getUserMessage(): ?string {

View File

@ -5,7 +5,11 @@ use ArrayAccess;
use Traversable;
/**
* Class cl: gestion de tableau de valeurs scalaires
* Class cl: gestion de tableaux ou d'instances de {@link ArrayAccess} le cas
* échéant
*
* contrairement à {@link A}, les méthodes de cette classes sont plutôt conçues
* pour retourner un nouveau tableau
*/
class cl {
/** retourner un array non null à partir de $array */
@ -24,30 +28,6 @@ class cl {
else return [$array];
}
/**
* s'assurer que $array est un array non null. retourner true si $array n'a
* pas été modifié (s'il était déjà un array), false sinon.
*/
static final function ensure_array(&$array): bool {
if (is_array($array)) return true;
elseif ($array === null || $array === false) $array = [];
elseif ($array instanceof Traversable) $array = iterator_to_array($array);
else $array = [$array];
return false;
}
/**
* s'assurer que $array est un array s'il est non null. retourner true si
* $array n'a pas été modifié (s'il était déjà un array ou s'il valait null).
*/
static final function ensure_narray(&$array): bool {
if ($array === null || is_array($array)) return true;
elseif ($array === false) $array = [];
elseif ($array instanceof Traversable) $array = iterator_to_array($array);
else $array = [$array];
return false;
}
/** tester si $array a au moins une clé numérique */
static final function have_num_keys(?array $array): bool {
if ($array === null) return false;
@ -159,7 +139,7 @@ class cl {
static final function merge(...$arrays): ?array {
$merges = [];
foreach ($arrays as $array) {
self::ensure_narray($array);
A::ensure_narray($array);
if ($array !== null) $merges[] = $array;
}
return $merges? array_merge(...$merges): null;
@ -170,7 +150,7 @@ class cl {
/**
* vérifier que le chemin $keys existe dans le tableau $array
*
* si $keys est vide ou null, retourner true
* si $pkey est vide ou null, retourner true
*/
static final function phas($array, $pkey): bool {
# optimisations
@ -214,7 +194,7 @@ class cl {
/**
* obtenir la valeur correspondant au chemin $keys dans $array
*
* si $keys est vide ou null, retourner $default
* si $pkey est vide ou null, retourner $default
*/
static final function pget($array, $pkey, $default=null) {
# optimisations
@ -264,7 +244,7 @@ class cl {
* - pset($array, ["a", "b", ""], $value) est équivalent à $array["a"]["b"][] = $value
* la clé "" n'a pas de propriété particulière quand elle n'est pas en dernière position
*
* si $keys est vide ou null, $array est remplacé par $value
* si $pkey est vide ou null, $array est remplacé par $value
*/
static final function pset(&$array, $pkey, $value): void {
# optimisations
@ -281,7 +261,7 @@ class cl {
$pkey = explode(".", strval($pkey));
}
# pset
self::ensure_array($array);
A::ensure_array($array);
$current =& $array;
$key = null;
$last = count($pkey) - 1;
@ -297,7 +277,7 @@ class cl {
$current = [$current];
}
} else {
self::ensure_array($current[$key]);
A::ensure_array($current[$key]);
$current =& $current[$key];
}
$i++;
@ -318,7 +298,7 @@ class cl {
* supprimer la valeur au chemin de clé $keys dans $array
*
* si $array vaut null ou false, sa valeur est inchangée.
* si $keys est vide ou null, $array devient null
* si $pkey est vide ou null, $array devient null
*/
static final function pdel(&$array, $pkey): void {
# optimisations
@ -334,7 +314,7 @@ class cl {
$pkey = explode(".", strval($pkey));
}
# pdel
self::ensure_array($array);
A::ensure_array($array);
$current =& $array;
$key = null;
$last = count($pkey) - 1;

56
php/src/db/Capacitor.php Normal file
View File

@ -0,0 +1,56 @@
<?php
namespace nulib\db;
/**
* Class Capacitor: un objet permettant d'attaquer un canal spécique d'une
* instance de {@link CapacitorStorage}
*/
class Capacitor {
function __construct(CapacitorStorage $storage, CapacitorChannel $channel, bool $ensureExists=true) {
$this->storage = $storage;
$this->channel = $channel;
if ($ensureExists) $this->ensureExists();
}
/** @var CapacitorStorage */
protected $storage;
/** @var CapacitorChannel */
protected $channel;
function exists(): bool {
return $this->storage->_exists($this->channel);
}
function ensureExists(): void {
$this->storage->_ensureExists($this->channel);
}
function reset(): void {
$this->storage->_reset($this->channel);
}
function charge($item, ?callable $func=null, ?array $args=null): int {
return $this->storage->_charge($this->channel, $item, $func, $args);
}
function count($filter=null): int {
return $this->storage->_count($this->channel, $filter);
}
function discharge($filter=null, ?bool $reset=null): iterable {
return $this->storage->_discharge($this->channel, $filter, $reset);
}
function get($filter) {
return $this->storage->_get($this->channel, $filter);
}
function each($filter, ?callable $func=null, ?array $args=null): int {
return $this->storage->_each($this->channel, $filter, $func, $args);
}
function close(): void {
$this->storage->close();
}
}

View File

@ -0,0 +1,111 @@
<?php
namespace nulib\db;
/**
* Class CapacitorChannel: un canal d'une instance de {@link ICapacitor}
*/
class CapacitorChannel {
const NAME = null;
const EACH_COMMIT_THRESHOLD = 100;
static function verifix_name(?string $name): string {
if ($name === null) $name = "default";
return strtolower($name);
}
function __construct(?string $name=null, ?int $eachCommitThreshold=null) {
$this->name = self::verifix_name($name ?? static::NAME);
$this->eachCommitThreshold = $eachCommitThreshold ?? static::EACH_COMMIT_THRESHOLD;
$this->created = false;
}
/** @var string */
protected $name;
function getName(): string {
return $this->name;
}
/**
* @var ?int nombre maximum de modifications dans une transaction avant un
* commit automatique dans {@link Capacitor::each()}. Utiliser null pour
* désactiver la fonctionnalité.
*/
protected $eachCommitThreshold;
function getEachCommitThreshold(): ?int {
return $this->eachCommitThreshold;
}
function getTableName(): string {
return $this->name."_channel";
}
protected $created;
function isCreated(): bool {
return $this->created;
}
function setCreated(bool $created=true): void {
$this->created = $created;
}
/**
* retourner un ensemble de définitions pour des colonnes supplémentaires à
* insérer lors du chargement d'une valeur
*
* la colonne "_id" de définition "integer primary key autoincrement" est la
* clé primaire par défaut. elle peut être redéfinie, et dans ce cas la valeur
* à utiliser doit être retournée par {@link getKeyValues()}
*/
function getKeyDefinitions(): ?array {
return null;
}
/**
* calculer les valeurs des colonnes supplémentaires à insérer pour le
* chargement de $item
*
* Si "_id" est retourné, la ligne existante est mise à jour le cas échéant.
*/
function getKeyValues($item): ?array {
return null;
}
/**
* méthode appelée lors du chargement d'un élément avec
* {@link Capacitor::charge()}
*
* @param mixed $item l'élément à charger
* @param array $values les valeurs calculées par {@link getKeyValues()}
* @param ?array $row la ligne à mettre à jour. vaut null s'il faut insérer
* une nouvelle ligne
* @return ?array le cas échéant, un tableau non null à marger dans $values et
* utiliser pour provisionner la ligne nouvelle créée, ou mettre à jour la
* ligne existante
*
* Si $item est modifié dans cette méthode, il est possible de le retourner
* avec la clé "_item" pour mettre à jour la ligne correspondante
*/
function onCharge($item, array $values, ?array $row): ?array {
return null;
}
/**
* méthode appelée lors du parcours des éléments avec
* {@link Capacitor::each()}
*
* @param mixed $item l'élément courant
* @param ?array $row la ligne à mettre à jour
* @return ?array le cas échéant, un tableau non null utilisé pour mettre à
* jour la ligne courante
*
* Si $item est modifié dans cette méthode, il est possible de le retourner
* avec la clé "_item" pour mettre à jour la ligne correspondante
*/
function onEach($item, array $row): ?array {
return null;
}
}

View File

@ -0,0 +1,92 @@
<?php
namespace nulib\db;
/**
* Class CapacitorStorage: objet permettant d'accumuler des données pour les
* réutiliser plus tard
*/
abstract class CapacitorStorage {
abstract protected function getChannel(?string $name): CapacitorChannel;
abstract function _exists(CapacitorChannel $channel): bool;
/** tester si le canal spécifié existe */
function exists(?string $channel): bool {
return $this->_exists($this->getChannel($channel));
}
abstract function _ensureExists(CapacitorChannel $channel): void;
/** s'assurer que le canal spécifié existe */
function ensureExists(?string $channel): void {
$this->_ensureExists($this->getChannel($channel));
}
abstract function _reset(CapacitorChannel $channel): void;
/** supprimer le canal spécifié */
function reset(?string $channel): void {
$this->_reset($this->getChannel($channel));
}
abstract function _charge(CapacitorChannel $channel, $item, ?callable $func, ?array $args): int;
/**
* charger une valeur dans le canal
*
* Si $func!==null, après avoir calculé les valeurs des clés supplémentaires
* avec {@link CapacitorChannel::getKeyValues()}, la fonction est appelée avec
* la signature ($item, $keyValues, $row, ...$args)
* Si la fonction retourne un tableau, il est utilisé pour modifier les valeurs
* insérées/mises à jour
*
* @return int 1 si l'objet a été chargé ou mis à jour, 0 s'il existait
* déjà à l'identique dans le canal
*/
function charge(?string $channel, $item, ?callable $func=null, ?array $args=null): int {
return $this->_charge($this->getChannel($channel), $item, $func, $args);
}
abstract function _count(CapacitorChannel $channel, $filter): int;
/** indiquer le nombre d'éléments du canal spécifié */
function count(?string $channel, $filter=null): int {
return $this->_count($this->getChannel($channel), $filter);
}
abstract function _discharge(CapacitorChannel $channel, $filter, ?bool $reset): iterable;
/** décharger les données du canal spécifié */
function discharge(?string $channel, $filter=null, ?bool $reset=null): iterable {
return $this->_discharge($this->getChannel($channel), $filter, $reset);
}
abstract function _get(CapacitorChannel $channel, $filter);
/**
* obtenir l'élément identifié par les clés spécifiées sur le canal spécifié
*
* si $filter n'est pas un tableau, il est transformé en ["_id" => $filter]
*/
function get(?string $channel, $filter) {
return $this->_get($this->getChannel($channel), $filter);
}
abstract function _each(CapacitorChannel $channel, $filter, ?callable $func, ?array $args): int;
/**
* appeler une fonction pour chaque élément du canal spécifié.
*
* $filter permet de filtrer parmi les élements chargés
*
* $func est appelé avec la signature ($item, $row, ...$args). si la fonction
* retourne un tableau, il est utilisé pour mettre à jour la ligne
*
* @return int le nombre de lignes parcourues
*/
function each(?string $channel, $filter, ?callable $func=null, ?array $args=null): int {
return $this->_each($this->getChannel($channel), $filter, $func, $args);
}
abstract function close(): void;
}

View File

@ -0,0 +1,226 @@
<?php
namespace nulib\db\sqlite;
use Generator;
use nulib\cl;
use SQLite3;
use SQLite3Result;
use SQLite3Stmt;
/**
* Class Sqlite: frontend vers une base de données sqlite3
*/
class Sqlite {
static function with($sqlite, ?array $params=null): self {
if ($sqlite instanceof static) {
return $sqlite;
} elseif ($sqlite instanceof self) {
# recréer avec les mêmes paramètres
return new static(null, cl::merge([
"file" => $sqlite->file,
"flags" => $sqlite->flags,
"encryption_key" => $sqlite->encryptionKey,
"allow_wal" => $sqlite->allowWal,
"config" => $sqlite->config,
"migrate" => $sqlite->migration,
], $params));
} elseif (is_array($sqlite)) {
return new static(null, cl::merge($sqlite, $params));
} else {
return new static($sqlite, $params);
}
}
static function config_enableExceptions(self $sqlite): void {
$sqlite->db->enableExceptions(true);
}
static function config_enableWalIfAllowed(self $sqlite): void {
if ($sqlite->isWalAllowed()) {
$sqlite->db->exec("PRAGMA journal_mode=WAL");
}
}
const ALLOW_WAL = null;
const CONFIG = [
[self::class, "config_enableExceptions"],
[self::class, "config_enableWalIfAllowed"],
];
const MIGRATE = null;
const SCHEMA = [
"file" => ["string", ""],
"flags" => ["int", SQLITE3_OPEN_READWRITE + SQLITE3_OPEN_CREATE],
"encryption_key" => ["string", ""],
"allow_wal" => ["?bool"],
"config" => ["?array|callable"],
"migrate" => ["?array|string|callable"],
"auto_open" => ["bool", true],
];
function __construct(?string $file=null, ?array $params=null) {
if ($file !== null) $params["file"] = $file;
##schéma
$defaultFile = self::SCHEMA["file"][1];
$this->file = $file = strval($params["file"] ?? $defaultFile);
$inMemory = $file === ":memory:";
#
$defaultFlags = self::SCHEMA["flags"][1];
$this->flags = intval($params["flags"] ?? $defaultFlags);
#
$defaultEncryptionKey = self::SCHEMA["encryption_key"][1];
$this->encryptionKey = strval($params["encryption_key"] ?? $defaultEncryptionKey);
#
$defaultAllowWal = static::ALLOW_WAL ?? !$inMemory;
$this->allowWal = $params["allow_wal"] ?? $defaultAllowWal;
# configuration
$this->config = $params["config"] ?? static::CONFIG;
# migrations
$this->migration = $params["migrate"] ?? static::MIGRATE;
#
$defaultAutoOpen = self::SCHEMA["auto_open"][1];
if ($params["auto_open"] ?? $defaultAutoOpen) {
$this->open();
}
}
/** @var string */
protected $file;
/** @var int */
protected $flags;
/** @var string */
protected $encryptionKey;
/** @var bool */
protected $allowWal;
/** vérifier s'il est autorisé de configurer le mode WAL */
function isWalAllowed(): bool {
return $this->allowWal;
}
/** @var array|string|callable */
protected $config;
/** @var array|string|callable */
protected $migration;
/** @var SQLite3 */
protected $db;
function open(): self {
if ($this->db === null) {
$this->db = new SQLite3($this->file, $this->flags, $this->encryptionKey);
_config::with($this->config)->configure($this);
_migration::with($this->migration)->migrate($this);
}
return $this;
}
function close(): void {
if ($this->db !== null) {
$this->db->close();
$this->db = null;
}
}
protected function checkStmt($stmt): SQLite3Stmt {
return SqliteException::check($this->db, $stmt);
}
protected function checkResult($result): SQLite3Result {
return SqliteException::check($this->db, $result);
}
protected function db(): SQLite3 {
$this->open();
return $this->db;
}
function _exec(string $query): bool {
return $this->db()->exec($query);
}
function exec($query, ?array $params=null): bool {
$db = $this->db();
$query = new _query($query, $params);
if ($query->useStmt($db, $stmt, $sql)) {
try {
return $stmt->execute()->finalize();
} finally {
$stmt->close();
}
} else {
return $db->exec($sql);
}
}
function beginTransaction(): void {
$this->db()->exec("begin");
}
function commit(): void {
$this->db()->exec("commit");
}
function rollback(): void {
$this->db()->exec("commit");
}
function _get(string $query, bool $entireRow=false) {
return $this->db()->querySingle($query, $entireRow);
}
function get($query, ?array $params=null, bool $entireRow=false) {
$db = $this->db();
$query = new _query($query, $params);
if ($query->useStmt($db, $stmt, $sql)) {
try {
$result = $this->checkResult($stmt->execute());
try {
$row = $result->fetchArray(SQLITE3_ASSOC);
if ($row === false) return null;
elseif ($entireRow) return $row;
else return cl::first($row);
} finally {
$result->finalize();
}
} finally {
$stmt->close();
}
} else {
return $db->querySingle($sql, $entireRow);
}
}
function one($query, ?array $params=null): ?array {
return $this->get($query, $params, true);
}
protected function _fetchResult(SQLite3Result $result, ?SQLite3Stmt $stmt=null): Generator {
try {
while (($row = $result->fetchArray(SQLITE3_ASSOC)) !== false) {
yield $row;
}
} finally {
$result->finalize();
if ($stmt !== null) $stmt->close();
}
}
function all($query, ?array $params=null): iterable {
$db = $this->db();
$query = new _query($query, $params);
if ($query->useStmt($db, $stmt, $sql)) {
$result = $this->checkResult($stmt->execute());
return $this->_fetchResult($result, $stmt);
} else {
$result = $this->checkResult($db->query($sql));
return $this->_fetchResult($result);
}
}
}

View File

@ -0,0 +1,241 @@
<?php
namespace nulib\db\sqlite;
use nulib\cl;
use nulib\db\CapacitorChannel;
use nulib\db\CapacitorStorage;
use nulib\php\func;
use nulib\ValueException;
/**
* Class SqliteCapacitor
*/
class SqliteCapacitor extends CapacitorStorage {
function __construct($sqlite) {
$this->sqlite = Sqlite::with($sqlite);
}
/** @var Sqlite */
protected $sqlite;
function sqlite(): Sqlite {
return $this->sqlite;
}
protected function _create(CapacitorChannel $channel): void {
if (!$channel->isCreated()) {
$columns = cl::merge([
"_id" => "integer primary key autoincrement",
"_item" => "text",
"_sum" => "varchar(40)",
"_created" => "datetime",
"_modified" => "datetime",
], $channel->getKeyDefinitions());
$this->sqlite->exec([
"create table if not exists",
"table" => $channel->getTableName(),
"cols" => $columns,
]);
$channel->setCreated();
}
}
/** @var CapacitorChannel[] */
protected $channels;
function addChannel(CapacitorChannel $channel): CapacitorChannel {
$this->_create($channel);
$this->channels[$channel->getName()] = $channel;
return $channel;
}
protected function getChannel(?string $name): CapacitorChannel {
$name = CapacitorChannel::verifix_name($name);
$channel = $this->channels[$name] ?? null;
if ($channel === null) {
$channel = $this->addChannel(new CapacitorChannel($name));
}
return $channel;
}
function _exists(CapacitorChannel $channel): bool {
$tableName = $this->sqlite->get([
"select name from sqlite_schema",
"where" => [
"name" => $channel->getTableName(),
],
]);
return $tableName !== null;
}
function _ensureExists(CapacitorChannel $channel): void {
$this->_create($channel);
}
function _reset(CapacitorChannel $channel): void {
$this->sqlite->exec([
"drop table if exists",
$channel->getTableName(),
]);
$channel->setCreated(false);
}
function _charge(CapacitorChannel $channel, $item, ?callable $func, ?array $args): int {
$this->_create($channel);
$now = date("Y-m-d H:i:s");
$_item = serialize($item);
$_sum = sha1($_item);
$values = cl::merge([
"_item" => $_item,
"_sum" => $_sum,
], $channel->getKeyValues($item));
$row = null;
$id = $values["_id"] ?? null;
if ($id !== null) {
# modification
$row = $this->sqlite->one([
"select _item, _sum, _created, _modified",
"from" => $channel->getTableName(),
"where" => ["_id" => $id],
]);
}
$insert = null;
if ($row === null) {
# création
$values = cl::merge($values, [
"_created" => $now,
"_modified" => $now,
]);
$insert = true;
} elseif ($_sum !== $row["_sum"]) {
# modification
$values = cl::merge($values, [
"_modified" => $now,
]);
$insert = false;
}
if ($func === null) $func = [$channel, "onCharge"];
$onCharge = func::_prepare($func);
$args ??= [];
$updates = func::_call($onCharge, [$item, $values, $row, ...$args]);
if (is_array($updates)) {
if (array_key_exists("_item", $updates)) {
$_item = serialize($updates["_item"]);
$updates["_item"] = $_item;
$updates["_sum"] = sha1($_item);
if (!array_key_exists("_modified", $updates)) {
$updates["_modified"] = $now;
}
}
$values = cl::merge($values, $updates);
}
if ($insert === null) {
# aucune modification
return 0;
} elseif ($insert) {
$this->sqlite->exec([
"insert",
"into" => $channel->getTableName(),
"values" => $values,
]);
} else {
$this->sqlite->exec([
"update",
"table" => $channel->getTableName(),
"values" => $values,
"where" => ["_id" => $id],
]);
}
return 1;
}
function _count(CapacitorChannel $channel, $filter): int {
if ($filter !== null && !is_array($filter)) $filter = ["_id" => $filter];
return $this->sqlite->get([
"select count(*)",
"from" => $channel->getTableName(),
"where" => $filter,
]);
}
function _discharge(CapacitorChannel $channel, $filter, ?bool $reset): iterable {
if ($filter !== null && !is_array($filter)) $filter = ["_id" => $filter];
if ($reset === null) $reset = $filter === null;
$rows = $this->sqlite->all([
"select _item",
"from" => $channel->getTableName(),
"where" => $filter,
]);
foreach ($rows as $row) {
$item = unserialize($row['_item']);
yield $item;
}
if ($reset) $this->_reset($channel);
}
function _get(CapacitorChannel $channel, $filter) {
if ($filter === null) throw ValueException::null("keys");
if (!is_array($filter)) $filter = ["_id" => $filter];
$row = $this->sqlite->one([
"select _item",
"from" => $channel->getTableName(),
"where" => $filter,
]);
if ($row === null) return null;
else return unserialize($row["_item"]);
}
function _each(CapacitorChannel $channel, $filter, ?callable $func, ?array $args): int {
if ($func === null) $func = [$channel, "onEach"];
$onEach = func::_prepare($func);
if ($filter !== null && !is_array($filter)) $filter = ["_id" => $filter];
$sqlite = $this->sqlite;
$tableName = $channel->getTableName();
$commited = false;
$count = 0;
$sqlite->beginTransaction();
$commitThreshold = $channel->getEachCommitThreshold();
try {
$rows = $sqlite->all([
"select",
"from" => $tableName,
"where" => $filter,
]);
$args ??= [];
foreach ($rows as $row) {
$item = unserialize($row['_item']);
$updates = func::_call($onEach, [$item, $row, ...$args]);
if (is_array($updates)) {
if (array_key_exists("_item", $updates)) {
$updates["_item"] = serialize($updates["_item"]);
}
$sqlite->exec([
"update",
"table" => $tableName,
"values" => $updates,
"where" => ["_id" => $row["_id"]],
]);
if ($commitThreshold !== null) {
$commitThreshold--;
if ($commitThreshold == 0) {
$sqlite->commit();
$commitThreshold = $channel->getEachCommitThreshold();
}
}
}
$count++;
}
$sqlite->commit();
$commited = true;
return $count;
} finally {
if (!$commited) $sqlite->rollback();
}
}
function close(): void {
$this->sqlite->close();
}
}

View File

@ -0,0 +1,18 @@
<?php
namespace nulib\db\sqlite;
use Exception;
use RuntimeException;
use SQLite3;
class SqliteException extends RuntimeException {
static final function check(SQLite3 $db, $value, bool $throw=true) {
if ($value !== false) return $value;
elseif (!$throw) return null;
else throw new static($db->lastErrorMsg(), $db->lastErrorCode());
}
static final function wrap(Exception $e): self{
return new static($e->getMessage(), $e->getCode(), $e);
}
}

View File

@ -0,0 +1,36 @@
<?php
namespace nulib\db\sqlite;
use nulib\php\func;
class _config {
static function with($configs): self {
if ($configs instanceof static) return $configs;
return new static($configs);
}
const CONFIG = null;
function __construct($configs) {
if ($configs === null) $configs = static::CONFIG;
if ($configs === null) $configs = [];
elseif (is_string($configs)) $configs = [$configs];
elseif (is_callable($configs)) $configs = [$configs];
elseif (!is_array($configs)) $configs = [strval($configs)];
$this->configs = $configs;
}
/** @var array */
protected $configs;
function configure(Sqlite $sqlite): void {
foreach ($this->configs as $key => $config) {
if (is_string($config) && !func::is_method($config)) {
$sqlite->exec($config);
} else {
func::ensure_func($config, $this, $args);
func::call($config, $sqlite, $key, ...$args);
}
}
}
}

View File

@ -0,0 +1,55 @@
<?php
namespace nulib\db\sqlite;
use nulib\php\func;
class _migration {
static function with($migrations): self {
if ($migrations instanceof static) {
return $migrations;
} elseif ($migrations instanceof self) {
return new static($migrations->migrations);
} else {
return new static($migrations);
}
}
const MIGRATE = null;
function __construct($migrations) {
if ($migrations === null) $migrations = static::MIGRATE;
if ($migrations === null) $migrations = [];
elseif (is_string($migrations)) $migrations = [$migrations];
elseif (is_callable($migrations)) $migrations = [$migrations];
elseif (!is_array($migrations)) $migrations = [strval($migrations)];
$this->migrations = $migrations;
}
/** @var callable[]|string[] */
protected $migrations;
function migrate(Sqlite $sqlite): void {
$sqlite->exec("create table if not exists _migration(key varchar primary key, value varchar not null, done integer default 0)");
foreach ($this->migrations as $key => $migration) {
$exists = $sqlite->get("select 1 from _migration where key = :key and done = 1", [
"key" => $key,
]);
if (!$exists) {
$sqlite->exec("insert or replace into _migration(key, value, done) values(:key, :value, :done)", [
"key" => $key,
"value" => $migration,
"done" => 0,
]);
if (is_string($migration) && !func::is_method($migration)) {
$sqlite->exec($migration);
} else {
func::ensure_func($migration, $this, $args);
func::call($migration, $sqlite, $key, ...$args);
}
$sqlite->exec("update _migration set done = 1 where key = :key", [
"key" => $key,
]);
}
}
}
}

View File

@ -0,0 +1,212 @@
<?php
namespace nulib\db\sqlite;
use nulib\cl;
use nulib\str;
use nulib\ValueException;
use SQLite3;
use SQLite3Stmt;
class _query {
static function verifix(&$query, ?array &$params=null): void {
if (is_array($query)) {
$prefix = $query[0] ?? null;
if ($prefix === null) {
throw new ValueException("requête invalide");
} elseif (_query_create::isa($prefix)) {
$query = _query_create::parse($query, $params);
} elseif (_query_select::isa($prefix)) {
$query = _query_select::parse($query, $params);
} elseif (_query_insert::isa($prefix)) {
$query = _query_insert::parse($query, $params);
} elseif (_query_update::isa($prefix)) {
$query = _query_update::parse($query, $params);
} elseif (_query_delete::isa($prefix)) {
$query = _query_delete::parse($query, $params);
} elseif (_query_generic::isa($prefix)) {
$query = _query_generic::parse($query, $params);
} else {
throw SqliteException::wrap(ValueException::invalid_kind($query, "query"));
}
} elseif (!is_string($query)) {
$query = strval($query);
}
}
protected static function consume(string $pattern, string &$string, ?array &$ms=null): bool {
if (!preg_match("/^$pattern/i", $string, $ms)) return false;
$string = substr($string, strlen($ms[0]));
return true;
}
/** fusionner toutes les parties séquentielles d'une requête */
protected static function merge_seq(array $query): string {
$index = 0;
$sql = "";
foreach ($query as $key => $value) {
if ($key === $index) {
$index++;
if ($sql && !str::ends_with(" ", $sql) && !str::starts_with(" ", $value)) {
$sql .= " ";
}
$sql .= $value;
}
}
return $sql;
}
protected static function is_sep(&$cond): bool {
if (!is_string($cond)) return false;
if (!preg_match('/^\s*(and|or|not)\s*$/i', $cond, $ms)) return false;
$cond = $ms[1];
return true;
}
static function parse_conds(?array $conds, ?array &$sql, ?array &$params): void {
if (!$conds) return;
$sep = null;
$index = 0;
$condsql = [];
foreach ($conds as $key => $cond) {
if ($key === $index) {
## séquentiel
if ($index === 0 && self::is_sep($cond)) {
$sep = $cond;
} elseif (is_array($cond)) {
# condition récursive
self::parse_conds($cond, $condsql, $params);
} else {
# condition litérale
$condsql[] = strval($cond);
}
$index++;
} else {
## associatif
# paramètre
$param = $key;
if ($params !== null && array_key_exists($param, $params)) {
$i = 1;
while (array_key_exists("$key$i", $params)) {
$i++;
}
$param = "$key$i";
}
# value ou [operator, value]
if (is_array($cond)) {
#XXX implémenter le support de ["between", lower, upper]
# et aussi ["in", values]
$op = null;
$value = null;
$condkeys = array_keys($cond);
if (array_key_exists("op", $cond)) $op = $cond["op"];
if (array_key_exists("value", $cond)) $value = $cond["value"];
$condkey = 0;
if ($op === null && array_key_exists($condkey, $condkeys)) {
$op = $cond[$condkeys[$condkey]];
$condkey++;
}
if ($value === null && array_key_exists($condkey, $condkeys)) {
$value = $cond[$condkeys[$condkey]];
$condkey++;
}
} else {
$op = "=";
$value = $cond;
}
$cond = [$key, $op];
if ($value !== null) {
$cond[] = ":$param";
$params[$param] = $value;
}
$condsql[] = implode(" ", $cond);
}
}
if ($sep === null) $sep = "and";
$count = count($condsql);
if ($count > 1) {
$sql[] = "(" . implode(" $sep ", $condsql) . ")";
} elseif ($count == 1) {
$sql[] = $condsql[0];
}
}
static function parse_set_values(?array $values, ?array &$sql, ?array &$params): void {
if (!$values) return;
$index = 0;
$parts = [];
foreach ($values as $key => $part) {
if ($key === $index) {
## séquentiel
if (is_array($part)) {
# paramètres récursifs
self::parse_set_values($part, $parts, $params);
} else {
# paramètre litéral
$parts[] = strval($part);
}
$index++;
} else {
## associatif
# paramètre
$param = $key;
if ($params !== null && array_key_exists($param, $params)) {
$i = 1;
while (array_key_exists("$key$i", $params)) {
$i++;
}
$param = "$key$i";
}
# value
$value = $part;
$part = [$key, "="];
if ($value === null) {
$part[] = "null";
} else {
$part[] = ":$param";
$params[$param] = $value;
}
$parts[] = implode(" ", $part);
}
}
$sql = cl::merge($sql, $parts);
}
protected static function check_eof(string $tmpsql, string $usersql): void {
self::consume(';\s*', $tmpsql);
if ($tmpsql) {
throw new ValueException("unexpected value at end: $usersql");
}
}
function __construct($sql, ?array $params=null) {
self::verifix($sql, $params);
$this->sql = $sql;
$this->params = $params;
}
/** @var string */
protected $sql;
/** @var ?array */
protected $params;
function useStmt(SQLite3 $db, ?SQLite3Stmt &$stmt=null, ?string &$sql=null): bool {
if ($this->params !== null) {
/** @var SQLite3Stmt $stmt */
$stmt = SqliteException::check($db, $db->prepare($this->sql));
$close = true;
try {
foreach ($this->params as $param => $value) {
SqliteException::check($db, $stmt->bindValue($param, $value));
}
$close = false;
return true;
} finally {
if ($close) $stmt->close();
}
} else {
$sql = $this->sql;
return false;
}
}
}

View File

@ -0,0 +1,47 @@
<?php
namespace nulib\db\sqlite;
class _query_create extends _query {
const SCHEMA = [
"prefix" => "?string",
"table" => "string",
"schema" => "?array",
"cols" => "?array",
"suffix" => "?string",
];
static function isa(string $sql): bool {
//return preg_match("/^create(?:\s+table)?\b/i", $sql);
#XXX implémentation minimale
return preg_match("/^create\s+table\b/i", $sql);
}
static function parse(array $query, ?array &$params=null): string {
#XXX implémentation minimale
$sql = [self::merge_seq($query)];
## préfixe
if (($prefix = $query["prefix"] ?? null) !== null) $sql[] = $prefix;
## table
$sql[] = $query["table"];
## columns
$cols = $query["cols"];
$index = 0;
foreach ($cols as $col => &$definition) {
if ($col === $index) {
$index++;
} else {
$definition = "$col $definition";
}
}; unset($definition);
$sql[] = "(".implode(", ", $cols).")";
## suffixe
if (($suffix = $query["suffix"] ?? null) !== null) $sql[] = $suffix;
## fin de la requête
return implode(" ", $sql);
}
}

View File

@ -0,0 +1,44 @@
<?php
namespace nulib\db\sqlite;
class _query_delete extends _query {
const SCHEMA = [
"prefix" => "?string",
"from" => "?string",
"where" => "?array",
"suffix" => "?string",
];
static function isa(string $sql): bool {
//return preg_match("/^delete(?:\s+from)?\b/i", $sql);
#XXX implémentation minimale
return preg_match("/^delete\s+from\b/i", $sql);
}
static function parse(array $query, ?array &$params=null): string {
#XXX implémentation minimale
$sql = [self::merge_seq($query)];
## préfixe
if (($prefix = $query["prefix"] ?? null) !== null) $sql[] = $prefix;
## table
$sql[] = $query["table"];
## where
$where = $query["where"] ?? null;
if ($where !== null) {
_query::parse_conds($where, $wheresql, $params);
if ($wheresql) {
$sql[] = "where";
$sql[] = implode(" and ", $wheresql);
}
}
## suffixe
if (($suffix = $query["suffix"] ?? null) !== null) $sql[] = $suffix;
## fin de la requête
return implode(" ", $sql);
}
}

View File

@ -0,0 +1,18 @@
<?php
namespace nulib\db\sqlite;
use nulib\cl;
use nulib\ValueException;
class _query_generic extends _query {
static function isa(string $sql): bool {
return preg_match('/^(?:drop\s+table)\b/i', $sql);
}
static function parse(array $query, ?array &$params=null): string {
if (!cl::is_list($query)) {
throw new ValueException("Seuls les tableaux séquentiels sont supportés");
}
return self::merge_seq($query);
}
}

View File

@ -0,0 +1,91 @@
<?php
namespace nulib\db\sqlite;
use nulib\cl;
use nulib\ValueException;
class _query_insert extends _query {
const SCHEMA = [
"prefix" => "?string",
"into" => "?string",
"schema" => "?array",
"cols" => "?array",
"values" => "?array",
"suffix" => "?string",
];
static function isa(string $sql): bool {
return preg_match("/^insert\b/i", $sql);
}
/**
* parser une chaine de la forme
* "insert [into] [TABLE] [(COLS)] [values (VALUES)]"
*/
static function parse(array $query, ?array &$params=null): string {
# fusionner d'abord toutes les parties séquentielles
$usersql = $tmpsql = self::merge_seq($query);
### vérifier la présence des parties nécessaires
$sql = [];
if (($prefix = $query["prefix"] ?? null) !== null) $sql[] = $prefix;
## insert
self::consume('insert\s*', $tmpsql);
$sql[] = "insert";
## into
self::consume('into\s*', $tmpsql);
$sql[] = "into";
$into = $query["into"] ?? null;
if (self::consume('([a-z_][a-z0-9_]*)\s*', $tmpsql, $ms)) {
if ($into === null) $into = $ms[1];
$sql[] = $into;
} elseif ($into !== null) {
$sql[] = $into;
} else {
throw new ValueException("expected table name: $usersql");
}
## cols & values
$usercols = [];
$uservalues = [];
if (self::consume('\(([^)]*)\)\s*', $tmpsql, $ms)) {
$usercols = array_merge($usercols, preg_split("/\s*,\s*/", $ms[1]));
}
$cols = cl::withn($query["cols"] ?? null);
$values = cl::withn($query["values"] ?? null);
$schema = $query["schema"] ?? null;
if ($cols === null) {
if ($usercols) {
$cols = $usercols;
} elseif ($values) {
$cols = array_keys($values);
$usercols = array_merge($usercols, $cols);
} elseif ($schema && is_array($schema)) {
#XXX implémenter support AssocSchema
$cols = array_keys($schema);
$usercols = array_merge($usercols, $cols);
}
}
if (self::consume('values\s+\(\s*(.*)\s*\)\s*', $tmpsql, $ms)) {
if ($ms[1]) $uservalues[] = $ms[1];
}
if ($cols !== null && !$uservalues) {
if (!$usercols) $usercols = $cols;
foreach ($cols as $col) {
$uservalues[] = ":$col";
$params[$col] = $values[$col] ?? null;
}
}
$sql[] = "(" . implode(", ", $usercols) . ")";
$sql[] = "values (" . implode(", ", $uservalues) . ")";
## suffixe
if (($suffix = $query["suffix"] ?? null) !== null) $sql[] = $suffix;
## fin de la requête
self::check_eof($tmpsql, $usersql);
return implode(" ", $sql);
}
}

View File

@ -0,0 +1,169 @@
<?php
namespace nulib\db\sqlite;
use nulib\cl;
use nulib\ValueException;
class _query_select extends _query {
const SCHEMA = [
"prefix" => "?string",
"schema" => "?array",
"cols" => "?array",
"from" => "?string",
"where" => "?array",
"order by" => "?array",
"group by" => "?array",
"having" => "?array",
"suffix" => "?string",
];
static function isa(string $sql): bool {
return preg_match("/^select\b/i", $sql);
}
/**
* parser une chaine de la forme
* "select [COLS] [from TABLE] [where CONDS] [order by ORDERS] [group by GROUPS] [having CONDS]"
*/
static function parse(array $query, ?array &$params=null): string {
# fusionner d'abord toutes les parties séquentielles
$usersql = $tmpsql = self::merge_seq($query);
### vérifier la présence des parties nécessaires
$sql = [];
## préfixe
if (($prefix = $query["prefix"] ?? null) !== null) $sql[] = $prefix;
## select
self::consume('select\s*', $tmpsql);
$sql[] = "select";
## cols
$usercols = [];
if (self::consume('(.*?)\s*(?=$|\bfrom\b)', $tmpsql, $ms)) {
if ($ms[1]) $usercols[] = $ms[1];
}
$tmpcols = cl::withn($query["cols"] ?? null);
$schema = $query["schema"] ?? null;
if ($tmpcols !== null) {
$cols = [];
$index = 0;
foreach ($tmpcols as $key => $col) {
if ($key === $index) {
$index++;
$cols[] = $col;
$usercols[] = $col;
} else {
$cols[] = $key;
$usercols[] = "$col as $key";
}
}
} else {
$cols = null;
if ($schema && is_array($schema) && !in_array("*", $usercols)) {
$cols = array_keys($schema);
$usercols = array_merge($usercols, $cols);
}
}
if (!$usercols && !$cols) $usercols = ["*"];
$sql[] = implode(" ", $usercols);
## from
$from = $query["from"] ?? null;
if (self::consume('from\s+([a-z_][a-z0-9_]*)\s*(?=;?\s*$|\bwhere\b)', $tmpsql, $ms)) {
if ($from === null) $from = $ms[1];
$sql[] = "from";
$sql[] = $from;
} elseif ($from !== null) {
$sql[] = "from";
$sql[] = $from;
} else {
throw new ValueException("expected table name: $usersql");
}
## where
$userwhere = [];
if (self::consume('where\b\s*(.*?)(?=;?\s*$|\border\s+by\b)', $tmpsql, $ms)) {
if ($ms[1]) $userwhere[] = $ms[1];
}
$where = cl::withn($query["where"] ?? null);
if ($where !== null) self::parse_conds($where, $userwhere, $params);
if ($userwhere) {
$sql[] = "where";
$sql[] = implode(" and ", $userwhere);
}
## order by
$userorderby = [];
if (self::consume('order\s+by\b\s*(.*?)(?=;?\s*$|\bgroup\s+by\b)', $tmpsql, $ms)) {
if ($ms[1]) $userorderby[] = $ms[1];
}
$orderby = cl::withn($query["order by"] ?? null);
if ($orderby !== null) {
$index = 0;
foreach ($orderby as $key => $value) {
if ($key === $index) {
$userorderby[] = $value;
$index++;
} else {
if ($value === null) $value = false;
if (!is_bool($value)) {
$userorderby[] = "$key $value";
} elseif ($value) {
$userorderby[] = $key;
}
}
}
}
if ($userorderby) {
$sql[] = "order by";
$sql[] = implode(", ", $userorderby);
}
## group by
$usergroupby = [];
if (self::consume('group\s+by\b\s*(.*?)(?=;?\s*$|\bhaving\b)', $tmpsql, $ms)) {
if ($ms[1]) $usergroupby[] = $ms[1];
}
$groupby = cl::withn($query["group by"] ?? null);
if ($groupby !== null) {
$index = 0;
foreach ($groupby as $key => $value) {
if ($key === $index) {
$usergroupby[] = $value;
$index++;
} else {
if ($value === null) $value = false;
if (!is_bool($value)) {
$usergroupby[] = "$key $value";
} elseif ($value) {
$usergroupby[] = $key;
}
}
}
}
if ($usergroupby) {
$sql[] = "group by";
$sql[] = implode(", ", $usergroupby);
}
## having
$userhaving = [];
if (self::consume('having\b\s*(.*?)(?=;?\s*$)', $tmpsql, $ms)) {
if ($ms[1]) $userhaving[] = $ms[1];
}
$having = cl::withn($query["having"] ?? null);
if ($having !== null) self::parse_conds($having, $userhaving, $params);
if ($userhaving) {
$sql[] = "having";
$sql[] = implode(" and ", $userhaving);
}
## suffixe
if (($suffix = $query["suffix"] ?? null) !== null) $sql[] = $suffix;
## fin de la requête
self::check_eof($tmpsql, $usersql);
return implode(" ", $sql);
}
}

View File

@ -0,0 +1,50 @@
<?php
namespace nulib\db\sqlite;
class _query_update extends _query {
const SCHEMA = [
"prefix" => "?string",
"table" => "?string",
"schema" => "?array",
"cols" => "?array",
"values" => "?array",
"where" => "?array",
"suffix" => "?string",
];
static function isa(string $sql): bool {
return preg_match("/^update\b/i", $sql);
}
static function parse(array $query, ?array &$params=null): string {
#XXX implémentation minimale
$sql = [self::merge_seq($query)];
## préfixe
if (($prefix = $query["prefix"] ?? null) !== null) $sql[] = $prefix;
## table
$sql[] = $query["table"];
## set
_query::parse_set_values($query["values"], $setsql, $params);
$sql[] = "set";
$sql[] = implode(", ", $setsql);
## where
$where = $query["where"] ?? null;
if ($where !== null) {
_query::parse_conds($where, $wheresql, $params);
if ($wheresql) {
$sql[] = "where";
$sql[] = implode(" and ", $wheresql);
}
}
## suffixe
if (($suffix = $query["suffix"] ?? null) !== null) $sql[] = $suffix;
## fin de la requête
return implode(" ", $sql);
}
}

View File

@ -1,12 +1,12 @@
<?php
namespace nulib;
use nulib\file\base\FileReader;
use nulib\file\base\FileWriter;
use nulib\file\base\MemoryStream;
use nulib\file\base\SharedFile;
use nulib\file\base\TempStream;
use nulib\file\base\TmpfileWriter;
use nulib\file\FileReader;
use nulib\file\FileWriter;
use nulib\file\MemoryStream;
use nulib\file\SharedFile;
use nulib\file\TempStream;
use nulib\file\TmpfileWriter;
/**
* Class file: outils pour gérer les fichiers

View File

@ -1,5 +1,5 @@
<?php
namespace nulib\file\base;
namespace nulib\file;
/**
* Class FileReader: un fichier accédé en lecture

View File

@ -1,5 +1,5 @@
<?php
namespace nulib\file\base;
namespace nulib\file;
use nulib\os\IOException;
use nulib\os\sh;

View File

@ -37,7 +37,7 @@ interface IReader extends _IFile {
* verrouiller en mode partagé puis retourner un objet permettant de lire le
* fichier.
*/
function getReader(bool $lockedByCanRead=false): IReader;
function getReader(bool $alreadyLocked=false): IReader;
/**
* lire tout le contenu du fichier en une seule fois, puis, si $close==true,
@ -45,8 +45,8 @@ interface IReader extends _IFile {
*
* @throws IOException si une erreur se produit
*/
function getContents(bool $close=true, bool $lockedByCanRead=false): string;
function getContents(bool $close=true, bool $alreadyLocked=false): string;
/** désérialiser le contenu du fichier, puis, si $close===true, le fermer */
function unserialize(?array $options=null, bool $close=true, bool $lockedByCanRead=false);
function unserialize(?array $options=null, bool $close=true, bool $alreadyLocked=false);
}

View File

@ -30,11 +30,11 @@ interface IWriter extends _IFile {
* verrouiller en mode exclusif puis retourner un objet permettant d'écrire
* dans le fichier
*/
function getWriter(bool $lockedByCanWrite=false): IWriter;
function getWriter(bool $alreadyLocked=false): IWriter;
/** écrire le contenu spécifié dans le fichier */
function putContents(string $contents, bool $close=true, bool $lockedByCanWrite=false): void;
function putContents(string $contents, bool $close=true, bool $alreadyLocked=false): void;
/** sérialiser l'objet dans la destination */
function serialize($object, bool $close=true, bool $lockedByCanWrite=false): void;
function serialize($object, bool $close=true, bool $alreadyLocked=false): void;
}

View File

@ -1,5 +1,5 @@
<?php
namespace nulib\file\base;
namespace nulib\file;
/**
* Class MemoryStream: un flux qui peut être lu ou écrit, et qui reste

View File

@ -1,5 +1,5 @@
<?php
namespace nulib\file\base;
namespace nulib\file;
use nulib\ValueException;

View File

@ -1,14 +1,12 @@
<?php
namespace nulib\file\base;
namespace nulib\file;
use nulib\file\csv\csv_flavours;
use nulib\file\IReader;
use nulib\file\IWriter;
use nulib\NoMoreDataException;
use nulib\os\EOFException;
use nulib\os\IOException;
use nulib\php\iter\AbstractIterator;
use nulib\ref\os\csv\ref_csv;
use nulib\ref\file\csv\ref_csv;
use nulib\str;
use nulib\ValueException;
@ -234,6 +232,14 @@ class Stream extends AbstractIterator implements IReader, IWriter {
return iterator_to_array($this);
}
/**
* verrouiller le fichier en lecture de façon inconditionelle (ignorer la
* valeur de $useLocking). bloquer jusqu'à ce que le verrou soit disponible
*/
function lockRead(): void {
$this->lock(LOCK_SH);
}
/**
* essayer de verrouiller le fichier en lecture. retourner true si l'opération
* réussit. dans ce cas, il faut appeler {@link getReader()} avec l'argument
@ -248,8 +254,8 @@ class Stream extends AbstractIterator implements IReader, IWriter {
* verrouiller en mode partagé puis retourner un objet permettant de lire le
* fichier.
*/
function getReader(bool $lockedByCanRead=false): IReader {
if ($this->useLocking && !$lockedByCanRead) $this->lock(LOCK_SH);
function getReader(bool $alreadyLocked=false): IReader {
if ($this->useLocking && !$alreadyLocked) $this->lock(LOCK_SH);
return new class($this->fd, ++$this->serial, $this) extends Stream {
function __construct($fd, int $serial, Stream $parent) {
$this->parent = $parent;
@ -270,9 +276,9 @@ class Stream extends AbstractIterator implements IReader, IWriter {
}
/** retourner le contenu du fichier sous forme de chaine */
function getContents(bool $close=true, bool $lockedByCanRead=false): string {
function getContents(bool $close=true, bool $alreadyLocked=false): string {
$useLocking = $this->useLocking;
if ($useLocking && !$lockedByCanRead) $this->lock(LOCK_SH);
if ($useLocking && !$alreadyLocked) $this->lock(LOCK_SH);
try {
return IOException::ensure_valid(stream_get_contents($this->fd), $this->throwOnError);
} finally {
@ -281,8 +287,8 @@ class Stream extends AbstractIterator implements IReader, IWriter {
}
}
function unserialize(?array $options=null, bool $close=true, bool $lockedByCanRead=false) {
$args = [$this->getContents($lockedByCanRead)];
function unserialize(?array $options=null, bool $close=true, bool $alreadyLocked=false) {
$args = [$this->getContents($close, $alreadyLocked)];
if ($options !== null) $args[] = $options;
return unserialize(...$args);
}
@ -331,9 +337,10 @@ class Stream extends AbstractIterator implements IReader, IWriter {
}
/** @throws IOException */
function ftruncate(int $size): self {
function ftruncate(int $size=0, bool $rewind=true): self {
$fd = $this->getResource();
IOException::ensure_valid(ftruncate($fd, $size), $this->throwOnError);
if ($rewind) rewind($fd);
return $this;
}
@ -347,6 +354,14 @@ class Stream extends AbstractIterator implements IReader, IWriter {
return $this;
}
/**
* verrouiller le fichier en écriture de façon inconditionelle (ignorer la
* valeur de $useLocking). bloquer jusqu'à ce que le verrou soit disponible
*/
function lockWrite(): void {
$this->lock(LOCK_EX);
}
/**
* essayer de verrouiller le fichier en écriture. retourner true si l'opération
* réussit. dans ce cas, il faut appeler {@link getWriter()} avec l'argument
@ -361,8 +376,8 @@ class Stream extends AbstractIterator implements IReader, IWriter {
* verrouiller en mode exclusif puis retourner un objet permettant d'écrire
* dans le fichier
*/
function getWriter(bool $lockedByCanWrite=false): IWriter {
if ($this->useLocking && !$lockedByCanWrite) $this->lock(LOCK_EX);
function getWriter(bool $alreadyLocked=false): IWriter {
if ($this->useLocking && !$alreadyLocked) $this->lock(LOCK_EX);
return new class($this->fd, ++$this->serial, $this) extends Stream {
function __construct($fd, int $serial, Stream $parent) {
$this->parent = $parent;
@ -383,9 +398,9 @@ class Stream extends AbstractIterator implements IReader, IWriter {
};
}
function putContents(string $contents, bool $close=true, bool $lockedByCanWrite=false): void {
function putContents(string $contents, bool $close=true, bool $alreadyLocked=false): void {
$useLocking = $this->useLocking;
if ($useLocking && !$lockedByCanWrite) $this->lock(LOCK_EX);
if ($useLocking && !$alreadyLocked) $this->lock(LOCK_EX);
try {
$this->fwrite($contents);
} finally {
@ -394,7 +409,7 @@ class Stream extends AbstractIterator implements IReader, IWriter {
}
}
function serialize($object, bool $close=true, bool $lockedByCanWrite=false): void {
$this->putContents(serialize($object), $lockedByCanWrite);
function serialize($object, bool $close=true, bool $alreadyLocked=false): void {
$this->putContents(serialize($object), $close, $alreadyLocked);
}
}

View File

@ -1,7 +1,6 @@
<?php
namespace nulib\file\base;
namespace nulib\file;
use nulib\file\_IFile;
use nulib\os\IOException;
trait TStreamFilter {

View File

@ -1,5 +1,5 @@
<?php
namespace nulib\file\base;
namespace nulib\file;
/**
* Class TempStream: un flux qui peut être lu ou écrit, et qui reste en mémoire,

View File

@ -1,5 +1,5 @@
<?php
namespace nulib\file\base;
namespace nulib\file;
use nulib\os\IOException;
use nulib\os\path;

View File

@ -1,5 +1,5 @@
<?php
namespace nulib\file\base;
namespace nulib\file;
use nulib\os\IOException;
use nulib\web\http;
@ -12,9 +12,17 @@ abstract class _File extends Stream {
/** @var string */
protected $file;
function getFile(): ?string {
return $this->file;
}
/** @var string */
protected $mode;
function getMode(): string {
return $this->mode;
}
/** @return resource */
protected function open() {
return IOException::ensure_valid(@fopen($this->file, $this->mode));

View File

@ -0,0 +1,87 @@
<?php
namespace nulib\file\app;
use nulib\cl;
use nulib\file\SharedFile;
use nulib\output\msg;
use nulib\php\time\DateTime;
/**
* Class LockFile: une classe qui permet à une application de verrouiller
* certaines actions
*/
class LockFile {
const NAME = null;
const TITLE = null;
function __construct($file, ?string $name=null, ?string $title=null) {
$this->file = new SharedFile($file);
$this->name = $name ?? static::NAME;
$this->title = $title ?? static::TITLE;
}
/** @var SharedFile */
protected $file;
/** @var ?string */
protected $name;
/** @var ?string */
protected $title;
protected function initData(): array {
return [
"name" => $this->name,
"title" => $this->title,
"locked" => false,
"date_lock" => null,
"date_release" => null,
];
}
function read(bool $close=true): array {
$data = $this->file->unserialize(null, $close);
if (!is_array($data)) $data = $this->initData();
return $data;
}
function isLocked(?array &$data=null): bool {
$data = $this->read();
return $data["locked"];
}
function warnIfLocked(?array $data=null): void {
if ($data === null) $data = $this->read();
if ($data["locked"]) {
msg::warning("$data[name]: possède le verrou depuis $data[date_lock] -- $data[title]");
}
}
function lock(?array &$data=null): bool {
$file = $this->file;
$data = $this->read(false);
if ($data["locked"]) {
$file->close();
return false;
} else {
$file->ftruncate();
$file->serialize(cl::merge($data, [
"locked" => true,
"date_lock" => new DateTime(),
"date_release" => null,
]));
return true;
}
}
function release(?array &$data=null): void {
$file = $this->file;
$data = $this->read(false);
$file->ftruncate();
$file->serialize(cl::merge($data, [
"locked" => false,
"date_release" => new DateTime(),
]));
}
}

View File

@ -0,0 +1,129 @@
<?php
namespace nulib\file\app;
use nulib\cl;
use nulib\file\SharedFile;
use nulib\os\path;
use nulib\php\time\DateTime;
use nulib\str;
/**
* Class RunFile: une classe permettant de suivre le fonctionnement d'une
* application qui tourne en tâche de fond
*/
class RunFile {
const RUN_EXT = ".run";
const LOCK_EXT = ".lock";
const NAME = null;
function __construct(string $file, ?string $name=null) {
$file = path::ensure_ext($file, self::RUN_EXT);
$this->file = new SharedFile($file);
$this->name = $name ?? static::NAME;
}
/** @var SharedFile */
protected $file;
/** @var ?string */
protected $name;
protected static function merge(array $data, array $merge): array {
return cl::merge($data, [
"serial" => $data["serial"] + 1,
], $merge);
}
protected function initData(bool $withDateStart=true): array {
$dateStart = $withDateStart? new DateTime(): null;
return [
"name" => $this->name,
"serial" => 0,
"date_start" => $dateStart,
"date_stop" => null,
"action" => null,
"action_date_start" => null,
"action_max_step" => null,
"action_current_step" => null,
"action_date_step" => null,
];
}
function read(): array {
$data = $this->file->unserialize();
if (!is_array($data)) $data = $this->initData(false);
return $data;
}
/** tester si l'application est démarrée */
function isStarted(): bool {
$data = $this->read();
return $data["date_start"] !== null;
}
/** tester si l'application est arrêtée */
function isStopped(): bool {
$data = $this->read();
return $data["date_stop"] !== null;
}
function haveWorked(int $serial, ?int &$currentSerial=null): bool {
$data = $this->read();
return $serial !== $data["serial"];
}
protected function willWrite(): array {
$file = $this->file;
$file->lockWrite();
$data = $file->unserialize(null, false, true);
if (!is_array($data)) {
$data = $this->initData();
$file->ftruncate();
$file->serialize($data, false, true);
}
$file->ftruncate();
return [$file, $data];
}
/** indiquer que l'application démarre */
function start(): void {
$this->file->serialize($this->initData());
}
/** indiquer le début d'une action */
function action(?string $title, ?int $maxSteps=null): void {
[$file, $data] = $this->willWrite();
$file->serialize(self::merge($data, [
"action" => $title,
"action_date_start" => new DateTime(),
"action_max_step" => $maxSteps,
"action_current_step" => 0,
]));
}
/** indiquer qu'une étape est franchie dans l'action en cours */
function step(int $nbSteps=1): void {
[$file, $data] = $this->willWrite();
$file->serialize(self::merge($data, [
"action_date_step" => new DateTime(),
"action_current_step" => $data["action_current_step"] + $nbSteps,
]));
}
/** indiquer que l'application s'arrête */
function stop(): void {
[$file, $data] = $this->willWrite();
$file->serialize(self::merge($data, [
"date_stop" => new DateTime(),
]));
}
function getLockFile(?string $name=null, ?string $title=null): LockFile {
$ext = self::LOCK_EXT;
if ($name !== null) $ext = ".$name$ext";
$file = path::ensure_ext($this->file->getFile(), $ext, self::RUN_EXT);
$name = str::join("/", [$this->name, $name]);
return new LockFile($file, $name, $title);
}
}

View File

@ -2,7 +2,7 @@
namespace nulib\file\csv;
use nulib\cl;
use nulib\ref\os\csv\ref_csv;
use nulib\ref\file\csv\ref_csv;
class csv_flavours {
const MAP = [

104
php/src/file/web/Upload.php Normal file
View File

@ -0,0 +1,104 @@
<?php
namespace nulib\file\web;
use nulib\file\FileReader;
use nulib\php\coll\BaseArray;
use nulib\ValueException;
/**
* Class Upload: un fichier téléversé
*
* @property-read string $name
* @property-read string $type
* @property-read string $tmpName
* @property-read int $error
* @property-read int $size
* @property-read string $fullPath
*/
class Upload extends BaseArray {
const MESSAGES = [
"invalid" => "Ceci n'est pas un fichier téléversé",
"nofile" => "Aucun fichier n'a été fourni",
"toobig" => "Le fichier que vous avez fourni est trop volumineux.",
"unknown" => "Une erreur s'est produite pendant le transfert du fichier. Veuillez réessayer.",
];
protected static function error(string $message) {
return new ValueException(static::MESSAGES[$message]);
}
function __construct(?array $file, bool $required=true, bool $check=true) {
parent::__construct($file);
if ($check) $this->check($required);
}
function check(bool $required=true, bool $throw=true): bool {
$file = $this->data;
if ($file) {
$name = $file["name"] ?? null;
$type = $file["type"] ?? null;
$error = $file["error"] ?? null;
if (!is_scalar($name) || !is_scalar($type) || !is_scalar($error)) {
if ($throw) throw static::error("invalid");
else return false;
}
switch ($error) {
case UPLOAD_ERR_OK:
break;
case UPLOAD_ERR_NO_FILE:
if ($required) {
if ($throw) throw self::error("nofile");
else return false;
}
break;
case UPLOAD_ERR_INI_SIZE:
case UPLOAD_ERR_FORM_SIZE:
if ($throw) throw self::error("toobig");
else return false;
default:
if ($throw) self::error("unknown");
else return false;
}
} elseif ($required) {
if ($throw) throw static::error("nofile");
else return false;
}
return true;
}
const _AUTO_PROPERTIES = [
"tmpName" => "tmp_name",
"fullPath" => "full_path",
];
function &__get($name) {
$name = static::_AUTO_PROPERTIES[$name] ?? $name;
return parent::__get($name);
}
function isError(): bool {
$error = $this->error;
return $error !== UPLOAD_ERR_OK && $error !== UPLOAD_ERR_NO_FILE;
}
function isValid(): bool {
return $this->error === UPLOAD_ERR_OK;
}
/** @var ?string chemin du fichier, s'il a été déplacé */
protected $file;
function moveTo(string $dest): bool {
if ($this->file === null) {
$moved = move_uploaded_file($this->tmpName, $dest);
if ($moved) $this->file = $dest;
} else {
$moved = false;
}
return $moved;
}
function getFile(): FileReader {
$file = $this->file ?? $this->tmpName;
return new FileReader($file, "r+b");
}
}

View File

@ -1,10 +0,0 @@
<?php
namespace nulib\output;
/**
* Interface IContent: un objet capable de produire du contenu à afficher
*/
interface IContent {
/** retourner le contenu à afficher */
function getContent(): iterable;
}

View File

@ -5,7 +5,7 @@ namespace nulib\output;
* Interface IMessenger: un objet pouvant afficher des messages de l'application
*/
interface IMessenger {
const DEBUG = -1, MINOR = 0, NORMAL = 1, MAJOR = 2, NONE = 3;
const DEBUG = 0, MINOR = 1, NORMAL = 2, MAJOR = 3, NONE = 4;
const MIN_LEVEL = self::DEBUG, MAX_LEVEL = self::MAJOR;
/** réinitialiser les paramètres de l'objet */
@ -54,21 +54,32 @@ interface IMessenger {
*
* démarrer une action le cas échéant (et la terminer aussitôt)
*/
function asuccess($content=null): void;
function asuccess($content=null, ?int $overrideLevel=null): void;
/**
* terminer l'action courante avec le résultat "échec"
*
* démarrer une action le cas échéant (et la terminer aussitôt)
*/
function afailure($content=null): void;
function afailure($content=null, ?int $overrideLevel=null): void;
/**
* terminer l'action courante avec le résultat "neutre"
*
* démarrer une action le cas échéant (et la terminer aussitôt)
*/
function adone($content=null): void;
function adone($content=null, ?int $overrideLevel=null): void;
/**
* terminer l'action courante avec le résultat "succès", "échec" ou "neutre"
* en fonction de la valeur de $result
* - si c'est un booléen, true vaut succès, false vaut échec
* - si c'est une exception, c'est un échec et le message est affiché
* - sinon, le résultat est neutre et le message est affiché s'il n'est pas null
*
* démarrer une action le cas échéant (et la terminer aussitôt)
*/
function aresult($result=null, ?int $overrideLevel=null): void;
/** afficher une donnée non structurée */
function print($content, ?int $level=null): void;
@ -80,7 +91,7 @@ interface IMessenger {
function note($content, ?int $level=null): void;
/** afficher un événément "avertissement" */
function warn($content, ?int $level=null): void;
function warning($content, ?int $level=null): void;
/** afficher un événément "erreur" */
function error($content, ?int $level=null): void;

View File

@ -1,11 +0,0 @@
<?php
namespace nulib\output;
/**
* Interface IPrintable: un objet qui peut écrire du contenu sur la sortie
* standard
*/
interface IPrintable {
/** afficher le contenu */
function print(): void;
}

View File

@ -4,8 +4,28 @@
rotation des logs
* [ ] lors de la rotation, si l'ouverture du nouveau fichier échoue, continuer
à écrire dans l'ancien fichier
* ou alors un moyen pour ré-ouvrir la sortie, afin de pouvoir indiquer à un
long running process qu'une rotation a eu lieu
* [ ] dans `StdMessenger::resetParams()`, `[output]` peut être une instance de
StdOutput pour mettre à jour $out ET $err, ou un tableau de deux éléments pour
mettre à jour séparément $out et $err
* [ ] vérifier que la date affichée pour un TITLE est celle à laquelle l'appel
a été fait, même si le premier événement en dessous arrive bien plus tard
* [ ] pareil pour action: sauf si c'est une seule ligne, la date de action est
la date du premier appel, alors que la date de $result est celui du result si
c'est affiché sur une autre ligne
* réorganiser pour que msg:: attaque un proxy dans lequel est configuré un
ensemble standard de sorties: say, log, debuglog
* `--aD, --av, --aq, --asilent` pour les valeurs d'ajustement qui sont un
incrément à la valeur courante (+2, +1, -1, -2)
* `--yD, --yv, --yq, --ysilent, -D, -v, -q, --silent` pour say
* `--lD, --lv, --lq, --lsilent` pour log, `-L:, --L` l'active
* `--DD, --Dv, --Dq, --Dsilent` pour debuglog, `--DL:` l'active
question à régler: trouver un moyen pour que l'affichage web soit "un niveau au
dessus" afin de ne pas se retrouver avec dans les logs des messages uniquement
pour l'UI
peut-être rajouter `ui` (ou `web`?) en plus de say, log, debuglog?
--> ou renommer `say` en `console`, et `ui` en `say`
-*- coding: utf-8 mode: markdown -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8:noeol:binary

View File

@ -2,7 +2,6 @@
namespace nulib\output;
use nulib\str;
use nulib\ValueException;
/**
* Class _messenger: classe de base pour say, log et msg
@ -59,13 +58,14 @@ abstract class _messenger {
static function desc($content, ?int $level=null): void { static::get()->desc($content, $level); }
static function action($content, ?callable $func=null, ?int $level=null): void { static::get()->action($content, $func, $level); }
static function step($content, ?int $level=null): void { static::get()->step($content, $level); }
static function asuccess($content=null): void { static::get()->asuccess($content); }
static function afailure($content=null): void { static::get()->afailure($content); }
static function adone($content=null): void { static::get()->adone($content); }
static function asuccess($content=null, ?int $override_level=null): void { static::get()->asuccess($content, $override_level); }
static function afailure($content=null, ?int $override_level=null): void { static::get()->afailure($content, $override_level); }
static function adone($content=null, ?int $override_level=null): void { static::get()->adone($content, $override_level); }
static function aresult($result=null, ?int $override_level=null): void { static::get()->aresult($result, $override_level); }
static function print($content, ?int $level=null): void { static::get()->print($content, $level); }
static function info($content, ?int $level=null): void { static::get()->info($content, $level); }
static function note($content, ?int $level=null): void { static::get()->note($content, $level); }
static function warn($content, ?int $level=null): void { static::get()->warn($content, $level); }
static function warning($content, ?int $level=null): void { static::get()->warning($content, $level); }
static function error($content, ?int $level=null): void { static::get()->error($content, $level); }
static function end(bool $all=false): void { static::get()->end($all); }
@ -74,6 +74,6 @@ abstract class _messenger {
static function minor($content): void { self::info($content, self::MINOR);}
static function important($content): void { self::info($content, self::MAJOR);}
static function attention($content): void { self::note($content, self::MAJOR);}
static function critwarn($content): void { self::warn($content, self::MAJOR);}
static function critwarning($content): void { self::warning($content, self::MAJOR);}
static function criterror($content): void { self::error($content, self::MAJOR);}
}

View File

@ -1,8 +1,9 @@
<?php
namespace nulib\output;
use nulib\ValueException;
use nulib\cl;
use nulib\output\std\ProxyMessenger;
use nulib\ValueException;
/**
* Class log: inscrire un message dans les logs uniquement
@ -13,13 +14,15 @@ use nulib\output\std\ProxyMessenger;
class log extends _messenger {
static function set_messenger(IMessenger $log=null) {
self::$log = $log;
// forcer la recréation de l'instance partagée $msg
self::$msg = null;
}
static function set_messenger_class(string $log_class=null, ?array $params=null) {
if (!is_subclass_of($log_class, IMessenger::class)) {
throw ValueException::invalid_class($log_class, IMessenger::class);
}
self::$log = new $log_class($params);
self::set_messenger(new $log_class($params));
}
static function get(): IMessenger {
@ -30,4 +33,17 @@ class log extends _messenger {
}
return self::$msg;
}
static function have_log(): bool {
return self::$log !== null;
}
static function create_or_reset_params(?array $params=null, string $log_class=null, ?array $create_params=null) {
if (self::$log === null) {
$params = cl::merge($params, $create_params);
self::set_messenger_class($log_class, $params);
} else {
self::reset_params($params);
}
}
}

View File

@ -1,8 +1,8 @@
<?php
namespace nulib\output;
use nulib\ValueException;
use nulib\output\std\ProxyMessenger;
use nulib\ValueException;
/**
* Class msg: inscrire un message dans les logs ET l'afficher sur la console
@ -12,22 +12,32 @@ use nulib\output\std\ProxyMessenger;
* les classes {@link say} et {@link log} sont utilisables aussi
*/
class msg extends _messenger {
static function set_messenger(IMessenger $say, ?IMessenger $log=null) {
self::$say = $say;
static function set_messenger(?IMessenger $say, ?IMessenger $log=null) {
if ($say !== null) self::$say = $say;
if ($log !== null) self::$log = $log;
if ($say !== null || $log !== null) {
// forcer la recréation de l'instance partagée $msg
self::$msg = null;
}
}
static function set_messenger_class(string $say_class, ?string $log_class=null) {
if (!is_subclass_of($say_class, IMessenger::class)) {
throw ValueException::invalid_class($say_class, IMessenger::class);
static function set_messenger_class(?string $say_class, ?string $log_class=null) {
if ($say_class !== null) {
if (!is_subclass_of($say_class, IMessenger::class)) {
throw ValueException::invalid_class($say_class, IMessenger::class);
}
self::$say = new $say_class();
}
self::$say = new $say_class();
if ($log_class !== null) {
if (!is_subclass_of($log_class, IMessenger::class)) {
throw ValueException::invalid_class($log_class, IMessenger::class);
}
self::$log = new $log_class();
}
if ($say_class !== null || $log_class !== null) {
// forcer la recréation de l'instance partagée $msg
self::$msg = null;
}
}
static function get(): IMessenger {

View File

@ -1,8 +1,8 @@
<?php
namespace nulib\output;
use nulib\ValueException;
use nulib\output\std\ProxyMessenger;
use nulib\ValueException;
/**
* Class say: afficher un message sur la console uniquement
@ -13,13 +13,15 @@ use nulib\output\std\ProxyMessenger;
class say extends _messenger {
static function set_messenger(IMessenger $say) {
self::$say = $say;
// forcer la recréation de l'instance partagée $msg
self::$msg = null;
}
static function set_messenger_class(string $say_class, ?array $params=null) {
if (!is_subclass_of($say_class, IMessenger::class)) {
throw ValueException::invalid_class($say_class, IMessenger::class);
}
self::$say = new $say_class($params);
self::set_messenger(new $say_class($params));
}
static function get(): IMessenger {

View File

@ -1,10 +1,15 @@
<?php
namespace nulib\output\std;
use Exception;
use nulib\output\IMessenger;
/**
* Class ProxyMessenger: un proxy vers ou un plusieurs instances de IMessenger
*
* NB: si cette classe est instanciée sans argument, elle agit comme un
* "NullMessenger", c'est à dire une instance qui envoie tous les messages vers
* /dev/null
*/
class ProxyMessenger implements IMessenger {
function __construct(?IMessenger ...$msgs) {
@ -46,11 +51,11 @@ class ProxyMessenger implements IMessenger {
$useFunc = false;
$untils = [];
foreach ($this->msgs as $msg) {
$msg->title($content, null, $level);
if ($msg instanceof _IMessenger) {
$useFunc = true;
$untils[] = $msg->_getTitleMark();
}
$msg->title($content, null, $level);
}
if ($useFunc && $func !== null) {
try {
@ -59,7 +64,9 @@ class ProxyMessenger implements IMessenger {
/** @var _IMessenger $msg */
$index = 0;
foreach ($this->msgs as $msg) {
$msg->_endTitle($untils[$index++]);
if ($msg instanceof _IMessenger) {
$msg->_endTitle($untils[$index++]);
}
}
}
}
@ -72,12 +79,25 @@ class ProxyMessenger implements IMessenger {
$msg->action($content, null, $level);
if ($msg instanceof _IMessenger) {
$useFunc = true;
$untils[] = $msg->_getTitleMark();
$untils[] = $msg->_getActionMark();
}
}
if ($useFunc && $func !== null) {
try {
$func($this);
$result = $func($this);
/** @var _IMessenger $msg */
$index = 0;
foreach ($this->msgs as $msg) {
if ($msg->_getActionMark() > $untils[$index++]) {
$msg->aresult($result);
}
}
} catch (Exception $e) {
/** @var _IMessenger $msg */
foreach ($this->msgs as $msg) {
$msg->afailure($e);
}
throw $e;
} finally {
/** @var _IMessenger $msg */
$index = 0;
@ -88,13 +108,14 @@ class ProxyMessenger implements IMessenger {
}
}
function step($content, ?int $level=null): void { foreach ($this->msgs as $msg) { $msg->step($content, $level); } }
function asuccess($content=null): void { foreach ($this->msgs as $msg) { $msg->asuccess($content); } }
function afailure($content=null): void { foreach ($this->msgs as $msg) { $msg->afailure($content); } }
function adone($content=null): void { foreach ($this->msgs as $msg) { $msg->adone($content); } }
function asuccess($content=null, ?int $overrideLevel=null): void { foreach ($this->msgs as $msg) { $msg->asuccess($content, $overrideLevel); } }
function afailure($content=null, ?int $overrideLevel=null): void { foreach ($this->msgs as $msg) { $msg->afailure($content, $overrideLevel); } }
function adone($content=null, ?int $overrideLevel=null): void { foreach ($this->msgs as $msg) { $msg->adone($content, $overrideLevel); } }
function aresult($result=null, ?int $overrideLevel=null): void { foreach ($this->msgs as $msg) { $msg->aresult($result, $overrideLevel); } }
function print($content, ?int $level=null): void { foreach ($this->msgs as $msg) { $msg->print($content, $level); } }
function info($content, ?int $level=null): void { foreach ($this->msgs as $msg) { $msg->info($content, $level); } }
function note($content, ?int $level=null): void { foreach ($this->msgs as $msg) { $msg->note($content, $level); } }
function warn($content, ?int $level=null): void { foreach ($this->msgs as $msg) { $msg->warn($content, $level); } }
function warning($content, ?int $level=null): void { foreach ($this->msgs as $msg) { $msg->warning($content, $level); } }
function error($content, ?int $level=null): void { foreach ($this->msgs as $msg) { $msg->error($content, $level); } }
function end(bool $all=false): void { foreach ($this->msgs as $msg) { $msg->end($all); } }
}

View File

@ -2,10 +2,11 @@
namespace nulib\output\std;
use Exception;
use nulib\A;
use nulib\cl;
use nulib\ExceptionShadow;
use nulib\UserException;
use nulib\output\IMessenger;
use nulib\UserException;
use Throwable;
class StdMessenger implements _IMessenger {
@ -40,7 +41,7 @@ class StdMessenger implements _IMessenger {
"title" => [false, "TITLE!", null, "<color @b>T", "</color>", "==="],
"desc" => ["DESC!", "<color @b>></color>", ""],
"error" => ["CRIT.ERROR!", "<color @r>E!", "</color>"],
"warn" => ["CRIT.WARN!", "<color @y>W!", "</color>"],
"warning" => ["CRIT.WARNING!", "<color @y>W!", "</color>"],
"note" => ["ATTENTION!", "<color @g>N!", "</color>"],
"info" => ["IMPORTANT!", "<color @b>N!", "</color>"],
"step" => ["*", "<color @w>.</color>", ""],
@ -51,7 +52,7 @@ class StdMessenger implements _IMessenger {
"title" => [false, "TITLE:", null, "<color @b>T</color><color b>", "</color>", "---"],
"desc" => ["DESC:", "<color @b>></color>", ""],
"error" => ["ERROR:", "<color @r>E</color><color r>", "</color>"],
"warn" => ["WARN:", "<color @y>W</color><color y>", "</color>"],
"warning" => ["WARNING:", "<color @y>W</color><color y>", "</color>"],
"note" => ["NOTE:", "<color @g>N</color>", ""],
"info" => ["INFO:", "<color @b>I</color>", ""],
"step" => ["*", "<color @w>.</color>", ""],
@ -62,7 +63,7 @@ class StdMessenger implements _IMessenger {
"title" => [false, "title", null, "<color b>t", "</color>", null],
"desc" => ["desc", "<color b>></color>", ""],
"error" => ["error", "<color r>E</color><color -r>", "</color>"],
"warn" => ["warn", "<color y>W</color><color -y>", "</color>"],
"warning" => ["warning", "<color y>W</color><color -y>", "</color>"],
"note" => ["note", "<color g>N</color>", ""],
"info" => ["info", "<color b>I</color><color w>", "</color>"],
"step" => ["*", "<color w>.</color>", ""],
@ -73,7 +74,7 @@ class StdMessenger implements _IMessenger {
"title" => [false, "title", null, "<color b>t", "</color>", null],
"desc" => ["desc", "<color b>></color>", ""],
"error" => ["debugE", "<color r>e</color><color -r>", "</color>"],
"warn" => ["debugW", "<color y>w</color><color -y>", "</color>"],
"warning" => ["debugW", "<color y>w</color><color -y>", "</color>"],
"note" => ["debugN", "<color b>i</color>", ""],
"info" => ["debug", "<color @w>D</color><color w>", "</color>"],
"step" => ["*", "<color w>.</color>", ""],
@ -306,7 +307,7 @@ class StdMessenger implements _IMessenger {
}
if ($printContent && $printResult) {
if ($rcontent) {
cl::ensure_array($content);
A::ensure_array($content);
$content[] = ": ";
$content[] = $rcontent;
}
@ -579,18 +580,19 @@ class StdMessenger implements _IMessenger {
if ($func !== null) {
try {
$result = $func($this);
if ($result !== null) {
if ($result === true) $this->asuccess();
elseif ($result === false) $this->afailure();
else $this->adone($result);
if ($this->_getActionMark() > $until) {
$this->aresult($result);
}
} catch (Exception $e) {
$this->afailure($e);
throw $e;
} finally {
$this->_endAction($until);
}
}
}
function printActions(bool $endAction=false): void {
function printActions(bool $endAction=false, ?int $overrideLevel=null): void {
$this->printTitles();
$err = $this->err;
$indentLevel = $this->getIndentLevel(false);
@ -599,7 +601,7 @@ class StdMessenger implements _IMessenger {
foreach ($this->actions as &$action) {
$mergeResult = $index++ == $lastIndex && $endAction;
$linePrefix = $action["line_prefix"];
$level = $action["level"];
$level = $overrideLevel?? $action["level"];
$content = $action["content"];
$printContent = $action["print_content"];
$rsuccess = $action["result_success"];
@ -628,25 +630,33 @@ class StdMessenger implements _IMessenger {
$this->_printGenericOrException($level, "step", $content, $this->getIndentLevel(), $this->err);
}
function asuccess($content=null): void {
function asuccess($content=null, ?int $overrideLevel=null): void {
if (!$this->actions) $this->action(null);
$this->action["result_success"] = true;
$this->action["result_content"] = $content;
$this->printActions(true);
$this->printActions(true, $overrideLevel);
}
function afailure($content=null): void {
function afailure($content=null, ?int $overrideLevel=null): void {
if (!$this->actions) $this->action(null);
$this->action["result_success"] = false;
$this->action["result_content"] = $content;
$this->printActions(true);
$this->printActions(true, $overrideLevel);
}
function adone($content=null): void {
function adone($content=null, ?int $overrideLevel=null): void {
if (!$this->actions) $this->action(null);
$this->action["result_success"] = null;
$this->action["result_content"] = $content;
$this->printActions(true);
$this->printActions(true, $overrideLevel);
}
function aresult($result=null, ?int $overrideLevel=null): void {
if (!$this->actions) $this->action(null);
if ($result === true) $this->asuccess(null, $overrideLevel);
elseif ($result === false) $this->afailure(null, $overrideLevel);
elseif ($result instanceof Exception) $this->afailure($result, $overrideLevel);
else $this->adone($result, $overrideLevel);
}
function _endAction(?int $until=null): void {
@ -674,8 +684,8 @@ class StdMessenger implements _IMessenger {
$this->_printGenericOrException($level, "note", $content, $this->getIndentLevel(), $this->err);
}
function warn($content, ?int $level=null): void {
$this->_printGenericOrException($level, "warn", $content, $this->getIndentLevel(), $this->err);
function warning($content, ?int $level=null): void {
$this->_printGenericOrException($level, "warning", $content, $this->getIndentLevel(), $this->err);
}
function error($content, ?int $level=null): void {

View File

@ -3,10 +3,8 @@ namespace nulib\output\std;
use Exception;
use nulib\cl;
use nulib\os\file\Stream;
use nulib\file\Stream;
use nulib\php\content\content;
use nulib\php\content\IContent;
use nulib\php\content\IPrintable;
/**
* Class StdOutput: affichage sur STDOUT, STDERR ou dans un fichier quelconque

View File

@ -1,7 +1,8 @@
<?php
namespace nulib\values;
namespace nulib\php;
use ArrayAccess;
use nulib\A;
use nulib\cl;
/**
@ -46,7 +47,7 @@ class akey {
$array->offsetSet($key, ++$value);
return $value;
} else {
cl::ensure_array($array);
A::ensure_array($array);
$value = (int)cl::get($array, $key);
return $array[$key] = ++$value;
}
@ -59,7 +60,7 @@ class akey {
if ($allow_negative || $value > 0) $array->offsetSet($key, --$value);
return $value;
} else {
cl::ensure_array($array);
A::ensure_array($array);
$value = (int)cl::get($array, $key);
if ($allow_negative || $value > 0) $array[$key] = --$value;
return $value;
@ -76,7 +77,7 @@ class akey {
$value = cl::merge($value, $merge);
$array->offsetSet($key, $value);
} else {
cl::ensure_array($array);
A::ensure_array($array);
$array[$key] = cl::merge($array[$key], $merge);
}
}
@ -91,7 +92,7 @@ class akey {
cl::set($value, null, $value);
$array->offsetSet($key, $value);
} else {
cl::ensure_array($array);
A::ensure_array($array);
cl::set($array[$key], null, $value);
}
}

View File

@ -5,12 +5,13 @@ use ArrayAccess;
use Countable;
use Iterator;
use nulib\cl;
use nulib\IArrayWrapper;
/**
* Class BaseArray: implémentation de base d'un objet array-like, qui peut aussi
* servir comme front-end pour un array
*/
class BaseArray implements ArrayAccess, Countable, Iterator {
class BaseArray implements ArrayAccess, Countable, Iterator, IArrayWrapper {
function __construct(?array &$data=null) {
$this->reset($data);
}
@ -18,10 +19,11 @@ class BaseArray implements ArrayAccess, Countable, Iterator {
/** @var array */
protected $data;
function &wrappedArray(): ?array { return $this->data; }
function __toString(): string { return var_export($this->data, true); }
#function __debugInfo() { return $this->data; }
function reset(?array &$data): void { $this->data =& $data; }
function &array(): ?array { return $this->data; }
function count(): int { return $this->data !== null? count($this->data): 0; }
function keys(): array { return $this->data !== null? array_keys($this->data): []; }

View File

@ -3,9 +3,9 @@ namespace nulib\php;
use Closure;
use nulib\cl;
use nulib\ValueException;
use nulib\ref\php\ref_func;
use nulib\schema\Schema;
use nulib\ValueException;
use ReflectionClass;
use ReflectionFunction;
use ReflectionMethod;

View File

@ -1,9 +1,8 @@
<?php
namespace nulib\values;
namespace nulib\php;
use nulib\cl;
use nulib\str;
use nulib\php\func;
use ReflectionClass;
use ReflectionException;

View File

@ -1,7 +1,8 @@
<?php
namespace nulib\values;
namespace nulib\php;
use nulib\cl;
use nulib\str;
use ReflectionClass;
use ReflectionException;

View File

@ -4,6 +4,11 @@ namespace nulib\php\time;
use InvalidArgumentException;
class DateInterval extends \DateInterval {
static function with($interval): self {
if ($interval instanceof static) return $interval;
else return new static($interval);
}
protected static function to_string(DateInterval $interval) {
$string = "P";
$y = $interval->y;
@ -24,6 +29,7 @@ class DateInterval extends \DateInterval {
}
function __construct($duration) {
if (is_int($duration)) $duration = "PT${duration}S";
if ($duration instanceof \DateInterval) {
$this->y = $duration->y;
$this->m = $duration->m;

View File

@ -24,6 +24,11 @@ use InvalidArgumentException;
* @property-read string $YmdHMSZ
*/
class DateTime extends \DateTime {
static function with($datetime): self {
if ($datetime instanceof static) return $datetime;
else return new static($datetime);
}
const DMY_PATTERN = '/^(\d+)\/(\d+)(?:\/(\d+))?$/';
const YMD_PATTERN = '/^((?:\d{2})?\d{2})(\d{2})(\d{2})$/';
const DMYHIS_PATTERN = '/^(\d+)\/(\d+)(?:\/(\d+))? +(\d+)[h:.](\d+)(?:[:.](\d+))?$/';

View File

@ -13,6 +13,7 @@ use InvalidArgumentException;
* - une chaine de la forme "x[WDHMS]y" x et y sont des nombres et la lettre
* est l'unité de temps: W représente une semaine, D une journée, H une heure,
* M une minute et S une seconde.
* - la chaine "INF" qui représente une durée infinie
*
* Dans cette dernière forme, le timestamp destination est calculé en ajoutant x
* unités. puis l'unité inférieure est ramenée à (0 + y)
@ -24,6 +25,11 @@ use InvalidArgumentException;
* NB: la valeur y pour l'unité S est ignorée
*/
class Delay {
static function with($delay, ?DateTimeInterface $from=null): self {
if ($delay instanceof static) return $delay;
else return new static($delay, $from);
}
/** valeurs par défaut de x et y pour les unités supportées */
const DEFAULTS = [
"w" => [0, 5],
@ -35,6 +41,7 @@ class Delay {
static function compute_dest(int $x, string $u, ?int $y, DateTime $from): array {
$dest = DateTime::clone($from);
$yu = null;
switch ($u) {
case "w":
if ($x > 0) {
@ -43,29 +50,28 @@ class Delay {
}
$w = 7 - intval($dest->format("w"));
$dest->add(new \DateInterval("P${w}D"));
$u = "h";
$yu = "h";
break;
case "d":
$dest->add(new \DateInterval("P${x}D"));
$u = "h";
$yu = "h";
break;
case "h":
$dest->add(new \DateInterval("PT${x}H"));
$u = "m";
$yu = "m";
break;
case "m":
$dest->add(new \DateInterval("PT${x}M"));
$u = "s";
$yu = "s";
break;
case "s":
$dest->add(new \DateInterval("PT${x}S"));
$u = null;
break;
}
if ($y !== null && $u !== null) {
if ($y !== null && $yu !== null) {
$h = intval($dest->format("H"));
$m = intval($dest->format("i"));
switch ($u) {
switch ($yu) {
case "h":
$dest->setTime($y, 0, 0, 0);
break;
@ -77,13 +83,18 @@ class Delay {
break;
}
}
$repr = $y !== null? "$x$y$y": "$x";
$u = strtoupper($u);
$repr = $y !== null? "$x$u$y": "$x";
return [$dest, $repr];
}
function __construct($delay, ?DateTimeInterface $from=null) {
if ($from === null) $from = new DateTime();
if (is_int($delay)) {
if ($delay === "INF") {
$dest = DateTime::clone($from);
$dest->add(new DateInterval("P9999Y"));
$repr = "INF";
} elseif (is_int($delay)) {
[$dest, $repr] = self::compute_dest($delay, "s", null, $from);
} elseif (is_string($delay) && preg_match('/^\d+$/', $delay)) {
$x = intval($delay);
@ -104,6 +115,13 @@ class Delay {
$this->repr = $repr;
}
function __serialize(): array {
return [$this->dest, $this->repr];
}
function __unserialize(array $data): void {
[$this->dest, $this->repr] = $data;
}
/** @var DateTime */
protected $dest;
@ -111,6 +129,22 @@ class Delay {
return $this->dest;
}
function addDuration($duration) {
if (is_int($duration) && $duration < 0) {
$this->dest->sub(DateInterval::with(-$duration));
} else {
$this->dest->add(DateInterval::with($duration));
}
}
function subDuration($duration) {
if (is_int($duration) && $duration < 0) {
$this->dest->add(DateInterval::with(-$duration));
} else {
$this->dest->sub(DateInterval::with($duration));
}
}
/** @var string */
protected $repr;
@ -125,7 +159,8 @@ class Delay {
/** retourner true si le délai imparti est écoulé */
function isElapsed(?DateTimeInterface $now=null): bool {
return $this->_getDiff($now)->invert == 0;
if ($this->repr === "INF") return false;
else return $this->_getDiff($now)->invert == 0;
}
/**

View File

@ -1,5 +1,5 @@
<?php
namespace nulib\values;
namespace nulib\php;
use ArrayAccess;
use nulib\str;

View File

@ -1,5 +1,5 @@
<?php
namespace nulib\values;
namespace nulib\php;
use ArrayAccess;
use nulib\str;

View File

@ -1,5 +1,5 @@
<?php
namespace nulib\ref\os\csv;
namespace nulib\ref\file\csv;
/**
* Class ref_csv: références des valeurs normalisées pour les fichiers CSV à

View File

@ -0,0 +1,22 @@
<?php
namespace nulib\schema;
class OldSchema {
/**
* @var bool true si toutes les clés du schéma doivent exister, avec leur
* valeur par défaut le cas échéant
*/
protected $ppEnsureKeys;
/** @var bool true si les clés doivent être dans l'ordre du schéma */
protected $ppOrderKeys;
/**
* @var bool true si les valeurs doivent être automatiquement vérifiées et
* corrigées
*/
protected $ppVerifix;
function ensureSchema(&$value, $key=null, ?array $params=null): void {
}
}

197
php/src/schema/README.md Normal file
View File

@ -0,0 +1,197 @@
# nulib\schema
objet: s'assurer que des données soit dans un type particulier, en les
convertissant si nécessaire. la source de ces données peut-être diverse:
formulaire web, résultat d'une requête SQL, flux CSV, etc.
les données dont on peut modéliser le schéma sont de 3 types:
* scalaire
* tableau associatif
* liste (tableau séquentiel ou associatif d'éléments du même type)
chaque type de données a une syntaxe spécifique pour la définition du schéma.
## Nature de schéma
Un schéma se présente sous la forme d'un tableau associatif avec des clés qui
dépendent de la nature du schéma. La nature du schéma est indiquée avec la clé
`""` (chaine vide), e.g
~~~php
const SCHEMA = [
"" => NATURE,
];
~~~
La nature indique le type de données représenté par le schéma.
* nature scalaire: modélise une donnée scalaire
~~~php
const SCALAR_SCHEMA = [
$type, [$default, $title, ...]
"" => "scalar",
];
~~~
Si le type est "array" ou "?array", on peut préciser le schéma de la donnée
~~~php
const SCALAR_SCHEMA = [
"?array", [$default, $title, ...]
"" => "scalar",
"schema" => NAKED_SCHEMA,
];
~~~
* nature tableau associatif: modélise un tableau associatif (le tableau peut
avoir des clés numériques ou chaines --> seules les clés décrites par le
schéma sont validées)
~~~php
const ASSOC_SCHEMA = [
KEY => VALUE_SCHEMA,
...
"" => "assoc",
];
~~~
la nature "tableau associatif" est du sucre syntaxique pour une valeur
scalaire de type "?array" dont on précise le schéma
~~~php
// la valeur ci-dessus est strictement équivalent à
const ASSOC_SCHEMA = [
"?array",
"" => "scalar",
"schema" => [
KEY => VALUE_SCHEMA,
...
],
];
~~~
* nature liste: modélise une liste de valeurs du même type (le tableau peut
avoir des clés numériques ou chaines --> on ne modélise ni le type ni la
valeur des clés)
~~~php
const LIST_SCHEMA = [
"?array", [$default, $title, ...]
"" => "list",
"schema" => ITEM_SCHEMA,
];
~~~
## Schéma d'une valeur scalaire
Dans sa forme normalisée, une valeur scalaire est généralement modélisée de
cette manière:
~~~php
const SCALAR_SCHEMA = [
"type" => "types autorisés de la valeur",
"default" => "valeur par défaut si la valeur n'existe pas",
"title" => "libellé de la valeur, utilisable par exemple dans un formulaire",
"required" => "la valeur est-elle requise? si oui, elle doit exister",
"nullable" => "si la valeur existe, peut-elle être nulle?",
"desc" => "description de la valeur",
"checker_func" => "une fonction qui vérifie une valeur et la classifie",
"parser_func" => "une fonction qui analyse une chaine pour produire la valeur",
"messages" => "messages à afficher en cas d'erreur d'analyse",
"formatter_func" => "une fonction qui formatte la valeur pour affichage",
"format" => "format à utiliser pour l'affichage",
"" => "scalar",
"schema" => "schéma de la valeur si le type est array ou ?array, null sinon",
];
~~~
L'ordre des clés du schéma ci-dessus indique la clé associé à une valeur si elle
est fournie dans un tableau séquentiel. Par exemple, les deux schéma suivants
sont équivalents:
~~~php
const SCALAR_SCHEMA1 = [
"string", null, "une valeur chaine",
];
const SCALAR_SCHEMA2 = [
"type" => "string",
"default" => null,
"title" => "une valeur chaine",
"" => "scalar",
];
~~~
Si la nature du schéma n'est pas spécifiée, on considère que c'est un schéma de
nature scalaire si:
* c'est une chaine, qui représente alors le type, e.g `"string"`
* c'est un tableau avec un unique élément à l'index 0 de type chaine, qui est
aussi le type, e.g `["string"]`
* c'est un tableau avec un élément à l'index 0, ainsi que d'autres éléments,
e.g `["string", null, "required" => true]`
message indique les messages à afficher en cas d'erreur d'analyse. les clés sont
normalisées et correspondent à différents états de la valeur tels qu'analysés
par `checker_func`
~~~php
const MESSAGE_SCHEMA = [
"missing" => "message si la valeur n'existe pas dans la source et qu'elle est requise",
"unavailable" => "message si la valeur vaut false dans la source et qu'elle est requise",
"null" => "message si la valeur est nulle et qu'elle n'est pas nullable",
"empty" => "message si la valeur est une chaine vide et que ce n'est pas autorisé",
"invalid" => "message si la valeur est invalide",
];
~~~
## Schéma d'un tableau associatif
Dans sa forme *non normalisée*, un tableau associatif est généralement modélisé
de cette manière:
~~~php
const ASSOC_SCHEMA = [
KEY => VALUE_SCHEMA,
...
"" => "assoc",
];
~~~
où chaque occurrence de `KEY => VALUE_SCHEMA` définit le schéma de la valeur
dont la clé est `KEY`
Si la nature du schéma n'est pas spécifiée, on considère que c'est un schéma de
nature associative si:
* c'est un tableau uniquement associatif avec aucun élément séquentiel, e.g
`["name" => "string", "age" => "int"]`
VALUE_SCHEMA peut-être n'importe quel schéma valide, qui sera analysé
récursivement, avec cependant l'ajout de quelques clés supplémentaires:
* description de la valeur dans le contexte du tableau
~~~php
VALUE_SCHEMA = [
...
"name" => "identifiant de la valeur",
"pkey" => "chemin de clé de la valeur dans le tableau associatif",
];
~~~
* s'il s'agit d'une valeur scalaire simple autre que array
~~~php
VALUE_SCHEMA = [
...
"header" => "nom de l'en-tête s'il faut présenter cette donnée dans un tableau",
"composite" => "ce champ fait-il partie d'une valeur composite?",
];
~~~
## Schéma d'une liste (tableau séquentiel ou associatif d'éléments du même type)
Dans sa forme *non normalisée*, une liste est généralement modélisée de cette
manière:
~~~php
const LIST_SCHEMA = [ITEM_SCHEMA];
~~~
où ITEM_SCHEMA est le schéma des éléments de la liste
Pour information, la forme normalisée est plutôt de la forme
~~~php
const LIST_SCHEMA = [
"?array",
"" => "list",
"schema" => ITEM_SCHEMA,
];
~~~
le type "?array" ou "array" indique si la liste est nullable ou non. la valeur
par défaut est "?array"
Si la nature du schéma n'est pas spécifiée, on considère que c'est un schéma de
nature liste si:
* c'est un tableau avec un unique élément de type tableau à l'index 0, e.g
`[["string", null, "required" => true]]`
-*- coding: utf-8 mode: markdown -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8:noeol:binary

30
php/src/schema/Result.php Normal file
View File

@ -0,0 +1,30 @@
<?php
namespace nulib\schema;
use nulib\schema\_assoc\AssocResult;
use nulib\schema\_list\ListResult;
use nulib\schema\_scalar\ScalarResult;
/**
* Class Result: résultat de l'analyse ou de la normalisation d'une valeur
*/
abstract class Result {
function __construct() {
$this->reset();
}
function isAssoc(?AssocResult &$assoc=null): bool { return false; }
function isList(?ListResult &$list=null): bool { return false; }
function isScalar(?ScalarResult &$scalar=null): bool { return false; }
/**
* Obtenir la liste des clés valides pour les valeurs accessibles via cet
* objet
*/
abstract function getKeys(): array;
/** obtenir un objet pour gérer la valeur spécifiée */
abstract function getResult($key=null): Result;
abstract function reset(): void;
}

94
php/src/schema/Schema.php Normal file
View File

@ -0,0 +1,94 @@
<?php
namespace nulib\schema;
use ArrayAccess;
use nulib\AccessException;
use nulib\cl;
use nulib\schema\_assoc\AssocSchema;
use nulib\schema\_list\ListSchema;
use nulib\schema\_scalar\ScalarSchema;
abstract class Schema implements ArrayAccess {
/**
* créer si besoin une nouvelle instance de {@link Schema} à partir d'une
* définition de schéma
*
* - si $schema est une instance de schéma, la retourner
* - si $schema est un array, c'est une définition, et elle est remplacée par
* l'instance de Schema nouvelle créée
* - sinon, prendre $definition comme définition
*/
static function ns(&$schema, $definition=null, $definitionKey=null): self {
if (is_array($schema)) {
$definition = $schema;
$schema = null;
}
if ($schema === null) {
if (AssocSchema::isa_definition($definition)) {
$schema = new AssocSchema($definition, $definitionKey);
} elseif (ListSchema::isa_definition($definition)) {
$schema = new ListSchema($definition, $definitionKey);
} elseif (ScalarSchema::isa_definition($definition)) {
$schema = new ScalarSchema($definition, $definitionKey);
} else {
throw SchemaException::invalid_schema();
}
}
return $schema;
}
/**
* Créer si besoin une nouvelle instance de {@link Value} qui référence la
* variable $dest (si $destKey===null) ou $dest[$destKey] si $destKey n'est
* pas null
*/
static function nv(?Value &$destv=null, &$dest=null, $destKey=null, &$schema=null, $definition=null): Value {
if ($definition === null) {
# bien que techniquement, $definition peut être null (il s'agit alors du
# schéma d'un scalaire quelconque), on ne l'autorise pas ici
throw SchemaException::invalid_schema("definition is required");
}
return self::ns($schema, $definition)->newValue($destv, $dest, $destKey);
}
/**
* @var array définition du schéma, à redéfinir le cas échéant dans une classe
* dérivée
*/
const SCHEMA = null;
/** @var array */
protected $definition;
/** retourner true si le schéma est de nature tableau associatif */
function isAssoc(?AssocSchema &$assoc=null): bool { return false; }
/** retourner true si le schéma est de nature liste */
function isList(?ListSchema &$list=null): bool { return false; }
/** retourner true si le schéma est de nature scalaire */
function isScalar(?ScalarSchema &$scalar=null): bool { return false; }
abstract function newValue(?Value &$destv=null, &$dest=null, $destKey=null): Value;
#############################################################################
# key & properties
function offsetExists($offset): bool {
return array_key_exists($offset, $this->definition);
}
function offsetGet($offset) {
if (!array_key_exists($offset, $this->definition)) return null;
else return $this->definition[$offset];
}
function offsetSet($offset, $value): void {
throw AccessException::read_only(null, $offset);
}
function offsetUnset($offset): void {
throw AccessException::read_only(null, $offset);
}
const _PROPERTY_PKEYS = [];
function __get($name) {
$pkey = cl::get(static::_PROPERTY_PKEYS, $name, $name);
return cl::pget($this->definition, $pkey);
}
}

View File

@ -0,0 +1,22 @@
<?php
namespace nulib\schema;
use Exception;
class SchemaException extends Exception {
static final function invalid_schema(?string $message=null): self {
$invalid_schema = "invalid schema";
if ($message !== null) $invalid_schema .= ": $message";
return new static($invalid_schema);
}
static final function invalid_kind(string $kind, $value, ?string $message=null): self {
$invalid_kind = var_export($value, true).": invalid $kind";
if ($message !== null) $invalid_kind .= ": $message";
return new static($invalid_kind);
}
static final function invalid_type($value, ?string $message=null): self {
return self::invalid_kind("type", $value, $message);
}
}

82
php/src/schema/TODO.md Normal file
View File

@ -0,0 +1,82 @@
# nulib\schema
* implémenter support `analyzer_func`, `extractor_func`, `parser_func`,
`normalizer_func`, `formatter_func`
* dans AssocSchema, support `[key_prefix]` qui permet de spécifier un préfixe
commun aux champs dans le tableau destination, e.g
~~~php
$value = Schema::ns($schema, [
"a" => "?string",
"b" => "?int",
])->newValue();
$dest = ["x_a" => 5, "x_b" => "10"],
$value->reset($dest, null, [
"key_prefix" => "x_",
]);
# $dest vaut ["x_a" => "5", "x_b" => 10];
~~~
définir si le préfixe doit être spécifié sur le schéma ou sur la valeur...
actuellement, le code ne permet pas de définir de tels paramètres...
alternative: c'est lors de la *définition* du schéma que le préfixe est ajouté
e.g
~~~php
$value = Schema::ns($schema, [
"a" => "?string",
"b" => "?int",
], [
"key_prefix" => "x_",
])->newValue();
$dest = ["x_a" => 5, "x_b" => "10"],
$value->reset($dest);
# $dest vaut ["x_a" => "5", "x_b" => 10];
~~~
* dans la définition, `[type]` est remplacé par l'instance de IType lors de sa
résolution?
* implémenter l'instanciation de types avec des paramètres particuliers. *si*
des paramètres sont fournis, le type est instancié avec la signature
`IType($typeDefinition, $schemaDefinition)` e.g
~~~php
const SCHEMA = ["type", default, "required" => true];
# le type est instancié comme suit:
$type = new ttype();
const SCHEMA = [[["type", ...]], default, "required" => true];
# le type est instancié comme suit:
# le tableau peut être une liste ou associatif, c'est au type de décider ce
# qu'il en fait
$type = new ttype(["type", ...], SCHEMA);
~~~
* ajouter à IType les méthodes getName() pour le nom officiel du type,
getAliases() pour les alias supportés, et getClass() pour la définition de la
classe dans les méthodes et propriétés
getName() et getAliases() sont juste pour information, ils ne sont pas utilisés
lors de la résolution du type effectif.
* si cela a du sens, dans AssocSchema, n'instancier les schémas de chaque clé qu'à la demande.
l'idée est de ne pas perdre du temps à instancier un schéma qui ne serait pas utilisé
on pourrait avoir d'une manière générale quelque chose comme:
~~~
Schema::ensure(&$schema, ?array $def=null, $defKey=null): Schema;
~~~
* si $schema est une instance de Schema, la retourner
* si c'est un array, c'est une définition et il faut la remplacer par l'instance de Schema correspondant
* sinon, prendre $def comme définition
$key est la clé si $schema est dans un autre schema
* actuellement, pour un schéma associatif, si on normalise un tableau séquentiel,
chaque valeur correspond à la clé de même rang, eg. pour un schéma
~~~php
const SCHEMA = ["first" => DEF, "second" => DEF];
const ARRAY = ["first", "second"];
~~~
la valeur normalisée de `ARRAY` est `["first" => "first", "second" => "second"]`
cependant, dans certaines circonstances (notamment pour des paramètres), on
devrait pouvoir considérer une valeur indexée comme un flag, i.e la valeur
normalisée de ARRAY serait `["first" => true, "second" => true]`
la définition de ces "circonstances" est encore à faire: soit un paramètre
lors de la définition du schéma, soit un truc magique du genre "toutes les
valeurs séquentielles sont des clés du schéma"
-*- coding: utf-8 mode: markdown -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8:noeol:binary

85
php/src/schema/Value.php Normal file
View File

@ -0,0 +1,85 @@
<?php
namespace nulib\schema;
use ArrayAccess;
use IteratorAggregate;
use nulib\schema\_assoc\AssocValue;
use nulib\schema\_list\ListValue;
use nulib\schema\_scalar\ScalarValue;
use nulib\schema\types\IType;
abstract class Value implements ArrayAccess, IteratorAggregate {
function isAssoc(?AssocValue &$assoc=null): bool { return false; }
function isList(?ListValue &$list=null): bool { return false; }
function isScalar(?ScalarValue &$scalar=null): bool { return false; }
/** spécifier la valeur destination gérée par cet objet */
abstract function reset(&$dest, $destKey=null, ?bool $verifix=null): self;
/**
* Obtenir la liste des clés valides pour les valeurs accessibles via cet
* objet
*/
abstract function getKeys(): array;
/** obtenir un objet pour gérer la valeur spécifiée */
abstract function getValue($key=null): Value;
function getIterator() {
foreach ($this->getKeys() as $key) {
yield $key => $this->getValue($key);
}
}
/**
* obtenir le résultat de l'appel d'une des fonctions {@link set()} ou
* {@link unset()}
*/
abstract function getResult(): Result;
/** retourner true si la valeur existe */
abstract function isPresent(): bool;
/** retourner le type associé à la valeur */
abstract function getType(): IType;
/** retourner true si la valeur est disponible */
abstract function isAvailable(): bool;
/** retourner true si la valeur est valide */
abstract function isValid(): bool;
/** retourner true si la valeur est dans sa forme normalisée */
abstract function isNormalized(): bool;
/** obtenir la valeur */
abstract function get($default=null);
/** remplacer la valeur */
abstract function set($value): self;
/** supprimer la valeur */
abstract function unset(): self;
/** formatter la valeur pour affichage */
abstract function format($format=null): string;
#############################################################################
# key & properties
function offsetExists($offset): bool {
return in_array($offset, $this->getKeys());
}
function offsetGet($offset) {
return $this->getValue($offset);
}
function offsetSet($offset, $value): void {
$this->getValue($offset)->set($value);
}
function offsetUnset($offset): void {
$this->getValue($offset)->unset();
}
}

View File

@ -0,0 +1,8 @@
<?php
namespace nulib\schema\_assoc;
use nulib\schema\Result;
class AssocResult extends Result {
function isAssoc(?AssocResult &$assoc=null): bool { $assoc = $this; return true;}
}

View File

@ -0,0 +1,55 @@
<?php
namespace nulib\schema\_assoc;
use nulib\cl;
use nulib\ref\schema\ref_schema;
use nulib\schema\Schema;
use nulib\schema\Value;
/**
* Class AssocSchema
*/
class AssocSchema extends Schema {
/** @var array meta-schema d'un schéma de nature tableau associatif */
const METASCHEMA = ref_schema::ASSOC_METASCHEMA;
/**
* indiquer si $definition est une définition de schéma de nature tableau
* associatif que {@link normalize()} pourrait normaliser
*/
static function isa_definition($definition): bool {
if (!is_array($definition)) return false;
# nature explicitement spécifiée
if (array_key_exists("", $definition)) {
$nature = $definition[""];
if ($nature === "assoc") return true;
if (is_array($nature)
&& array_key_exists(0, $nature)
&& $nature[0] === "assoc") {
return true;
}
return false;
}
# un tableau associatif
return !cl::have_num_keys($definition);
}
static function normalize($definition, $definitionKey=null): array {
}
function __construct($definition=null, $definitionKey=null, bool $normalize=true) {
if ($definition === null) $definition = static::SCHEMA;
if ($normalize) $definition = self::normalize($definition, $definitionKey);
$this->definition = $definition;
}
function isAssoc(?AssocSchema &$assoc=null): bool {
$assoc = $this;
return true;
}
function newValue(?Value &$destv=null, &$dest=null, $destKey=null): AssocValue {
if ($destv instanceof AssocValue) return $destv->reset($dest, $destKey);
else return ($destv = new AssocValue($this, $dest, $destKey));
}
}

View File

@ -0,0 +1,18 @@
<?php
namespace nulib\schema\_assoc;
use nulib\schema\Result;
use nulib\schema\Value;
class AssocValue extends Value {
function isAssoc(?AssocValue &$assoc=null): bool { $assoc = $this; return true; }
function ensureKeys(): bool {
}
function orderKeys(): bool {
}
/** @param Result[] $results */
function verifix(bool $throw=true, ?array &$results=null): bool {
}
}

View File

@ -0,0 +1,8 @@
<?php
namespace nulib\schema\_list;
use nulib\schema\Result;
class ListResult extends Result {
function isList(?ListResult &$list=null): bool { $list = $this; return true;}
}

View File

@ -0,0 +1,53 @@
<?php
namespace nulib\schema\_list;
use nulib\ref\schema\ref_schema;
use nulib\schema\Schema;
use nulib\schema\Value;
class ListSchema extends Schema {
/** @var array meta-schema d'un schéma de nature liste */
const METASCHEMA = ref_schema::LIST_METASCHEMA;
/**
* indiquer si $definition est une définition de schéma de nature liste que
* {@link normalize()} pourrait normaliser
*/
static function isa_definition($definition): bool {
if (!is_array($definition)) return false;
# nature explicitement spécifiée
if (array_key_exists("", $definition)) {
$nature = $definition[""];
if ($nature === "list") return true;
if (is_array($nature)
&& array_key_exists(0, $nature)
&& $nature[0] === "list") {
return true;
}
return false;
}
# un unique élément tableau à l'index 0
$count = count($definition);
$haveIndex0 = array_key_exists(0, $definition);
return $count == 1 && $haveIndex0 && is_array($definition[0]);
}
static function normalize($definition, $definitionKey=null): array {
}
function __construct($definition=null, $definitionKey=null, bool $normalize=true) {
if ($definition === null) $definition = static::SCHEMA;
if ($normalize) $definition = self::normalize($definition, $definitionKey);
$this->definition = $definition;
}
function isList(?ListSchema &$list=null): bool {
$list = $this;
return true;
}
function newValue(?Value &$destv=null, &$dest=null, $destKey=null): ListValue {
if ($destv instanceof ListValue) return $destv->reset($dest, $destKey);
else return ($destv = new ListValue($this, $dest, $destKey));
}
}

View File

@ -0,0 +1,19 @@
<?php
namespace nulib\schema\_list;
use nulib\schema\Result;
use nulib\schema\Value;
class ListValue extends Value {
function isList(?ListValue &$list=null): bool { $list = $this; return true; }
function ensureKeys(): bool {
}
function orderKeys(): bool {
}
/** @param Result[] $results */
function verifix(bool $throw=true, ?array &$results=null): bool {
}
}

View File

@ -0,0 +1,165 @@
<?php
namespace nulib\schema\_scalar;
use nulib\cl;
use nulib\ref\schema\ref_analyze;
use nulib\ref\schema\ref_schema;
use nulib\schema\Result;
use nulib\ValueException;
/**
* Class ScalarResult: résultat de l'analyse ou de la normalisation d'une valeur
*
* @property bool $resultAvailable le résultat est-il disponible?
* @property bool $present la valeur existe-t-elle?
* @property bool $available si la valeur existe, est-elle disponible?
* @property bool $null si la valeur est disponible, est-elle nulle?
* @property bool $valid si la valeur est disponible, est-elle valide?
* @property bool $normalized si la valeur est valide, est-elle normalisée?
* @property string|null $orig valeur originale avant analyse avec parse()
* @property string|null $message message si la valeur n'est pas valide
*/
class ScalarResult extends Result {
const KEYS = ["resultAvailable", "present", "available", "null", "valid", "normalized", "orig", "message"];
function isScalar(?ScalarResult &$scalar=null): bool { $scalar = $this; return true; }
function getKeys(): array {
return [null];
}
function getResult($key=null): Result {
if ($key === null) return $this;
else throw ValueException::invalid_key($key);
}
/** @var array */
protected $result;
function reset(): void {
$this->result = array_merge(
array_fill_keys(static::KEYS, null), [
"resultAvailable" => false,
"present" => false,
"available" => false,
"null" => false,
"valid" => false,
"normalized" => false,
]);
}
function __get(string $name) {
return $this->result[$name];
}
function __set(string $name, $value): void {
$this->result[$name] = $value;
}
protected static function replace_key(string &$message, ?string $key): void {
if ($key) {
$message = str_replace("{key}", $key, $message);
} else {
$message = str_replace("{key}: ", "", $message);
$message = str_replace("cette valeur", "la valeur", $message);
}
}
protected static function replace_orig(string &$message, $orig): void {
$message = str_replace("{orig}", strval($orig), $message);
}
protected function getMessage(string $key, ScalarSchema $schema): string {
$message = cl::get($schema->messages, $key);
if ($message !== null) return $message;
return cl::get(ref_schema::MESSAGES, $key);
}
function setMissing(ScalarSchema $schema): int {
$this->resultAvailable = true;
$this->present = false;
$this->available = false;
if (!$schema->required) {
$this->null = false;
$this->valid = true;
$this->normalized = true;
return ref_analyze::NORMALIZED;
} else {
$message = $this->getMessage("missing", $schema);
self::replace_key($message, $schema->name);
$this->message = $message;
return ref_analyze::MISSING;
}
}
function setUnavailable(ScalarSchema $schema): int {
$this->resultAvailable = true;
$this->present = true;
$this->available = false;
if (!$schema->required) {
$this->null = false;
$this->valid = true;
$this->normalized = true;
return ref_analyze::NORMALIZED;
} else {
$message = $this->getMessage("unavailable", $schema);
self::replace_key($message, $schema->name);
$this->message = $message;
return ref_analyze::UNAVAILABLE;
}
}
function setNull(ScalarSchema $schema): int {
$this->resultAvailable = true;
$this->present = true;
$this->available = true;
$this->null = true;
if ($schema->nullable) {
$this->valid = true;
$this->normalized = true;
return ref_analyze::NORMALIZED;
} else {
$message = $this->getMessage("null", $schema);
self::replace_key($message, $schema->name);
$this->message = $message;
return ref_analyze::NULL;
}
}
function setInvalid($value, ScalarSchema $schema): int {
$this->resultAvailable = true;
$this->present = true;
$this->available = true;
$this->null = false;
$this->valid = false;
$this->orig = $value;
$message = $this->getMessage("invalid", $schema);
self::replace_key($message, $schema->name);
self::replace_orig($message, $schema->orig);
$this->message = $message;
return ref_analyze::INVALID;
}
function setValid(): int {
$this->resultAvailable = true;
$this->present = true;
$this->available = true;
$this->null = false;
$this->valid = true;
return ref_analyze::VALID;
}
function setNormalized(): int {
$this->resultAvailable = true;
$this->present = true;
$this->available = true;
$this->null = false;
$this->valid = true;
$this->normalized = true;
return ref_analyze::NORMALIZED;
}
function throw(bool $throw): void {
if ($throw) throw new ValueException($this->message);
}
}

View File

@ -0,0 +1,193 @@
<?php
namespace nulib\schema\_scalar;
use nulib\cl;
use nulib\ref\schema\ref_schema;
use nulib\ref\schema\ref_types;
use nulib\schema\Schema;
use nulib\schema\SchemaException;
use nulib\schema\types\tarray;
use nulib\schema\types\tbool;
use nulib\schema\types\tcallable;
use nulib\schema\types\tcontent;
use nulib\schema\types\tpkey;
use nulib\schema\types\tstring;
use nulib\schema\Value;
/**
* Class ScalarSchema
*
* @property-read array $type
* @property-read mixed $default
* @property-read string|null $title
* @property-read bool $required
* @property-read bool $nullable
* @property-read string|array|null $desc
* @property-read callable|null $analyzerFunc
* @property-read callable|null $extractorFunc
* @property-read callable|null $parserFunc
* @property-read callable|null $normalizerFunc
* @property-read array|null $messages
* @property-read callable|null $formatterFunc
* @property-read mixed $format
* @property-read array $nature
* @property-read string|int|null $name
* @property-read string|array|null $pkey
* @property-read string|null $header
* @property-read bool|null $composite
*/
class ScalarSchema extends Schema {
/** @var array meta-schema d'un schéma de nature scalaire */
const METASCHEMA = ref_schema::SCALAR_METASCHEMA;
/**
* indiquer si $definition est une définition de schéma scalaire que
* {@link normalize()} pourrait normaliser
*/
static function isa_definition($definition): bool {
# chaine ou null
if ($definition === null) return true;
if (is_string($definition)) return true;
if (!is_array($definition)) return false;
# nature explicitement spécifiée
if (array_key_exists("", $definition)) {
$nature = $definition[""];
if ($nature === "scalar") return true;
if (is_array($nature)
&& array_key_exists(0, $nature)
&& $nature[0] === "scalar") {
return true;
}
return false;
}
# un unique élément chaine à l'index 0
$count = count($definition);
$haveIndex0 = array_key_exists(0, $definition);
if ($count == 1 && $haveIndex0
&& ($definition[0] === null || is_string($definition[0]))) {
return true;
}
# un élément à l'index 0, et d'autres éléments
return $haveIndex0 && $count > 1;
}
static function normalize($definition, $definitionKey=null): array {
if (!is_array($definition)) $definition = [$definition];
# s'assurer que toutes les clés existent avec leur valeur par défaut
$index = 0;
foreach (array_keys(self::METASCHEMA) as $key) {
if (!array_key_exists($key, $definition)) {
if (array_key_exists($index, $definition)) {
$definition[$key] = $definition[$index];
unset($definition[$index]);
$index++;
} else {
$definition[$key] = self::METASCHEMA[$key][1];
}
}
}
# réordonner les clés numériques
if (cl::have_num_keys($definition)) {
$keys = array_keys($definition);
$index = 0;
foreach ($keys as $key) {
if (!is_int($key)) continue;
$definition[$index] = $definition[$key];
unset($definition[$key]);
$index++;
}
}
# type
$types = [];
$deftype = $definition["type"];
$nullable = $definition["nullable"];
if ($deftype === null) {
$types[] = null;
$nullable = true;
} else {
if (!is_array($deftype)) {
if (!is_string($deftype)) throw SchemaException::invalid_type($deftype);
$deftype = explode("|", $deftype);
}
foreach ($deftype as $type) {
if ($type === null || $type === "null") {
$nullable = true;
continue;
}
if (!is_string($type)) throw SchemaException::invalid_type($type);
if (substr($type, 0, 1) == "?") {
$type = substr($type, 1);
$nullable = true;
}
if ($type === "") throw SchemaException::invalid_type($type);
$type = cl::get(ref_types::ALIASES, $type, $type);
$types = array_merge($types, explode("|", $type));
}
if (!$types) throw SchemaException::invalid_schema("scalar: type is required");
$types = array_keys(array_fill_keys($types, true));
}
$definition["type"] = $types;
$definition["nullable"] = $nullable;
# nature
$nature = $definition[""];
tarray::ensure_array($nature);
if (!array_key_exists(0, $nature) || $nature[0] !== "scalar") {
throw SchemaException::invalid_schema("expected scalar nature");
}
$definition[""] = $nature;
# name, pkey, header
$name = $definition["name"];
$pkey = $definition["pkey"];
$header = $definition["header"];
if ($name === null) $name = $definitionKey;
tstring::ensure_nstring($name);
tpkey::ensure_npkey($pkey);
tstring::ensure_nstring($header);
if ($pkey === null) $pkey = $name;
if ($header === null) $header = $name;
$definition["name"] = $name;
$definition["pkey"] = $pkey;
$definition["header"] = $header;
# autres éléments
tstring::ensure_nstring($definition["title"]);
tbool::ensure_bool($definition["required"]);
tbool::ensure_bool($definition["nullable"]);
tcontent::ensure_ncontent($definition["desc"]);
tcallable::ensure_ncallable($definition["analyzer_func"]);
tcallable::ensure_ncallable($definition["extractor_func"]);
tcallable::ensure_ncallable($definition["parser_func"]);
tcallable::ensure_ncallable($definition["normalizer_func"]);
tarray::ensure_narray($definition["messages"]);
tcallable::ensure_ncallable($definition["formatter_func"]);
tbool::ensure_nbool($definition["composite"]);
return $definition;
}
function __construct($definition=null, $definitionKey=null, bool $normalize=true) {
if ($definition === null) $definition = static::SCHEMA;
if ($normalize) $definition = self::normalize($definition, $definitionKey);
$this->definition = $definition;
}
function isScalar(?ScalarSchema &$scalar=null): bool {
$scalar = $this;
return true;
}
function newValue(?Value &$destv=null, &$dest=null, $destKey=null): ScalarValue {
if ($destv instanceof ScalarValue) return $destv->reset($dest, $destKey);
else return ($destv = new ScalarValue($this, $dest, $destKey));
}
#############################################################################
# key & properties
const _PROPERTY_PKEYS = [
"analyzerFunc" => "analyzer_func",
"extractorFunc" => "extractor_func",
"parserFunc" => "parser_func",
"normalizerFunc" => "normalizer_func",
"formatterFunc" => "formatter_func",
"nature" => ["", 0],
];
}

View File

@ -0,0 +1,198 @@
<?php
namespace nulib\schema\_scalar;
use nulib\ref\schema\ref_analyze;
use nulib\schema\input\Input;
use nulib\schema\types;
use nulib\schema\types\IType;
use nulib\schema\Value;
use nulib\ValueException;
class ScalarValue extends Value {
function __construct(ScalarSchema $schema, &$dest=null, $destKey=null, bool $defaultVerifix=true, ?bool $defaultThrow=null) {
if ($dest !== null && $defaultThrow = null) {
# Si $dest est null, ne pas lancer d'exception, parce qu'on considère que
# c'est une initialisation sans conséquences
$defaultThrow = true;
}
$this->schema = $schema;
$this->defaultVerifix = $defaultVerifix;
$this->defaultThrow = $defaultThrow !== null? $defaultThrow: false;
$this->result = new ScalarResult();
$this->reset($dest, $destKey);
$this->defaultThrow = $defaultThrow !== null? $defaultThrow: true;
}
function isScalar(?ScalarValue &$scalar=null): bool { $scalar = $this; return true; }
/** @var ScalarSchema schéma de cette valeur */
protected $schema;
/** @var Input source et destination de la valeur */
protected $input;
/** @var string|int|null clé de la valeur dans le tableau destination */
protected $destKey;
/** @var bool */
protected $defaultVerifix;
/** @var bool */
protected $defaultThrow;
/** @var IType type de la valeur après analyse */
protected $type;
/** @var ?ScalarResult résultat de l'analyse de la valeur */
protected $result;
function reset(&$dest, $destKey=null, ?bool $verifix=null): Value {
if ($dest instanceof Input) $input = $dest;
else $input = new Input($dest);
$this->input = $input;
$this->destKey = $destKey;
$this->type = null;
$this->_analyze();
if ($verifix == null) $verifix = $this->defaultVerifix;
if ($verifix) $this->verifix();
return $this;
}
function getKeys(): array {
return [null];
}
function getValue($key=null): ScalarValue {
if ($key === null) return $this;
throw ValueException::invalid_key($key);
}
/** analyser la valeur et résoudre son type */
function _analyze(): int {
$schema = $this->schema;
$input = $this->input;
$destKey = $this->destKey;
$result = $this->result;
$result->reset();
if (!$input->isPresent($destKey)) return $result->setMissing($schema);
$haveType = false;
$types = [];
$type = $firstType = null;
$haveValue = false;
$value = null;
# d'abord chercher un type pour lequel c'est une valeur normalisée
foreach ($schema->type as $name) {
$type = types::get($name);
if ($firstType === null) $firstType = $type;
$types[] = $type;
if ($type->isAvailable($input, $destKey)) {
if (!$haveValue) {
$value = $input->get($destKey);
$haveValue = true;
}
if ($type->isValid($value, $normalized) && $normalized) {
$haveType = true;
$this->type = $type;
break;
}
}
}
if (!$haveType) {
# ensuite chercher un type pour lequel la valeur est valide
foreach ($types as $type) {
if ($type->isAvailable($input, $destKey) && $type->isValid($value)) {
$haveType = true;
$this->type = $type;
break;
}
}
}
# sinon prendre le premier type
if (!$haveType) $type = $this->type = $firstType;
if (!$type->isAvailable($input, $destKey)) return $result->setUnavailable($schema);
$value = $input->get($destKey);
if ($type->isNull($value)) return $result->setNull($schema);
if ($type->isValid($value, $normalized)) {
if ($normalized) return $result->setNormalized();
else return $result->setValid();
}
if (is_string($value)) return ref_analyze::STRING;
else return $result->setInvalid($value, $schema);
}
function verifix(?bool $throw=null): bool {
if ($throw === null) $throw = $this->defaultThrow;
$destKey = $this->destKey;
$verifix = false;
$result =& $this->result;
$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, $destKey);
} elseif ($result->valid && !$result->normalized) {
# normaliser la valeur
$verifix = true;
}
} else {
$verifix = true;
}
if ($verifix) {
$value = $this->input->get($destKey);
$modified = $this->type->verifix($value, $result, $this->schema);
if ($result->valid) $this->input->set($value, $destKey);
}
if (!$result->valid) $result->throw($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->destKey);
else return $default;
}
function set($value, ?bool $verifix=null): ScalarValue {
$this->input->set($value, $this->destKey);
$this->_analyze();
if ($verifix === null) $verifix = $this->defaultVerifix;
if ($verifix) $this->verifix();
return $this;
}
function unset(?bool $verifix=null): ScalarValue {
$this->input->unset($this->destKey);
$this->_analyze();
if ($verifix === null) $verifix = $this->defaultVerifix;
if ($verifix) $this->verifix();
return $this;
}
function format($format=null): string {
return $this->type->format($this->input->get($this->destKey), $format);
}
}

View File

@ -0,0 +1,46 @@
<?php
namespace nulib\schema\input;
#XXX implémenter le renommage de paramètres et faire des méthodes pour
# construire des querystring et paramètres de formulaires
/**
* Class FormInput: accès à des paramètres de formulaire (POST ou GET, dans cet
* ordre)
*
* cette implémentation lit depuis les paramètres de formulaire et écrit dans
* une référence
*/
class FormInput extends Input {
const ALLOW_EMPTY = false;
function isPresent($key=null): bool {
if ($key === null) return false;
return array_key_exists($key, $_POST) || array_key_exists($key, $_GET);
}
function isAvailable($key=null): bool {
if ($key === null) return false;
if (array_key_exists($key, $_POST)) {
return $this->allowEmpty || $_POST[$key] !== "";
} elseif (array_key_exists($key, $_GET)) {
return $this->allowEmpty || $_GET[$key] !== "";
} else {
return false;
}
}
function get($key=null) {
if ($key === null) return null;
if (array_key_exists($key, $_POST)) {
$value = $_POST[$key];
if ($value === "" && !$this->allowEmpty) return null;
return $value;
} elseif (array_key_exists($key, $_GET)) {
$value = $_GET[$key];
if ($value === "" && !$this->allowEmpty) return null;
return $value;
} else {
return null;
}
}
}

View File

@ -0,0 +1,35 @@
<?php
namespace nulib\schema\input;
/**
* Class GetInput: accès à des paramètres de formulaire de type GET uniquement
*
* cette implémentation lit depuis les paramètres de formulaire et écrit dans
* une référence
*/
class GetInput extends FormInput {
function isPresent($key=null): bool {
if ($key === null) return false;
return array_key_exists($key, $_GET);
}
function isAvailable($key=null): bool {
if ($key === null) return false;
if (array_key_exists($key, $_GET)) {
return $this->allowEmpty || $_GET[$key] !== "";
} else {
return false;
}
}
function get($key=null) {
if ($key === null) return null;
if (array_key_exists($key, $_GET)) {
$value = $_GET[$key];
if ($value === "" && !$this->allowEmpty) return null;
return $value;
} else {
return null;
}
}
}

View File

@ -0,0 +1,61 @@
<?php
namespace nulib\schema\input;
use nulib\cl;
/**
* Class Input: accès à une valeur
*
* cette implémentation lit depuis et écrit dans une référence
*/
class Input {
const ALLOW_EMPTY = true;
function __construct(&$dest=null, ?array $params=null) {
$this->dest =& $dest;
$allowEmpty = cl::get($params, "allow_empty");
if ($allowEmpty === null) $allowEmpty = static::ALLOW_EMPTY;
$this->allowEmpty = $allowEmpty;
}
protected $dest;
/** tester si la valeur existe sans tenir compte de $allowEmpty */
function isPresent($key=null): bool {
if ($key === null) return true;
$dest = $this->dest;
return $dest !== null && array_key_exists($key, $dest);
}
/**
* @var bool comment considérer une chaine vide: "" si allowEmpty, null sinon
*/
protected $allowEmpty;
/** tester si la valeur est disponible en tenant compte de $allowEmpty */
function isAvailable($key=null): bool {
if ($key === null) return true;
$dest = $this->dest;
if ($dest === null || !array_key_exists($key, $dest)) return false;
return $this->allowEmpty || $dest[$key] !== "";
}
function get($key=null) {
$dest = $this->dest;
if ($key === null) return $dest;
if ($dest === null || !array_key_exists($key, $dest)) return null;
$value = $dest[$key];
if ($value === "" && !$this->allowEmpty) return null;
return $value;
}
function set($value, $key=null): void {
if ($key === null) $this->dest = $value;
else $this->dest[$key] = $value;
}
function unset($key=null): void {
if ($key === null) $this->dest = null;
else unset($this->dest[$key]);
}
}

View File

@ -0,0 +1,35 @@
<?php
namespace nulib\schema\input;
/**
* Class PostInput: accès à des paramètres de formulaire de type POST uniquement
*
* cette implémentation lit depuis les paramètres de formulaire et écrit dans
* une référence
*/
class PostInput extends FormInput {
function isPresent($key=null): bool {
if ($key === null) return false;
return array_key_exists($key, $_POST);
}
function isAvailable($key=null): bool {
if ($key === null) return false;
if (array_key_exists($key, $_POST)) {
return $this->allowEmpty || $_POST[$key] !== "";
} else {
return false;
}
}
function get($key=null) {
if ($key === null) return null;
if (array_key_exists($key, $_POST)) {
$value = $_POST[$key];
if ($value === "" && !$this->allowEmpty) return null;
return $value;
} else {
return null;
}
}
}

37
php/src/schema/types.php Normal file
View File

@ -0,0 +1,37 @@
<?php
namespace nulib\schema;
use nulib\schema\types\IType;
use nulib\schema\types\Registry;
use nulib\schema\types\tarray;
use nulib\schema\types\tbool;
use nulib\schema\types\tcallable;
use nulib\schema\types\tfloat;
use nulib\schema\types\tint;
use nulib\schema\types\tstring;
/**
* Class types: classe outil pour gérer le registre de types
*/
class types {
/** @var Registry */
private static $registry;
static function registry(): Registry {
if (self::$registry === null) {
self::$registry = new Registry();
}
return self::$registry;
}
static function get(string $name): IType {
return self::registry()->get($name);
}
static function string(): tstring { return self::get("string"); }
static function bool(): tbool { return self::get("bool"); }
static function int(): tint { return self::get("int"); }
static function float(): tfloat { return self::get("float"); }
static function array(): tarray { return self::get("array"); }
static function callable(): tcallable { return self::get("callable"); }
}

View File

@ -0,0 +1,33 @@
<?php
namespace nulib\schema\types;
use nulib\schema\input\Input;
use nulib\schema\Result;
use nulib\schema\Schema;
/**
* Interface IType: un type de données
*/
interface IType {
/** la donnée $input($destKey) est-elle disponible? */
function isAvailable(Input $input, $destKey): bool;
/** la valeur $value est-elle nulle? */
function isNull($value): bool;
/** la valeur $value est-elle valide et normalisée le cas échéant? */
function isValid($value, ?bool &$normalized=null): bool;
/**
* analyser, corriger éventuellement et normaliser la valeur
*
* si la valeur était déjà normalisée, ou si une erreur s'est produite,
* retourner false.
*/
function verifix(&$value, Result &$result, Schema $schema): bool;
/**
* formatter la valeur pour affichage. $value est garanti d'être du bon type
*/
function format($value, $format=null): string;
}

View File

@ -0,0 +1,36 @@
<?php
namespace nulib\schema\types;
use nulib\cl;
class Registry {
const TYPES = [
"string" => tstring::class,
"bool" => tbool::class, "boolean" => tbool::class,
"int" => tint::class, "integer" => tint::class,
"float" => tfloat::class, "flt" => tfloat::class,
"double" => tfloat::class, "dbl" => tfloat::class,
"array" => tarray::class,
"callable" => tcallable::class,
# types spéciaux
"key" => tkey::class,
"pkey" => tpkey::class,
"content" => tcontent::class,
];
function __construct() {
$this->types = [];
}
/** @var IType[] */
protected $types;
function get(string $name): IType {
$type = cl::get($this->types, $name);
if ($type === null) {
$class = self::TYPES[$name];
$type = $this->types[$name] = new $class();
}
return $type;
}
}

View File

@ -0,0 +1,14 @@
<?php
namespace nulib\schema\types;
use nulib\schema\input\Input;
abstract class _tsimple implements IType {
function isAvailable(Input $input, $destKey): bool {
return $input->isAvailable($destKey) && $input->get($destKey) !== false;
}
function isNull($value): bool {
return $value === null || (is_string($value) && trim($value) === "");
}
}

View File

@ -0,0 +1,27 @@
<?php
namespace nulib\schema\types;
use nulib\cl;
use nulib\schema\Result;
use nulib\schema\Schema;
class tarray extends _tsimple {
static function ensure_array(&$array): void {
if (!is_array($array)) $array = cl::with($array);
}
static function ensure_narray(&$array): void {
if ($array !== null) self::ensure_array($array);
}
function isValid($value, ?bool &$normalized=null): bool {
$normalized = is_array($value);
return is_scalar($value) || is_array($value);
}
function verifix(&$value, Result &$result, Schema $schema): bool {
}
function format($value, $format=null): string {
}
}

View File

@ -0,0 +1,123 @@
<?php
namespace nulib\schema\types;
use nulib\cl;
use nulib\schema\_scalar\ScalarResult;
use nulib\schema\_scalar\ScalarSchema;
use nulib\schema\input\Input;
use nulib\schema\Result;
use nulib\schema\Schema;
use nulib\ValueException;
class tbool extends _tsimple {
/** liste de valeurs chaines à considérer comme 'OUI' */
const YES_VALUES = [
# 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 = [
# 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(?string $value): bool {
if ($value === null) return false;
$value = strtolower(trim($value));
if (in_array($value, self::YES_VALUES, true)) return true;
// n'importe quelle valeur numérique
if (is_numeric($value)) return $value != 0;
return false;
}
/** Vérifier si $value est 'NON' */
static final function is_no(?string $value): bool {
if ($value === null) return true;
$value = strtolower(trim($value));
return in_array($value, self::NO_VALUES, true);
}
static function ensure_bool(&$bool): void {
if (is_string($bool)) {
if (self::is_yes($bool)) $bool = true;
elseif (self::is_no($bool)) $bool = false;
}
if (!is_bool($bool)) $bool = boolval($bool);
}
static function ensure_nbool(&$bool): void {
if ($bool !== null) self::ensure_bool($bool);
}
function isAvailable(Input $input, $destKey): bool {
return $input->isAvailable($destKey);
}
function isValid($value, ?bool &$normalized=null): bool {
$normalized = is_bool($value);
if (is_string($value)) {
$value = trim($value);
$valid = self::is_yes($value) || self::is_no($value);
} else {
$valid = is_scalar($value);
}
return $valid;
}
/**
* @var ScalarResult $result
* @var ScalarSchema $schema
*/
function verifix(&$value, Result &$result, Schema $schema): bool {
if (is_bool($value)) {
$result->setNormalized();
return false;
} elseif (is_string($value)) {
$bool = trim($value);
if (self::is_yes($bool)) $value = true;
elseif (self::is_no($bool)) $value = false;
else return $result->setInvalid($value, $schema);
} elseif (is_scalar($value)) {
$value = boolval($value);
} else {
return $result->setInvalid($value, $schema);
}
$result->setValid();
return true;
}
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 DEFAULT_FORMAT = self::OUINON_FORMAT;
function format($value, $format=null): string {
if ($format === null) $format = static::DEFAULT_FORMAT;
if (is_string($format)) {
$oformat = $format;
$format = cl::get(self::FORMATS, strtolower($oformat));
if ($format === null) throw ValueException::invalid_kind($oformat, "format");
}
if ($value === null) {
$null = $format[2];
if ($null !== false) return $null;
}
return $value? $format[0]: $format[1];
}
}

View File

@ -0,0 +1,29 @@
<?php
namespace nulib\schema\types;
use nulib\php\func;
use nulib\schema\Result;
use nulib\schema\Schema;
use nulib\ValueException;
use stdClass;
class tcallable extends _tsimple {
static function ensure_callable(&$callable): void {
if (!is_callable($callable)) throw ValueException::invalid_type($callable, "callable");
}
static function ensure_ncallable(&$callable): void {
if ($callable !== null) self::ensure_callable($callable);
}
function isValid($value, ?bool &$normalized=null): bool {
$normalized = is_callable($value);
return func::check_func($value, stdClass::class);
}
function verifix(&$value, Result &$result, Schema $schema): bool {
}
function format($value, $format=null): string {
}
}

View File

@ -0,0 +1,26 @@
<?php
namespace nulib\schema\types;
use nulib\schema\Result;
use nulib\schema\Schema;
abstract class tcontent extends _tsimple {
static function ensure_content(&$content): void {
if (!is_string($content) && !is_array($content)) $content = strval($content);
}
static function ensure_ncontent(&$content): void {
if ($content !== null) self::ensure_content($content);
}
function isValid($value, ?bool &$normalized=null): bool {
$normalized = is_string($value) || is_array($value);
return is_scalar($value) || is_array($value);
}
function verifix(&$value, Result &$result, Schema $schema): bool {
}
function format($value, $format=null): string {
}
}

View File

@ -0,0 +1,50 @@
<?php
namespace nulib\schema\types;
use nulib\schema\_scalar\ScalarResult;
use nulib\schema\_scalar\ScalarSchema;
use nulib\schema\Result;
use nulib\schema\Schema;
class tfloat extends _tsimple {
static function ensure_float(&$float): void {
if (!is_float($float)) $float = floatval($float);
}
static function ensure_nfloat(&$float): void {
if ($float !== null) self::ensure_float($float);
}
function isValid($value, ?bool &$normalized=null): bool {
$normalized = is_float($value);
if (is_string($value)) $valid = is_numeric(trim($value));
else $valid = is_scalar($value);
return $valid;
}
/**
* @var ScalarResult $result
* @var ScalarSchema $schema
*/
function verifix(&$value, Result &$result, Schema $schema): bool {
if (is_float($value)) {
$result->setNormalized();
return false;
} elseif (is_string($value)) {
$float = trim($value);
if (is_numeric($float)) $value = floatval($float);
else return $result->setInvalid($value, $schema);
} elseif (is_scalar($value)) {
$value = floatval($value);
} else {
return $result->setInvalid($value, $schema);
}
$result->setValid();
return true;
}
function format($value, $format=null): string {
if ($format !== null) return sprintf($format, $value);
else return strval($value);
}
}

View File

@ -0,0 +1,52 @@
<?php
namespace nulib\schema\types;
use nulib\schema\_scalar\ScalarResult;
use nulib\schema\_scalar\ScalarSchema;
use nulib\schema\Result;
use nulib\schema\Schema;
class tint extends _tsimple {
static function ensure_int(&$int): void {
if (!is_int($int)) $int = intval($int);
}
static function ensure_nint(&$int): void {
if ($int !== null) self::ensure_int($int);
}
const INT_PATTERN = '/^[-+]?[0-9]+(?:\.[0-9]*)?$/';
function isValid($value, ?bool &$normalized=null): bool {
$normalized = is_int($value);
if (is_string($value)) $valid = is_numeric(trim($value));
else $valid = is_scalar($value);
return $valid;
}
/**
* @var ScalarResult $result
* @var ScalarSchema $schema
*/
function verifix(&$value, Result &$result, Schema $schema): bool {
if (is_int($value)) {
$result->setNormalized();
return false;
} elseif (is_string($value)) {
$int = trim($value);
if (is_numeric($int)) $value = intval($int);
else return $result->setInvalid($value, $schema);
} elseif (is_scalar($value)) {
$value = intval($value);
} else {
return $result->setInvalid($value, $schema);
}
$result->setValid();
return true;
}
function format($value, $format=null): string {
if ($format !== null) return sprintf($format, $value);
else return strval($value);
}
}

View File

@ -0,0 +1,26 @@
<?php
namespace nulib\schema\types;
use nulib\schema\Result;
use nulib\schema\Schema;
class tkey extends _tsimple {
static function ensure_key(&$key): void {
if (!is_string($key) && !is_int($key)) $key = strval($key);
}
static function ensure_nkey(&$key): void {
if ($key !== null) self::ensure_key($key);
}
function isValid($value, ?bool &$normalized=null): bool {
$normalized = is_string($value) || is_int($value);
return is_scalar($value);
}
function verifix(&$value, Result &$result, Schema $schema): bool {
}
function format($value, $format=null): string {
}
}

View File

@ -0,0 +1,32 @@
<?php
namespace nulib\schema\types;
use nulib\schema\Result;
use nulib\schema\Schema;
class tpkey extends _tsimple {
static function ensure_pkey(&$pkey): void {
if (!is_string($pkey) && !is_int($pkey) && !is_array($pkey)) $pkey = strval($pkey);
if (is_array($pkey)) {
foreach ($pkey as &$key) {
tkey::ensure_key($key);
};
unset($key);
}
}
static function ensure_npkey(&$pkey): void {
if ($pkey !== null) self::ensure_pkey($pkey);
}
function isValid($value, ?bool &$normalized=null): bool {
$normalized = is_string($value) || is_int($value) || is_array($value);
return is_scalar($value) || is_array($value);
}
function verifix(&$value, Result &$result, Schema $schema): bool {
}
function format($value, $format=null): string {
}
}

View File

@ -0,0 +1,48 @@
<?php
namespace nulib\schema\types;
use nulib\schema\_scalar\ScalarResult;
use nulib\schema\_scalar\ScalarSchema;
use nulib\schema\Result;
use nulib\schema\Schema;
class tstring extends _tsimple {
static function ensure_string(&$string): void {
if (!is_string($string)) $string = strval($string);
}
static function ensure_nstring(&$string): void {
if ($string !== null) self::ensure_string($string);
}
function isNull($value): bool {
return $value === null;
}
function isValid($value, ?bool &$normalized=null): bool {
$normalized = is_string($value);
return is_scalar($value);
}
/**
* @var ScalarResult $result
* @var ScalarSchema $schema
*/
function verifix(&$value, Result &$result, Schema $schema): bool {
if (is_string($value)) {
$result->setNormalized();
return false;
} elseif (is_scalar($value)) {
$value = strval($value);
$result->setValid();
return true;
} else {
$result->setInvalid($value, $schema);
return false;
}
}
function format($value, $format=null): string {
return strval($value);
}
}

View File

@ -1,8 +1,6 @@
<?php
namespace nulib;
use nur\base;
/**
* Class str: gestion des chaines de caractère "simples"
*/

212
php/src/text/Word.php Normal file
View File

@ -0,0 +1,212 @@
<?php
namespace nulib\text;
use nur\b\ValueException;
use nur\txt;
/**
* Class Word: accord d'un nom ou d'un adjectif en genre et en nombre
*
* Pour accorder un nom, construire l'objet avec une spécification de la forme
* "GENRE:ARTICLE NOM"
* - L'article peut être "l'", "le", "la". Le genre est requis avec "l'"
* - Le genre peut être "masculin:" ou "féminin:"
* - Le nom est composé d'un ou plusieurs mots qui se terminent par
* - #s pour un pluriel en "s", e.g porte#s
* - #x pour un pluriel en "x", e.g lieu#x
* - rien si le mot est invariable
*
* Pour accorder un adjectif, la spécification peut se limiter à "ADJECTIF"
* - L'adjectif est composé d'un ou plusieurs mots, qui en plus des marques du
* pluriel peuvent se terminent par
* - #e pour indiquer la marque du féminin, e.g "né#e"
* - rien si le mot est invariable
*
* Chaque mot peut aussi commencer par "^" pour indiquer les caractères qui
* peuvent être mis en majuscule par la méthode u(). Par défaut, seule la
* première lettre est mise en majuscule
*/
class Word {
/** @var bool le mot est-il féminin? */
private $fem;
/** @var string article "le", "la", "l'" */
private $le;
/** @var string article "du", "de la", "de l'" */
private $du;
/** @var string article "au", "à la", "à l'" */
private $au;
/** @var string le mot sans article */
private $w;
function __construct(string $spec, bool $adjective=false) {
if (preg_match('/^f([eé]m(inin)?)?\s*:\s*/iu', $spec, $ms)) {
$fem = true;
$spec = substr($spec, strlen($ms[0]));
} elseif (preg_match('/^m(asc(ulin)?)?\s*:\s*/i', $spec, $ms)) {
$fem = false;
$spec = substr($spec, $ms[0]);
} elseif (preg_match('/\s*\|f(?:[eé]m(?:inin)?)?\s*$/iu', $spec, $ms, PREG_OFFSET_CAPTURE)) {
$fem = true;
$spec = substr($spec, 0, $ms[0][1]);
} elseif (preg_match('/\s*\|m(?:asc(?:ulin)?)?\s*$/i', $spec, $ms, PREG_OFFSET_CAPTURE)) {
$fem = false;
$spec = substr($spec, 0, $ms[0][1]);
} else {
$fem = null;
}
if (preg_match('/^l\'\s*/i', $spec, $ms) && $fem !== null) {
$le = "l'";
$du = "de l'";
$au = "à l'";
$spec = substr($spec, strlen($ms[0]));
} elseif (preg_match('/^la\s+/i', $spec, $ms)) {
$fem = true;
$le = "la ";
$du = "de la ";
$au = "à la ";
$spec = substr($spec, strlen($ms[0]));
} elseif (preg_match('/^le\s+/i', $spec, $ms)) {
$fem = false;
$le = "le ";
$du = "du ";
$au = "au ";
$spec = substr($spec, strlen($ms[0]));
} else {
$le = null;
$du = null;
$au = null;
}
if (!$adjective) {
# si c'est un nom, il faut l'article et le genre
if ($fem === null) {
throw new ValueException("Vous devez spécifier le genre du nom");
} elseif ($le === null || $du === null || $au === null) {
throw new ValueException("Vous devez spécifier l'article du nom");
}
}
$this->fem = $fem;
$this->le = $le;
$this->du = $du;
$this->au = $au;
$this->w = $spec;
}
/**
* retourner le mot sans article
*
* @param bool|int $amount nombre du nom, avec l'équivalence false===0 et
* true===2. à partir de 2, le mot est ecrit au pluriel
* @param bool|string $fem genre du nom avec lequel accorder les adjectifs,
* avec l'équivalence false==="M" et true==="F"
*/
function w($amount=1, bool $upper1=false, $fem=false): string {
if ($amount === true) $amount = 2;
elseif ($amount === false) $amount = 0;
$amount = abs($amount);
$w = $this->w;
# marque du nombre
if ($amount <= 1) {
$w = preg_replace('/#[sx]/', "", $w);
} else {
$w = preg_replace('/#([sx])/', "$1", $w);
}
# marque du genre
if ($fem === "f" || $fem === "F") $fem = true;
elseif ($fem === "m" || $fem === "M") $fem = false;
$repl = $fem? "$1": "";
$w = preg_replace('/#([e])/', $repl, $w);
# mise en majuscule
if ($upper1) {
if (strpos($w, "^") === false) {
# uniquement la première lettre
$w = txt::upper1($w);
} else {
# toutes les lettres qui suivent les occurences de ^
$w = preg_replace_callback('/\^([[:alpha:]])/u', function ($ms) {
return mb_strtoupper($ms[1]);
}, $w);
}
}
return $w;
}
/**
* retourner le mot sans article avec la première lettre en majuscule.
* alias pour $this->w($amount, true, $fem)
*
* @param bool|int $amount
*/
function u($amount=1, $fem=false): string {
return $this->w($amount, true, $fem);
}
/**
* retourner l'adjectif accordé avec le genre spécifié.
* alias pour $this->w($amount, false, $fem)
*
* @param bool|int $amount
*/
function a($fem=false, $amount=1): string {
return $this->w($amount, false, $fem);
}
/** retourner le mot sans article et avec la quantité */
function q(int $amount=1, $fem=false): string {
return $amount." ".$this->w($amount, $fem);
}
/** retourner le mot sans article et avec la quantité $amount/$max */
function r(int $amount, int $max, $fem=false): string {
return "$amount/$max ".$this->w($amount, $fem);
}
/** retourner le mot avec l'article indéfini et la quantité */
function un(int $amount=1, $fem=false): string {
$abs_amount = abs($amount);
if ($abs_amount == 0) {
$aucun = $this->fem? "aucune ": "aucun ";
return $aucun.$this->w($amount, $fem);
} elseif ($abs_amount == 1) {
$un = $this->fem? "une ": "un ";
return $un.$this->w($amount, $fem);
} else {
return "les $amount ".$this->w($amount, $fem);
}
}
function le(int $amount=1, $fem=false): string {
$abs_amount = abs($amount);
if ($abs_amount == 0) {
$le = $this->fem? "la 0 ": "le 0 ";
return $le.$this->w($amount, $fem);
} elseif ($abs_amount == 1) {
return $this->le.$this->w($amount, $fem);
} else {
return "les $amount ".$this->w($amount, $fem);
}
}
function du(int $amount=1, $fem=false): string {
$abs_amount = abs($amount);
if ($abs_amount == 0) {
$du = $this->fem? "de la 0 ": "du 0 ";
return $du.$this->w($amount, $fem);
} elseif ($abs_amount == 1) {
return $this->du.$this->w($amount, $fem);
} else {
return "des $amount ".$this->w($amount, $fem);
}
}
function au(int $amount=1, $fem=false): string {
$abs_amount = abs($amount);
if ($abs_amount == 0) {
$au = $this->fem? "à la 0 ": "au 0 ";
return $au.$this->w($amount, $fem);
} elseif ($abs_amount == 1) {
return $this->au.$this->w($amount, $fem);
} else {
return "aux $amount ".$this->w($amount, $fem);
}
}
}

14
php/src/text/words.php Normal file
View File

@ -0,0 +1,14 @@
<?php
namespace nulib\text;
class words {
static function q(int $count, string $spec, bool $adjective=true): string {
$word = new Word($spec, $adjective);
return $word->q($count);
}
static function r(int $count, int $max, string $spec, bool $adjective=true): string {
$word = new Word($spec, $adjective);
return $word->r($count, $max);
}
}

View File

@ -1,5 +1,5 @@
<?php
namespace nulib\web\ext;
namespace nulib\web\curl;
use nulib\UserException;
use Throwable;
@ -7,16 +7,16 @@ use Throwable;
class CurlException extends UserException {
function __construct($ch, ?string $message=null, $code=0, ?Throwable $previous=null) {
if ($message === null) $message = "(unknown error)";
$user_message = $message;
$tech_message = null;
$userMessage = $message;
$techMessage = null;
if ($ch !== null) {
$parts = [];
$errno = curl_errno($ch);
if ($errno != 0) $parts[] = "errno: $errno";
$error = curl_error($ch);
if ($error != "") $parts[] = "error: $error";
if ($parts) $tech_message = implode(", ", $parts);
if ($parts) $techMessage = implode(", ", $parts);
}
parent::__construct($user_message, $tech_message, $code, $previous);
parent::__construct($userMessage, $techMessage, $code, $previous);
}
}

Some files were not shown because too many files have changed in this diff Show More