Compare commits

..

1 Commits

Author SHA1 Message Date
44806fe161 début 2025-10-08 15:36:19 +04:00
119 changed files with 1777 additions and 5385 deletions

View File

@ -147,23 +147,18 @@ EOF
################################################################################
# Config
function check_gitdir() {
function ensure_gitdir() {
# commencer dans le répertoire indiqué
local chdir="$1"
if [ -n "$chdir" ]; then
cd "$chdir" || return 1
cd "$chdir" || die || return
fi
# se mettre à la racine du dépôt git
local gitdir
git_ensure_gitvcs
setx gitdir=git_get_toplevel
cd "$gitdir" || return 1
}
function ensure_gitdir() {
# commencer dans le répertoire indiqué
check_gitdir "$@" || die || return
cd "$gitdir" || die || return
}
function load_branches() {
@ -250,9 +245,6 @@ function load_config() {
ConfigFile="$(pwd)/.pman.conf"
source "$ConfigFile"
elif [ -n "$1" -a -n "${MYNAME#$1}" ]; then
# $1 est le nom de base de l'outil e.g "pdev", et le suffixe est la
# configuration à charger par défaut. i.e pdev74 chargera par défaut la
# configuration pman74.conf
ConfigFile="$NULIBDIR/bash/src/pman${MYNAME#$1}.conf.sh"
source "$ConfigFile"
else

View File

@ -1,10 +0,0 @@
# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8
PMAN_TOOLS=pdev
SRC_TYPE=DEVELOP
SRC_BRANCH="${SRC_TYPE,,}"; SRC_BRANCH="${SRC_BRANCH^}Branch"
DEST_TYPE=MAIN
DEST_BRANCH="${DEST_TYPE,,}"; DEST_BRANCH="${DEST_BRANCH^}Branch"
ALLOW_MERGE=1
MERGE_PREL=1
ALLOW_DELETE=

View File

@ -1,10 +0,0 @@
# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8
PMAN_TOOL=pdist
SRC_TYPE=DIST
SRC_BRANCH="${SRC_TYPE,,}"; SRC_BRANCH="${SRC_BRANCH^}Branch"
DEST_TYPE=
DEST_BRANCH=
ALLOW_MERGE=
MERGE_PREL=
ALLOW_DELETE=

View File

@ -1,10 +0,0 @@
# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8
PMAN_TOOL=pmain
SRC_TYPE=MAIN
SRC_BRANCH="${SRC_TYPE,,}"; SRC_BRANCH="${SRC_BRANCH^}Branch"
DEST_TYPE=DIST
DEST_BRANCH="${DEST_TYPE,,}"; DEST_BRANCH="${DEST_BRANCH^}Branch"
ALLOW_MERGE=1
MERGE_PREL=
ALLOW_DELETE=

View File

@ -1 +0,0 @@
../php/bin/cachectl.php

View File

@ -1 +0,0 @@
runphp

View File

@ -20,8 +20,8 @@ fi
[ -f /etc/profile ] && source /etc/profile
[ -f ~/.bash_profile ] && source ~/.bash_profile
# Modifier le PATH
PATH=$(qval "$NULIBDIR/wip:$NULIBDIR/bin:$PATH")
# Modifier le PATH. Ajouter aussi le chemin vers les uapps python
PATH=$(qval "$NULIBDIR/bin:$PATH")
if [ -n '$DEFAULT_PS1' ]; then
DEFAULT_PS1=$(qval "[nlshell] $DEFAULT_PS1")

View File

@ -48,7 +48,6 @@
}
},
"bin": [
"php/bin/cachectl.php",
"php/bin/dumpser.php",
"php/bin/json2yml.php",
"php/bin/yml2json.php",

View File

@ -1,7 +0,0 @@
#!/usr/bin/php
<?php
require $_composer_autoload_path?? __DIR__.'/../vendor/autoload.php';
use cli\CachectlApp;
CachectlApp::run();

View File

@ -1,132 +0,0 @@
<?php
namespace cli;
use Exception;
use nulib\app\cli\Application;
use nulib\cache\CacheFile;
use nulib\ext\yaml;
use nulib\os\path;
use nulib\output\msg;
class CachectlApp extends Application {
const ACTION_READ = 10, ACTION_INFOS = 20, ACTION_CLEAN = 30;
const ACTION_UPDATE = 40, ACTION_UPDATE_ADD = 41, ACTION_UPDATE_SUB = 42, ACTION_UPDATE_SET = 43;
const ARGS = [
"merge" => parent::ARGS,
"purpose" => "gestion de fichiers cache",
["-r", "--read", "name" => "action", "value" => self::ACTION_READ,
"help" => "Afficher le contenu d'un fichier cache",
],
["-d::", "--data",
"help" => "Identifiant de la donnée à afficher",
],
["-i", "--infos", "name" => "action", "value" => self::ACTION_INFOS,
"help" => "Afficher des informations sur le fichier cache",
],
["-k", "--clean", "name" => "action", "value" => self::ACTION_CLEAN,
"help" => "Supprimer le fichier cache s'il a expiré",
],
["-a", "--add-duration", "args" => 1,
"action" => [null, "->setActionUpdate", self::ACTION_UPDATE_ADD],
"help" => "Ajouter le nombre de secondes spécifié à la durée du cache",
],
["-b", "--sub-duration", "args" => 1,
"action" => [null, "->setActionUpdate", self::ACTION_UPDATE_SUB],
"help" => "Enlever le nombre de secondes spécifié à la durée du cache",
],
#XXX pas encore implémenté
//["-s", "--set-duration", "args" => 1,
// "action" => [null, "->setActionUpdate", self::ACTION_UPDATE_SET],
// "help" => "Mettre à jour la durée du cache à la valeur spécifiée",
//],
];
protected $action = self::ACTION_READ;
protected $updateAction, $updateDuration;
protected $data = null;
function setActionUpdate(int $action, $updateDuration): void {
$this->action = self::ACTION_UPDATE;
switch ($action) {
case self::ACTION_UPDATE_SUB:
$this->updateAction = CacheFile::UPDATE_SUB;
break;
case self::ACTION_UPDATE_SET:
$this->updateAction = CacheFile::UPDATE_SET;
break;
case self::ACTION_UPDATE_ADD:
$this->updateAction = CacheFile::UPDATE_ADD;
break;
}
$this->updateDuration = $updateDuration;
}
protected function findCaches(string $dir, ?array &$files): void {
foreach (glob("$dir/*") as $file) {
if (is_dir($file)) {
$this->findCaches($file, $files);
} elseif (is_file($file) && fnmatch("*.cache", $file)) {
$files[] = $file;
}
}
}
function main() {
$files = [];
foreach ($this->args as $arg) {
if (is_dir($arg)) {
$this->findCaches($arg, $files);
} elseif (is_file($arg)) {
$files[] = $arg;
} else {
msg::warning("$arg: fichier introuvable");
}
}
$showSection = count($files) > 1;
foreach ($files as $file) {
switch ($this->action) {
case self::ACTION_READ:
if ($showSection) msg::section($file);
$cache = new CacheFile($file, null, [
"readonly" => true,
"duration" => "INF",
"override_duration" => true,
]);
yaml::dump($cache->get($this->data));
break;
case self::ACTION_INFOS:
if ($showSection) msg::section($file);
$cache = new CacheFile($file, null, [
"readonly" => true,
]);
yaml::dump($cache->getInfos());
break;
case self::ACTION_CLEAN:
msg::action(path::ppath($file));
$cache = new CacheFile($file);
try {
if ($cache->deleteExpired()) msg::asuccess("fichier supprimé");
else msg::adone("fichier non expiré");
} catch (Exception $e) {
msg::afailure($e);
}
break;
case self::ACTION_UPDATE:
msg::action(path::ppath($file));
$cache = new CacheFile($file);
try {
$cache->updateDuration($this->updateDuration, $this->updateAction);
msg::asuccess("fichier mis à jour");
} catch (Exception $e) {
msg::afailure($e);
}
break;
default:
self::die("$this->action: action non implémentée");
}
}
}
}

View File

@ -2,16 +2,17 @@
namespace cli\pman;
use nulib\cl;
use nulib\exceptions;
use nulib\ext\json;
use nulib\file;
use nulib\os\path;
use nulib\ValueException;
class ComposerFile {
function __construct(string $composerFile=".", bool $ensureExists=true) {
if (is_dir($composerFile)) $composerFile = path::join($composerFile, 'composer.json');
if ($ensureExists && !file_exists($composerFile)) {
throw exceptions::invalid_value(path::ppath($composerFile), "ce fichier", "il est introuvable");
$message = path::ppath($composerFile).": fichier introuvable";
throw new ValueException($message);
}
$this->composerFile = $composerFile;
$this->load();

View File

@ -2,10 +2,10 @@
namespace cli\pman;
use nulib\A;
use nulib\exceptions;
use nulib\ext\yaml;
use nulib\os\path;
use nulib\str;
use nulib\ValueException;
class ComposerPmanFile {
const NAMES = [".composer.pman", ".pman"];
@ -29,7 +29,8 @@ class ComposerPmanFile {
}
}
if ($ensureExists && !file_exists($configFile)) {
throw exceptions::invalid_value(path::ppath($configFile), "ce fichier", "il est introuvable");
$message = path::ppath($configFile).": fichier introuvable";
throw new ValueException($message);
}
$this->configFile = $configFile;
$this->load();
@ -65,7 +66,9 @@ class ComposerPmanFile {
function getProfileConfig(string $profile, ?array $composerRequires=null, ?array $composerRequireDevs=null): array {
$config = $this->data["composer"][$profile] ?? null;
if ($config === null) throw exceptions::invalid_value($profile, "ce profil");
if ($config === null) {
throw new ValueException("$profile: profil invalide");
}
if ($composerRequires !== null) {
$matchRequires = $this->data["composer"]["match_require"];
foreach ($composerRequires as $dep => $version) {

View File

@ -1,38 +1,36 @@
<?php
namespace nulib;
use RuntimeException;
/**
* Class AccessException: indiquer que la resource ou l'objet auquel on veut
* accéder n'est pas accessible. il s'agit donc d'une erreur de l'utilisateur
*/
class AccessException extends RuntimeException {
class AccessException extends UserException {
static final function read_only(?string $dest=null, ?string $prefix=null): self {
if ($prefix) $prefix = "$prefix: ";
if ($dest === null) $dest = "this property";
$message = "$dest is read-only";
return new static("$prefix$message");
return new static($prefix.$message);
}
static final function immutable_object(?string $dest=null, ?string $prefix=null): self {
if ($prefix) $prefix = "$prefix: ";
if ($dest === null) $dest = "this object";
$message = "$dest is immutable";
return new static("$prefix$message");
return new static($prefix.$message);
}
static final function not_allowed(?string $action=null, ?string $prefix=null): self {
if ($prefix) $prefix = "$prefix: ";
if ($action === null) $action = "this operation";
$message = "$action is not allowed";
return new static("$prefix$message");
return new static($prefix.$message);
}
static final function not_accessible(?string $dest=null, ?string $prefix=null): self {
if ($prefix) $prefix = "$prefix: ";
if ($dest === null) $dest = "this resource";
$message = "$dest is not accessible";
return new static("$prefix$message");
return new static($prefix.$message);
}
}

View File

@ -38,22 +38,17 @@ class ExceptionShadow {
$this->trace = self::extract_trace($exception->getTrace());
$previous = $exception->getPrevious();
if ($previous !== null) $this->previous = new static($previous);
if ($exception instanceof UserException) {
$this->userMessage = $exception->getUserMessage();
$this->techMessage = $exception->getTechMessage();
} else {
$this->userMessage = null;
$this->techMessage = null;
}
}
protected string $class;
/** @var string */
protected $class;
function getClass(): string {
return $this->class;
}
protected string $message;
/** @var string */
protected $message;
function getMessage(): string {
return $this->message;
@ -66,19 +61,22 @@ class ExceptionShadow {
return $this->code;
}
protected string $file;
/** @var string */
protected $file;
function getFile(): string {
return $this->file;
}
protected int $line;
/** @var int */
protected $line;
function getLine(): int {
return $this->line;
}
protected array $trace;
/** @var array */
protected $trace;
function getTrace(): array {
return $this->trace;
@ -94,21 +92,10 @@ class ExceptionShadow {
return implode("\n", $lines);
}
protected ?ExceptionShadow $previous;
/** @var ExceptionShadow */
protected $previous;
function getPrevious(): ?ExceptionShadow {
return $this->previous;
}
protected ?array $userMessage;
function getUserMessage(): ?array {
return $this->userMessage;
}
protected ?array $techMessage;
function getTechMessage(): ?array {
return $this->techMessage;
}
}

View File

@ -18,7 +18,8 @@ class ExitError extends Error {
return $this->getCode() !== 0;
}
protected ?string $userMessage;
/** @var ?string */
protected $userMessage;
function haveUserMessage(): bool {
return $this->userMessage !== null;

View File

@ -12,12 +12,12 @@ class StateException extends LogicException {
if ($method === null) $method = "this method";
$message = "$method is not implemented";
if ($prefix) $prefix = "$prefix: ";
return new static("$prefix$message");
return new static($prefix.$message);
}
static final function unexpected_state(?string $suffix=null): self {
$message = "unexpected state";
if ($suffix) $suffix = ": $suffix";
return new static("$message$suffix");
return new static($message.$suffix);
}
}

View File

@ -1,35 +1,90 @@
<?php
namespace nulib;
use nulib\php\content\c;
use RuntimeException;
use Throwable;
/**
* Class UserException: une exception qui peut contenir un message utilisateur
* et un message technique
* Class UserException: une exception qui peut en plus contenir un message
* utilisateur
*/
class UserException extends RuntimeException {
function __construct($userMessage, $code=0, ?Throwable $previous=null) {
$this->userMessage = $userMessage = c::resolve($userMessage);
parent::__construct(c::to_string($userMessage), $code, $previous);
/** @param Throwable|ExceptionShadow $e */
static function get_user_message($e): ?string {
if ($e instanceof self) return $e->getUserMessage();
else return null;
}
protected ?array $userMessage;
/** @param Throwable|ExceptionShadow $e */
static final function get_user_summary($e): string {
$parts = [];
$first = true;
while ($e !== null) {
$message = self::get_user_message($e);
if (!$message) $message = "(no message)";
if ($first) $first = false;
else $parts[] = "caused by ";
$parts[] = get_class($e) . ": " . $message;
$e = $e->getPrevious();
}
return implode(", ", $parts);
}
function getUserMessage(): ?array {
/** @param Throwable|ExceptionShadow $e */
static function get_message($e): ?string {
$message = $e->getMessage();
if (!$message && $e instanceof self) $message = $e->getUserMessage();
return $message;
}
/** @param Throwable|ExceptionShadow $e */
static final function get_summary($e): string {
$parts = [];
$first = true;
while ($e !== null) {
$message = self::get_message($e);
if (!$message) $message = "(no message)";
if ($first) $first = false;
else $parts[] = "caused by ";
if ($e instanceof ExceptionShadow) $class = $e->getClass();
else $class = get_class($e);
$parts[] = "$class: $message";
$e = $e->getPrevious();
}
return implode(", ", $parts);
}
/** @param Throwable|ExceptionShadow $e */
static final function get_traceback($e): string {
$tbs = [];
$previous = false;
while ($e !== null) {
if (!$previous) {
$efile = $e->getFile();
$eline = $e->getLine();
$tbs[] = "at $efile($eline)";
} else {
$tbs[] = "~~ caused by: " . self::get_summary($e);
}
$tbs[] = $e->getTraceAsString();
$e = $e->getPrevious();
$previous = true;
#XXX il faudrait ne pas réinclure les lignes communes aux exceptions qui
# ont déjà été affichées
}
return implode("\n", $tbs);
}
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 {
return $this->userMessage;
}
protected ?array $techMessage = null;
function getTechMessage(): ?array {
return $this->techMessage;
}
function setTechMessage($techMessage): self {
$techMessage ??= c::resolve($techMessage);
$this->techMessage = $techMessage;
return $this;
}
}

View File

@ -5,4 +5,72 @@ namespace nulib;
* Class ValueException: indiquer qu'une valeur est invalide
*/
class ValueException extends UserException {
private static function value($value): string {
if (is_object($value)) {
return "<".get_class($value).">";
} elseif (is_array($value)) {
$values = $value;
$parts = [];
$index = 0;
foreach ($values as $key => $value) {
if ($key === $index) {
$index++;
$parts[] = self::value($value);
} else {
$parts[] = "$key=>".self::value($value);
}
}
return "[" . implode(", ", $parts) . "]";
} elseif (is_string($value)) {
return $value;
} else {
return var_export($value, true);
}
}
private static function message($value, ?string $message, ?string $kind, ?string $prefix, ?string $suffix): string {
if ($kind === null) $kind = "value";
if ($message === null) $message = "$kind$suffix";
if ($value !== null) {
$value = self::value($value);
if ($prefix) $prefix = "$prefix: $value";
else $prefix = $value;
}
if ($prefix) $prefix = "$prefix: ";
return $prefix.$message;
}
static final function null(?string $kind=null, ?string $prefix=null, ?string $message=null): self {
return new static(self::message(null, $message, $kind, $prefix, " should not be null"));
}
static final function check_null($value, ?string $kind=null, ?string $prefix=null, ?string $message=null) {
if ($value === null) throw static::null($kind, $prefix, $message);
return $value;
}
static final function invalid_kind($value=null, ?string $kind=null, ?string $prefix=null, ?string $message=null): self {
return new static(self::message($value, $message, $kind, $prefix, " is invalid"));
}
static final function invalid_key($value, ?string $prefix=null, ?string $message=null): self {
return self::invalid_kind($value, "key", $prefix, $message);
}
static final function invalid_value($value, ?string $prefix=null, ?string $message=null): self {
return self::invalid_kind($value, "value", $prefix, $message);
}
static final function invalid_type($value, string $expected_type): self {
return new static(self::message($value, null, "type", null, " is invalid, expected $expected_type"));
}
static final function invalid_class($class, string $expected_class): self {
if (is_object($class)) $class = get_class($class);
return new static(self::message($class, null, "class", null, " is invalid, expected $expected_class"));
}
static final function forbidden($value=null, ?string $kind=null, ?string $prefix=null, ?string $message=null): self {
return new static(self::message($value, $message, $kind, $prefix, " is forbidden"));
}
}

View File

@ -5,13 +5,12 @@ use nulib\A;
use nulib\app\cli\Application;
use nulib\app\config\ProfileManager;
use nulib\cl;
use nulib\exceptions;
use nulib\ExitError;
use nulib\os\path;
use nulib\os\sh;
use nulib\php\func;
use nulib\ref\ref_profiles;
use nulib\str;
use nulib\ValueException;
class app {
private static function isa_Application($app): bool {
@ -59,7 +58,7 @@ class app {
} elseif (is_array($app)) {
$params = $app;
} else {
throw exceptions::invalid_type($app, "app", Application::class);
throw ValueException::invalid_type($app, Application::class);
}
return $params;
}
@ -115,21 +114,13 @@ class app {
static function get_profile(?bool &$productionMode=null): string {
return self::get()->getProfile($productionMode);
}
static function is_production_mode(): bool {
return self::get()->isProductionMode();
}
static function is_prod(): bool {
return self::get_profile() === ref_profiles::PROD;
}
static function is_test(): bool {
return self::get_profile() === ref_profiles::TEST;
return self::get_profile() === "prod";
}
static function is_devel(): bool {
return self::get_profile() === ref_profiles::DEVEL;
return self::get_profile() === "devel";
}
static function set_profile(?string $profile=null, ?bool $productionMode=null): void {
@ -410,7 +401,7 @@ class app {
function fencedJoin(string $basedir, ?string ...$paths): string {
$path = path::reljoin($basedir, ...$paths);
if (!path::is_within($path, $basedir)) {
throw exceptions::invalid_type($path, $kind, "path");
throw ValueException::invalid_value($path, "path");
}
return $path;
}

View File

@ -6,8 +6,7 @@ use stdClass;
abstract class AbstractArgsParser {
protected function notEnoughArgs(int $needed, ?string $arg=null): ArgsException {
if ($arg !== null) $arg .= ": ";
$reason = $arg._exceptions::missing_value_message($needed);
return _exceptions::missing_value(null, null, $reason);
return new ArgsException("${arg}nécessite $needed argument(s) supplémentaires");
}
protected function checkEnoughArgs(?string $option, int $count): void {
@ -16,17 +15,16 @@ abstract class AbstractArgsParser {
protected function tooManyArgs(int $count, int $expected, ?string $arg=null): ArgsException {
if ($arg !== null) $arg .= ": ";
$reason = $arg._exceptions::unexpected_value_message($count - $expected);
return _exceptions::unexpected_value(null, null, $reason);
return new ArgsException("${arg}trop d'arguments (attendu $expected, reçu $count)");
}
protected function invalidArg(string $arg): ArgsException {
return _exceptions::invalid_value($arg);
return new ArgsException("$arg: argument invalide");
}
protected function ambiguousArg(string $arg, array $candidates): ArgsException {
$candidates = implode(", ", $candidates);
return new ArgsException("$arg: cet argument est ambigû (les valeurs possibles sont $candidates)");
return new ArgsException("$arg: argument ambigû (les valeurs possibles sont $candidates)");
}
/**

View File

@ -147,11 +147,11 @@ class Aodef {
protected function processExtends(Aolist $argdefs): void {
$option = $this->extends;
if ($option === null) {
throw _exceptions::null_value("extends", "il doit spécifier l'argument destination");
throw ArgsException::missing("extends", "destination arg");
}
$dest = $argdefs->get($option);
if ($dest === null) {
throw _exceptions::invalid_value($option, "extends", "il doit spécifier un argument valide");
throw ArgsException::invalid($option, "destination arg");
}
if ($this->ensureArray !== null) $dest->ensureArray = $this->ensureArray;
@ -178,7 +178,7 @@ class Aodef {
$args = $ms[2] ?? null;
$option = "--$name";
} else {
throw _exceptions::invalid_value($option, "cette option longue");
throw ArgsException::invalid($option, "long option");
}
} elseif (substr($option, 0, 1) === "-") {
$type = self::TYPE_SHORT;
@ -187,7 +187,7 @@ class Aodef {
$args = $ms[2] ?? null;
$option = "-$name";
} else {
throw _exceptions::invalid_value($option, " cette option courte");
throw ArgsException::invalid($option, "short option");
}
} else {
$type = self::TYPE_COMMAND;
@ -196,7 +196,7 @@ class Aodef {
$args = null;
$option = "$name";
} else {
throw _exceptions::invalid_value($option, "cette commande");
throw ArgsException::invalid($option, "command");
}
}
if ($args === ":") {
@ -347,7 +347,7 @@ class Aodef {
$haveNull = true;
break;
} else {
throw _exceptions::invalid_value("$desc: $arg", $kind, "ce n'est pas un argument valide");
throw ArgsException::invalid("$desc: $arg", "option arg");
}
}
@ -366,7 +366,7 @@ class Aodef {
$haveNull = true;
break;
} else {
throw _exceptions::invalid_value("$desc: $arg", $kind, "ce n'est pas un argument valide");
throw ArgsException::invalid("$desc: $arg", "option arg");
}
}
if (!$haveOpt) $haveNull = true;
@ -436,11 +436,6 @@ class Aodef {
$longest ??= self::get_longest($this->_options, self::TYPE_SHORT);
if ($longest !== null) {
$longest = preg_replace('/[^A-Za-z0-9]+/', "_", $longest);
# les options --no-name mettent à jour la valeur $name et inversent
# le traitement
if ($longest !== "no_" && str::del_prefix($longest, "no_")) {
$this->inverse ??= true;
}
if (preg_match('/^[0-9]/', $longest)) {
# le nom de la propriété ne doit pas commencer par un chiffre
$longest = "p$longest";
@ -519,7 +514,7 @@ class Aodef {
case "--set-args": $this->actionSetArgs($dest, $value); break;
case "--set-command": $this->actionSetCommand($dest, $value); break;
case "--show-help": $parser->actionPrintHelp($arg); break;
default: throw _exceptions::invalid_value($this->action, $kind, "action non supportée");
default: throw ArgsException::invalid($this->action, "arg action");
}
}

View File

@ -10,7 +10,7 @@ class Aogroup extends Aolist {
function __construct(array $defs, bool $setup=false) {
$marker = A::pop($defs, 0);
if ($marker !== "group") {
throw _exceptions::missing_value(null, $kind, "ce n'est pas un groupe valide");
throw ArgsException::invalid(null, "group");
}
# réordonner les clés numériques
$defs = array_merge($defs);

View File

@ -1,7 +1,20 @@
<?php
namespace nulib\app\args;
use nulib\UserException;
use nulib\ValueException;
class ArgsException extends UserException {
class ArgsException extends ValueException {
static function missing(?string $value, string $kind): self {
$msg = $value;
if ($msg !== null) $msg .= ": ";
$msg .= "missing $kind";
throw new self($msg);
}
static function invalid(?string $value, string $kind): self {
$msg = $value;
if ($msg !== null) $msg .= ": ";
$msg .= "invalid $kind";
throw new self($msg);
}
}

View File

@ -1,10 +0,0 @@
<?php
namespace nulib\app\args;
use nulib\exceptions;
class _exceptions extends exceptions {
const EXCEPTION = ArgsException::class;
const WORD = "masc:l'argument#s";
}

View File

@ -14,7 +14,6 @@ use nulib\output\console;
use nulib\output\log;
use nulib\output\msg;
use nulib\output\std\StdMessenger;
use nulib\ref\ref_profiles;
/**
* Class Application: application de base
@ -258,9 +257,9 @@ EOT);
"action" => [app::class, "set_profile"],
"help" => "spécifier le profil d'exécution",
],
["-P", "--prod", "action" => [app::class, "set_profile", ref_profiles::PROD]],
["-T", "--test", "action" => [app::class, "set_profile", ref_profiles::TEST]],
["--devel", "action" => [app::class, "set_profile", ref_profiles::DEVEL]],
["-P", "--prod", "action" => [app::class, "set_profile", "prod"]],
["-T", "--test", "action" => [app::class, "set_profile", "test"]],
["--devel", "action" => [app::class, "set_profile", "devel"]],
],
];
@ -308,9 +307,9 @@ EOT);
}
const PROFILE_COLORS = [
ref_profiles::PROD => "@r",
ref_profiles::TEST => "@g",
ref_profiles::DEVEL => "@w",
"prod" => "@r",
"test" => "@g",
"devel" => "@w",
];
const DEFAULT_PROFILE_COLOR = "y";

View File

@ -4,8 +4,8 @@ namespace nulib\app;
use nulib\app\config\ConfigManager;
use nulib\app\config\JsonConfig;
use nulib\app\config\YamlConfig;
use nulib\exceptions;
use nulib\os\path;
use nulib\ValueException;
/**
* Class config: gestion de la configuration de l'application
@ -37,7 +37,7 @@ class config {
} elseif ($ext === ".json") {
$config = new JsonConfig($file);
} else {
throw exceptions::invalid_type($file, $kind, "config file");
throw ValueException::invalid_value($file, "config file");
}
self::add($config);
}

View File

@ -4,8 +4,8 @@ namespace nulib\app\config;
use nulib\A;
use nulib\app\app;
use nulib\cl;
use nulib\exceptions;
use nulib\php\func;
use nulib\ValueException;
use ReflectionClass;
class ConfigManager {
@ -93,7 +93,7 @@ class ConfigManager {
} elseif (is_array($config)) {
$config = new ArrayConfig($config);
} elseif (!($config instanceof IConfig)) {
throw exceptions::invalid_type($config, "config", ["array", IConfig::class]);
throw ValueException::invalid_type($config, "array|IConfig");
}
if (!$inProfiles) $inProfiles = [IConfig::PROFILE_ALL];

View File

@ -3,7 +3,6 @@ namespace nulib\app\config;
use nulib\app\app;
use nulib\app\config;
use nulib\ref\ref_profiles;
/**
* Class ProfileManager: gestionnaire de profils
@ -22,7 +21,10 @@ class ProfileManager {
const PROFILES = null;
/** @var array profils dont le mode production doit être actif */
const PRODUCTION_MODES = ref_profiles::PRODUCTION_MODES;
const PRODUCTION_MODES = [
"prod" => true,
"test" => true,
];
/**
* @var array mapping profil d'application --> profil effectif
@ -112,7 +114,7 @@ class ProfileManager {
$profile ??= $this->getConfigProfile();
$profile ??= $this->getDefaultProfile();
if ($this->isAppProfile) {
$profile ??= $this->profiles[0] ?? ref_profiles::PROD;
$profile ??= $this->profiles[0] ?? "prod";
} else {
$profile ??= $this->mapProfile(app::get_profile());
}

View File

@ -1,50 +0,0 @@
<?php
namespace nulib\cache;
use nulib\php\func;
/**
* Class CacheData: gestion d'une donnée mise en cache
*/
abstract class CacheData {
function __construct(?string $name, $compute) {
$this->name = $name ?? "";
$this->compute = func::withn($compute ?? static::COMPUTE);
}
protected string $name;
function getName() : string {
return $this->name;
}
protected ?func $compute;
/** calculer la donnée */
function compute() {
$compute = $this->compute;
$data = $compute !== null? $compute->invoke(): null;
return $data;
}
/**
* le cache est-il externe? si non, utiliser {@link setDatafile()} pour
* spécifier le fichier destination de la valeur
*/
abstract function isExternal(): bool;
/** spécifier le chemin du cache à partir du fichier de base */
abstract function setDatafile(?string $basefile): void;
/** indiquer si le cache existe */
abstract function exists(): bool;
/** charger la donnée depuis le cache */
abstract function load();
/** sauvegarder la donnée dans le cache et la retourner */
abstract function save($data);
/** supprimer le cache */
abstract function delete();
}

View File

@ -1,354 +0,0 @@
<?php
namespace nulib\cache;
use Exception;
use nulib\cv;
use nulib\ext\utils;
use nulib\file\SharedFile;
use nulib\os\path;
use nulib\php\func;
use nulib\php\time\DateTime;
use nulib\php\time\Delay;
use nulib\str;
class CacheFile extends SharedFile {
/** @var string|int durée de vie par défaut des données mises en cache */
const DURATION = "1D"; // jusqu'au lendemain
static function with($data, ?string $file=null): self {
if ($data instanceof self) return $data;
else return new static($file, $data);
}
protected static function ensure_source($data, ?CacheData &$source, bool $allowArray=true): bool {
if ($data === null || $data instanceof CacheData) {
$source = $data;
} elseif (is_subclass_of($data, CacheData::class)) {
$source = new $data();
} elseif (func::is_callable($data)) {
$source = new DataCacheData(null, $data);
} elseif (is_array($data) && $allowArray) {
return false;
} elseif (is_iterable($data)) {
$source = new DataCacheData(null, static function() use ($data) {
yield from $data;
});
} else {
throw exceptions::invalid_type($source, "source", CacheData::class);
}
return true;
}
function __construct(?string $file, $data=null, ?array $params=null) {
$file ??= path::join(sys_get_temp_dir(), utils::uuidgen());
$file = path::ensure_ext($file, cache::EXT);
$basefile = str::without_suffix(cache::EXT, $file);
$this->initialDuration = Delay::with($params["duration"] ?? static::DURATION);
$this->overrideDuration = $params["override_duration"] ?? false;
$this->readonly = $params["readonly"] ?? false;
$this->cacheNull = $params["cache_null"] ?? false;
$data ??= $params["data"] ?? null;
$this->sources = null;
if (self::ensure_source($data, $source)) {
if ($source !== null) $source->setDatafile($basefile);
$this->sources = ["" => $source];
} else {
$sources = [];
$index = 0;
foreach ($data as $key => $source) {
self::ensure_source($source, $source, false);
if ($source !== null) {
$source->setDatafile($basefile);
if ($key === $index) {
$index++;
$key = $source->getName();
}
} elseif ($key === $index) {
$index++;
}
$sources[$key] = $source;
}
$this->sources = $sources;
}
parent::__construct($file);
}
protected Delay $initialDuration;
protected bool $overrideDuration;
protected bool $readonly;
protected bool $cacheNull;
/** @var ?CacheData[] */
protected ?array $sources;
/**
* vérifier si le fichier est valide. s'il est invalide, il faut le recréer.
*
* on assume que le fichier existe, vu qu'il a été ouvert en c+b
*/
function isValid(): bool {
# considèrer que le fichier est invalide s'il est de taille nulle
return $this->getSize() > 0;
}
protected ?DateTime $start;
protected ?Delay $duration;
protected $data;
/** charger les données. le fichier a déjà été verrouillé en lecture */
protected function loadMetadata(): void {
if ($this->isValid()) {
$this->rewind();
[
"start" => $start,
"duration" => $duration,
"data" => $data,
] = $this->unserialize(null, false, true);
if ($this->overrideDuration) {
$duration = Delay::with($this->initialDuration, $start);
}
} else {
$start = null;
$duration = null;
$data = null;
}
$this->start = $start;
$this->duration = $duration;
$this->data = $data;
}
/**
* tester s'il faut mettre les données à jour. le fichier a déjà été
* verrouillé en lecture
*/
protected function shouldUpdate(bool $noCache=false): bool {
if ($this->isValid()) {
$expired = $this->duration->isElapsed();
} else {
$expired = false;
$noCache = true;
}
return $noCache || $expired;
}
/** sauvegarder les données. le fichier a déjà été verrouillé en écriture */
protected function saveMetadata(): void {
$this->duration ??= $this->initialDuration;
if ($this->start === null) {
$this->start = new DateTime();
$this->duration = Delay::with($this->duration, $this->start);
}
$this->ftruncate();
$this->serialize([
"start" => $this->start,
"duration" => $this->duration,
"data" => $this->data,
], false, true);
}
protected function unlinkFiles(bool $datafilesOnly=false): void {
foreach ($this->sources as $source) {
if ($source !== null) $source->delete();
}
if (!$datafilesOnly) @unlink($this->file);
}
/** tester si $value peut être mis en cache */
protected function shouldCache($value): bool {
return $this->cacheNull || $value !== null;
}
protected ?DateTime $ostart;
protected ?Delay $oduration;
protected $odata;
protected function beforeAction() {
$this->loadMetadata();
$this->ostart = cv::clone($this->start);
$this->oduration = cv::clone($this->duration);
$this->odata = cv::clone($this->data);
}
protected function afterAction() {
$modified = false;
if ($this->start != $this->ostart) $modified = true;
$duration = $this->duration;
$oduration = $this->oduration;
if ($duration === null || $oduration === null) $modified = true;
elseif ($duration->getDest() != $oduration->getDest()) $modified = true;
# égalité stricte uniquement pour $data et $datafiles
if ($this->data !== $this->odata) $modified = true;
if ($modified && !$this->readonly) {
$this->lockWrite();
$this->saveMetadata();
}
}
protected function action(callable $callback, bool $willWrite=false) {
if ($willWrite && !$this->readonly) $this->lockWrite();
else $this->lockRead();
try {
$this->beforeAction();
$result = $callback();
$this->afterAction();
return $result;
} finally {
$this->ostart = null;
$this->oduration = null;
$this->odata = null;
$this->start = null;
$this->duration = null;
$this->data = null;
$this->unlock(true);
}
}
protected function compute() {
return null;
}
protected function refreshData($key, bool $noCache) {
$source = $this->sources[$key] ?? null;
$external = $source !== null && $source->isExternal();
$updateMetadata = $this->shouldUpdate($noCache);
if (!$key && !$external) $updateData = $this->data === null;
else $updateData = !$source->exists();
if (!$this->readonly && ($updateMetadata || $updateData)) {
$this->lockWrite();
if ($updateMetadata) {
# il faut refaire tout le cache
$this->unlinkFiles(true);
$this->start = null;
$this->duration = null;
$this->data = null;
}
if (!$key && !$external) {
# calculer la valeur
try {
if ($source !== null) $data = $source->compute();
else $data = $this->compute();
} catch (Exception $e) {
# le fichier n'est pas mis à jour, mais ce n'est pas gênant: lors
# des futurs appels, l'exception continuera d'être lancée ou la
# valeur sera finalement mise à jour
throw $e;
}
if ($this->shouldCache($data)) $this->data = $data;
else $this->data = $data = null;
} elseif ($source !== null) {
# calculer la valeur
try {
$data = $source->compute();
} catch (Exception $e) {
# le fichier n'est pas mis à jour, mais ce n'est pas gênant: lors
# des futurs appels, l'exception continuera d'être lancée ou la
# valeur sera finalement mise à jour
throw $e;
}
if ($this->shouldCache($data)) {
$data = $source->save($data);
} else {
# ne pas garder le fichier s'il ne faut pas mettre en cache
$source->delete();
$data = null;
}
} else {
$data = null;
}
} elseif (!$key && !$external) {
$data = $this->data;
} elseif ($source !== null && $source->exists()) {
$data = $source->load();
} else {
$data = null;
}
return $data;
}
/**
* s'assurer que le cache est à jour avec les données les plus récentes. si
* les données sont déjà présentes dans le cache et n'ont pas encore expirées
* cette méthode est un NOP
*/
function refresh(bool $noCache=false): self {
$this->action(function() use ($noCache) {
foreach (array_keys($this->sources) as $data) {
$this->refreshData($data, $noCache);
}
});
return $this;
}
function get($data=null, bool $noCache=false) {
return $this->action(function () use ($data, $noCache) {
return $this->refreshData($data, $noCache);
});
}
function all($data=null, bool $noCache=false): ?iterable {
$data = $this->get($data, $noCache);
if ($data !== null && !is_iterable($data)) $data = [$data];
return $data;
}
function delete($data=null): void {
$source = $this->sources[$data] ?? null;
if ($source !== null) $source->delete();
}
/** obtenir les informations sur le fichier */
function getInfos(): array {
return $this->action(function () {
if (!$this->isValid()) {
return ["valid" => false];
}
$start = $this->start;
$duration = $this->duration;
return [
"valid" => true,
"start" => $start,
"duration" => strval($duration),
"date_start" => $start->format(),
"date_end" => $duration->getDest()->format(),
];
});
}
const UPDATE_SUB = -1, UPDATE_SET = 0, UPDATE_ADD = 1;
/**
* mettre à jour la durée de validité du fichier
*
* XXX UPDATE_SET n'est pas implémenté
*/
function updateDuration($nduration, int $action=self::UPDATE_ADD): void {
if ($this->readonly) return;
$this->action(function () use ($nduration, $action) {
if (!$this->isValid()) return;
$duration = $this->duration;
if ($action < 0) $duration->subDuration($nduration);
elseif ($action > 0) $duration->addDuration($nduration);
}, true);
}
/** supprimer les fichiers s'ils ont expiré */
function deleteExpired(bool $force=false): bool {
if ($this->readonly) return false;
return $this->action(function () use ($force) {
if ($force || $this->shouldUpdate()) {
$this->unlinkFiles();
return true;
}
return false;
}, true);
}
}

View File

@ -1,68 +0,0 @@
<?php
namespace nulib\cache;
use nulib\cl;
/**
* Class CacheManager: un gestionnaire de cache permettant de désactiver la mise
* en cache d'une valeur dans le cadre d'une session.
*
* en effet, si on désactive le cache, il doit être réactivé après que la valeur
* est calculée, pour éviter qu'une valeur soit calculée encore et encore dans
* une session de travail
*/
class CacheManager {
function __construct(?array $includes=null, ?array $excludes=null) {
$this->shouldCaches = [];
$this->defaultCache = true;
$this->includes = $includes;
$this->excludes = $excludes;
}
/**
* @var array tableau {id => shouldCache} indiquant si l'élément id doit être
* mis en cache
*/
protected array $shouldCaches;
/**
* @var bool valeur par défaut de shouldCache si la valeur n'est pas trouvée
* dans $shouldCache
*/
protected bool $defaultCache;
/**
* @var array|null groupes à toujours inclure dans le cache. pour les
* identifiants de ces groupe, {@link self::shouldCache()} retourne toujours
* true.
*
* $excludes est prioritaire par rapport à $includes
*/
protected ?array $includes;
/**
* @var array|null groupes à exclure de la mise en cache. la mise en cache est
* toujours calculée pour les identifiants de ces groupes.
*/
protected ?array $excludes;
function setNoCache(bool $noCache=true, bool $reset=true): self {
if ($reset) $this->shouldCaches = [];
$this->defaultCache = !$noCache;
return $this;
}
function shouldCache(string $id, ?string $groupId=null, bool $reset=true): bool {
if ($groupId !== null) {
$includes = $this->includes;
$shouldInclude = $includes !== null && in_array($groupId, $includes);
$excludes = $this->excludes;
$shouldExclude = $excludes !== null && in_array($groupId, $excludes);
if ($shouldInclude && !$shouldExclude) return true;
}
$cacheId = "$groupId-$id";
$shouldCache = cl::get($this->shouldCaches, $cacheId, $this->defaultCache);
$this->shouldCaches[$cacheId] = $reset?: $shouldCache;
return $shouldCache;
}
}

View File

@ -1,40 +0,0 @@
<?php
namespace nulib\cache;
class CursorCacheData extends CacheData {
function __construct(array $cursorId, $compute=null, ?CursorChannel $channel=null) {
$name = $cursorId["group_id"];
if ($name) $name .= "_";
$name .= $cursorId["id"];
parent::__construct($name, $compute);
$channel ??= (new CursorChannel($cursorId))->initStorage(cache::storage());
$this->channel = $channel;
}
function isExternal(): bool {
return true;
}
function setDatafile(?string $basefile): void {
}
protected CursorChannel $channel;
function exists(): bool {
return $this->channel->count() > 0;
}
function load() {
return $this->channel;
}
function save($data) {
if (!is_iterable($data)) $data = [$data];
$this->channel->rechargeAll($data);
return $this->channel;
}
function delete() {
$this->channel->delete(null);
}
}

View File

@ -1,127 +0,0 @@
<?php
namespace nulib\cache;
use IteratorAggregate;
use nulib\cl;
use nulib\db\CapacitorChannel;
use nulib\db\CapacitorStorage;
use nulib\php\func;
use Traversable;
class CursorChannel extends CapacitorChannel implements IteratorAggregate {
static function with($cursorId=null, ?iterable $rows=null, ?CapacitorStorage $storage=null): self {
$storage ??= cache::storage();
$channel = (new static($cursorId))->initStorage($storage);
if ($rows !== null) $channel->rechargeAll($rows);
return $channel;
}
const NAME = "cursor";
const TABLE_NAME = "cursor";
const COLUMN_DEFINITIONS = [
"group_id_" => "varchar(32) not null", // groupe de curseur
"id_" => "varchar(128) not null", // nom du curseur
"key_index_" => "integer not null",
"key_" => "varchar(128) not null",
"search_" => "varchar(255)",
"primary key (group_id_, id_, key_index_)",
];
const ADD_COLUMNS = null;
protected function COLUMN_DEFINITIONS(): ?array {
return cl::merge(self::COLUMN_DEFINITIONS, static::ADD_COLUMNS);
}
/**
* @param array|string $cursorId
*/
function __construct($cursorId) {
parent::__construct();
cache::verifix_id($cursorId);
[
"group_id" => $this->groupId,
"id" => $this->id,
] = $cursorId;
}
protected string $groupId;
protected string $id;
function getCursorId(): array {
return [
"group_id" => $this->groupId,
"id" => $this->id,
];
}
function getBaseFilter(): ?array {
return [
"group_id_" => $this->groupId,
"id_" => $this->id,
];
}
protected int $index = 0;
protected function getSearch($item): ?string {
$search = cl::filter_n(cl::with($item));
$search = implode(" ", $search);
return substr($search, 0, 255);
}
function getItemValues($item, $key=null): ?array {
$index = $this->index++;
$key = $key ?? $index;
$key = substr(strval($key), 0, 128);
$addColumns = static::ADD_COLUMNS ?? [];
$addColumns = cl::select($item,
array_filter(array_keys($addColumns), function ($key) {
return is_string($key);
}));
return cl::merge($addColumns, [
"group_id_" => $this->groupId,
"id_" => $this->id,
"key_index_" => $index,
"key_" => $key,
"search_" => $this->getSearch($item),
]);
}
function reset(bool $recreate=false): void {
$this->index = 0;
parent::reset($recreate);
}
function chargeAll(?iterable $items, $func=null, ?array $args=null): int {
if ($items === null) return 0;
$count = 0;
if ($func !== null) $func = func::with($func, $args)->bind($this);
foreach ($items as $key => $item) {
$count += $this->charge($item, $func, [$key]);
}
return $count;
}
function rechargeAll(?iterable $items): self {
$this->delete(null);
$this->index = 0;
$this->chargeAll($items);
return $this;
}
function getIterator(): Traversable {
$rows = $this->dbAll([
"cols" => ["key_", "item__"],
"where" => $this->getBaseFilter(),
]);
foreach ($rows as $row) {
$key = $row["key_"];
$item = $this->unserialize($row["item__"]);
yield $key => $item;
}
}
}

View File

@ -1,62 +0,0 @@
<?php
namespace nulib\cache;
use nulib\cl;
use nulib\file;
use nulib\os\path;
use Traversable;
class DataCacheData extends CacheData {
/** @var string identifiant de cette donnée */
const NAME = null;
/** @var callable une fonction permettant de calculer la donnée */
const COMPUTE = null;
function __construct(?string $name=null, $compute=null, ?string $basefile=null) {
$name ??= static::NAME ?? "";
$compute ??= static::COMPUTE;
parent::__construct($name, $compute);
$this->setDatafile($basefile);
}
function compute() {
$data = parent::compute();
if ($data instanceof Traversable) $data = cl::all($data);
return $data;
}
function isExternal(): bool {
return false;
}
protected string $datafile;
function setDatafile(?string $basefile): void {
if ($basefile === null) {
$basedir = ".";
$basename = "";
} else {
$basedir = path::dirname($basefile);
$basename = path::filename($basefile);
}
$this->datafile = "$basedir/.$basename.{$this->name}".cache::EXT;
}
function exists(): bool {
return file_exists($this->datafile);
}
function load() {
return file::reader($this->datafile)->unserialize();
}
function save($data) {
file::writer($this->datafile)->serialize($data);
return $data;
}
function delete(): void {
@unlink($this->datafile);
}
}

View File

@ -1,6 +0,0 @@
# nulib\cache
* [ ] CacheChannel: stocker aussi la clé primaire, ce qui permet de récupérer
la donnée correspondante dans la source?
-*- coding: utf-8 mode: markdown -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8:noeol:binary

View File

@ -1,93 +0,0 @@
<?php
namespace nulib\cache;
use nulib\app\app;
use nulib\db\CapacitorStorage;
use nulib\db\sqlite\SqliteStorage;
use nulib\ext\utils;
class cache {
/** @var string extension des fichiers de cache */
const EXT = ".cache";
protected static ?string $dbfile = null;
protected static function dbfile(): ?string {
return self::$dbfile ??= app::get()->getVarfile("cache.db");
}
protected static ?CapacitorStorage $storage = null;
static function storage(): CapacitorStorage {
return self::$storage ??= new SqliteStorage(self::dbfile());
}
static function set_storage(CapacitorStorage $storage): CapacitorStorage {
return self::$storage = $storage;
}
protected static ?CacheManager $manager = null;
static function manager(): CacheManager {
return self::$manager ??= new CacheManager();
}
static function set_manager(CacheManager $manager): CacheManager {
return self::$manager = $manager;
}
static function nc(bool $noCache=true, bool $reset=false): void {
self::manager()->setNoCache($noCache, $reset);
}
protected static function should_cache(string $id, ?string $groupId=null, bool $reset=true): bool {
return self::manager()->shouldCache($id, $groupId, $reset);
}
static function verifix_id(&$cacheId): void {
$cacheId ??= utils::uuidgen();
if (is_array($cacheId)) {
$keys = array_keys($cacheId);
if (array_key_exists("id", $cacheId)) $idKey = "id";
else $idKey = $keys[0] ?? null;
$id = strval($cacheId[$idKey] ?? "");
if (array_key_exists("group_id", $cacheId)) $groupIdKey = "group_id";
else $groupIdKey = $keys[1] ?? null;
$groupId = strval($cacheId[$groupIdKey] ?? "");
} else {
$id = strval($cacheId);
$groupId = "";
}
# si le groupe ou le nom sont trop grand, en faire un hash
if (strlen($groupId) > 32) $groupId = md5($groupId);
if (strlen($id) > 128) $id = substr($id, 0, 128 - 32).md5($id);
$cacheId = ["group_id" => $groupId, "id" => $id];
}
private static function new(array $cacheId, ?string $suffix, $data, ?array $params=null): CacheFile {
$file = $cacheId["group_id"];
if ($file) $file .= "_";
$file .= $cacheId["id"];
$file .= $suffix;
return new CacheFile($file, $data, $params);
}
static function cache($dataId, $data, ?array $params=null): CacheFile {
self::verifix_id($dataId);
return self::new($dataId, null, $data, $params);
}
static function get($dataId, $data, ?array $params=null) {
self::verifix_id($dataId);
$noCache = !self::should_cache($dataId["id"], $dataId["group_id"]);
$cache = self::new($dataId, null, $data, $params);
return $cache->get(null, $noCache);
}
static function all($cursorId, $rows, ?array $params=null): ?iterable {
self::verifix_id($cursorId);
$noCache = !self::should_cache($cursorId["id"], $cursorId["group_id"]);
$cache = self::new($cursorId, "_rows", new CursorCacheData($cursorId, $rows), $params);
return $cache->get(null, $noCache);
}
}

View File

@ -166,12 +166,6 @@ class cv {
#############################################################################
/** retourner $value si elle est non nulle, lancer une exception sinon */
static final function not_null($value, ?string $kind=null) {
if ($value !== null) return $value;
throw exceptions::null_value($kind);
}
/** vérifier si $value est un booléen, sinon retourner null */
static final function check_bool($value): ?bool {
return is_bool($value)? $value: null;
@ -202,7 +196,7 @@ class cv {
$index = is_int($value)? $value : null;
$key = is_string($value)? $value : null;
if ($index === null && $key === null && $throw_exception) {
throw exceptions::invalid_type($value, $kind, "key", $prefix);
throw ValueException::invalid_kind($value, "key", $prefix);
} else {
return [$index, $key];
}
@ -219,7 +213,7 @@ class cv {
$scalar = !is_bool($value) && is_scalar($value)? $value : null;
$array = is_array($value)? $value : null;
if ($bool === null && $scalar === null && $array === null && $throw_exception) {
throw exceptions::invalid_type($value, $kind, ["bool", "scalar", "array"], $prefix);
throw ValueException::invalid_kind($value, "value", $prefix);
} else {
return [$bool, $scalar, $array];
}

View File

@ -2,215 +2,727 @@
namespace nulib\db;
use nulib\cl;
use nulib\exceptions;
use nulib\cv;
use nulib\db\_private\_migration;
use nulib\php\func;
use nulib\ValueException;
use Traversable;
/**
* Class Capacitor: un objet permettant d'attaquer un canal spécifique d'une
* instance de {@link CapacitorStorage}
* Class Capacitor: objet permettant d'accumuler des données pour les
* réutiliser plus tard
*/
class Capacitor implements ITransactor {
function __construct(CapacitorStorage $storage, CapacitorChannel $channel, bool $ensureExists=true) {
$this->storage = $storage;
$this->channel = $channel;
$this->channel->setCapacitor($this);
if ($ensureExists) $this->ensureExists();
}
/** @var CapacitorStorage */
protected $storage;
function getStorage(): CapacitorStorage {
return $this->storage;
}
function db(): IDatabase {
return $this->getStorage()->db();
}
abstract class Capacitor {
abstract function db(): IDatabase;
function ensureLive(): self {
$this->getStorage()->ensureLive();
$this->db()->ensure();
return $this;
}
/** @var CapacitorChannel */
protected $channel;
function getChannel(): CapacitorChannel {
return $this->channel;
}
function getTableName(): string {
return $this->getChannel()->getTableName();
}
function getCreateSql(): string {
$channel = $this->channel;
return $this->storage->_getMigration($channel)->getSql(get_class($channel), $this->db());
}
/** @var CapacitorChannel[] */
protected ?array $subChannels = null;
protected ?array $subManageTransactions = null;
function willUpdate(...$channels): self {
if ($this->subChannels === null) {
# désactiver la gestion des transaction sur le channel local aussi
$this->subChannels[] = $this->channel;
function newChannel($channel): CapacitorChannel {
if (!($channel instanceof CapacitorChannel)) {
if (!is_array($channel)) $channel = ["name" => $channel];
$channel = new CapacitorChannel($channel);
}
if ($channels) {
foreach ($channels as $channel) {
if ($channel instanceof Capacitor) $channel = $channel->getChannel();
if ($channel instanceof CapacitorChannel) {
$this->subChannels[] = $channel;
} else {
throw exceptions::invalid_type($channel, "channel", CapacitorChannel::class);
}
}
return $channel->initCapacitor($this);
}
const CDATA_DEFINITION = null;
const CSUM_DEFINITION = null;
const CTIMESTAMP_DEFINITION = null;
const GSERIAL_DEFINITION = null;
const GLIC_DEFINITION = null;
const GLIB_DEFINITION = null;
const GTEXT_DEFINITION = null;
const GBOOL_DEFINITION = null;
const GUUID_DEFINITION = null;
protected static function verifix_col($def): string {
if (!is_string($def)) $def = strval($def);
$def = trim($def);
$parts = preg_split('/\s+/', $def, 2);
if (count($parts) == 2) {
$def = $parts[0];
$rest = " $parts[1]";
} else {
$rest = null;
}
return $this;
switch ($def) {
case "serdata":
case "Cdata": $def = static::CDATA_DEFINITION; break;
case "sersum":
case "Csum": $def = static::CSUM_DEFINITION; break;
case "serts":
case "Ctimestamp": $def = static::CTIMESTAMP_DEFINITION; break;
case "genserial":
case "Gserial": $def = static::GSERIAL_DEFINITION; break;
case "genlic":
case "Glic": $def = static::GLIC_DEFINITION; break;
case "genlib":
case "Glib": $def = static::GLIB_DEFINITION; break;
case "gentext":
case "Gtext": $def = static::GTEXT_DEFINITION; break;
case "genbool":
case "Gbool": $def = static::GBOOL_DEFINITION; break;
case "genuuid":
case "Guuid": $def = static::GUUID_DEFINITION; break;
}
return "$def$rest";
}
function inTransaction(): bool {
return $this->db()->inTransaction();
}
const PRIMARY_KEY_DEFINITION = [
"id_" => "Gserial",
];
function beginTransaction(?callable $func=null, bool $commit=true): void {
$db = $this->db();
if ($this->subChannels !== null) {
# on gère des subchannels: ne débuter la transaction que si ce n'est déjà fait
if ($this->subManageTransactions === null) {
foreach ($this->subChannels as $channel) {
$name = $channel->getName();
$this->subManageTransactions ??= [];
if (!array_key_exists($name, $this->subManageTransactions)) {
$this->subManageTransactions[$name] = $channel->isManageTransactions();
const COLUMN_DEFINITIONS = [
"item__" => "Cdata",
"item__sum_" => "Csum",
"created_" => "Ctimestamp",
"modified_" => "Ctimestamp",
];
protected function getColumnDefinitions(CapacitorChannel $channel, bool $ignoreMigrations=false): array {
$definitions = [];
if ($channel->getPrimaryKeys() === null) {
$definitions[] = static::PRIMARY_KEY_DEFINITION;
}
$definitions[] = $channel->getColumnDefinitions();
$definitions[] = static::COLUMN_DEFINITIONS;
# forcer les définitions sans clé à la fin (sqlite requière par exemple que
# primary key (columns) soit à la fin)
$tmp = cl::merge(...$definitions);
$definitions = [];
$constraints = [];
$index = 0;
foreach ($tmp as $col => $def) {
if ($col === $index) {
$index++;
if (is_array($def)) {
if (!$ignoreMigrations) {
$mdefs = $def;
$mindex = 0;
foreach ($mdefs as $mcol => $mdef) {
if ($mcol === $mindex) {
$mindex++;
} else {
if ($mdef) {
$definitions[$mcol] = self::verifix_col($mdef);
} else {
unset($definitions[$mcol]);
}
}
}
}
$channel->setManageTransactions(false);
} else {
$constraints[] = $def;
}
if (!$db->inTransaction()) $db->beginTransaction();
} else {
$definitions[$col] = self::verifix_col($def);
}
} elseif (!$db->inTransaction()) {
}
return cl::merge($definitions, $constraints);
}
/** sérialiser les valeurs qui doivent l'être dans $row */
protected function serialize(CapacitorChannel $channel, ?array $row): ?array {
if ($row === null) return null;
$colDefs = $this->getColumnDefinitions($channel);
$index = 0;
$raw = [];
foreach (array_keys($colDefs) as $col) {
$key = $col;
if ($key === $index) {
$index++;
} elseif ($channel->isSerialCol($key)) {
[$serialCol, $sumCol] = $channel->getSumCols($key);
if (array_key_exists($key, $row)) {
$sum = $channel->getSum($key, $row[$key]);
$raw[$serialCol] = $sum[$serialCol];
if (array_key_exists($sumCol, $colDefs)) {
$raw[$sumCol] = $sum[$sumCol];
}
}
} elseif (array_key_exists($key, $row)) {
$raw[$col] = $row[$key];
}
}
return $raw;
}
/** désérialiser les valeurs qui doivent l'être dans $values */
protected function unserialize(CapacitorChannel $channel, ?array $raw): ?array {
if ($raw === null) return null;
$colDefs = $this->getColumnDefinitions($channel);
$index = 0;
$row = [];
foreach (array_keys($colDefs) as $col) {
$key = $col;
if ($key === $index) {
$index++;
} elseif (!array_key_exists($col, $raw)) {
} elseif ($channel->isSerialCol($key)) {
$value = $raw[$col];
if ($value !== null) $value = $channel->unserialize($value);
$row[$key] = $value;
} else {
$row[$key] = $raw[$col];
}
}
return $row;
}
function getPrimaryKeys(CapacitorChannel $channel): array {
$primaryKeys = $channel->getPrimaryKeys();
if ($primaryKeys === null) $primaryKeys = ["id_"];
return $primaryKeys;
}
function getRowIds(CapacitorChannel $channel, ?array $row, ?array &$primaryKeys=null): ?array {
$primaryKeys = $this->getPrimaryKeys($channel);
$rowIds = cl::select($row, $primaryKeys);
if (cl::all_n($rowIds)) return null;
else return $rowIds;
}
#############################################################################
# Migration et metadata
abstract protected function tableExists(string $tableName): bool;
const METADATA_TABLE = "_metadata";
const METADATA_COLS = [
"name" => "varchar not null primary key",
"value" => "varchar",
];
protected function prepareMetadata(): void {
if (!$this->tableExists(static::METADATA_TABLE)) {
$db = $this->db();
$db->exec([
"create table",
"table" => static::METADATA_TABLE,
"cols" => static::METADATA_COLS,
]);
$db->exec([
"insert",
"into" => static::METADATA_TABLE,
"values" => [
"name" => "version",
"value" => "1",
],
]);
}
}
protected function getCreateChannelSql(CapacitorChannel $channel): array {
return [
"create table if not exists",
"table" => $channel->getTableName(),
"cols" => $this->getColumnDefinitions($channel, true),
];
}
abstract function getMigration(CapacitorChannel $channel): _migration;
#############################################################################
# Catalogue
const CATALOG_TABLE = "_channels";
const CATALOG_COLS = [
"name" => "varchar not null primary key",
"table_name" => "varchar",
"class_name" => "varchar",
];
protected function getCreateCatalogSql(): array {
return [
"create table if not exists",
"table" => static::CATALOG_TABLE,
"cols" => static::CATALOG_COLS,
];
}
protected function addToCatalogSql(CapacitorChannel $channel): array {
return [
"insert",
"into" => static::CATALOG_TABLE,
"values" => [
"name" => $channel->getName(),
"table_name" => $channel->getTableName(),
"class_name" => get_class($channel),
],
];
}
function getCatalog(): iterable {
return $this->db()->all([
"select",
"from" => static::CATALOG_TABLE,
]);
}
function isInCatalog(array $filter, ?array &$raw=null): bool {
$raw = $this->db()->one([
"select",
"from" => static::CATALOG_TABLE,
"where" => $filter,
]);
return $raw !== null;
}
#############################################################################
protected function afterCreate(CapacitorChannel $channel): void {
$db = $this->db();
$db->exec($this->getCreateCatalogSql());
$db->exec($this->addToCatalogSql($channel));
}
function create(CapacitorChannel $channel): void {
$this->prepareMetadata();
$this->getMigration($channel)->migrate($this->db());
$this->afterCreate($channel);
}
function autocreate(CapacitorChannel $channel, bool $force=false): void {
if ($force || !$channel->isCreated()) {
$channel->ensureSetup();
$this->create($channel);
$channel->setCreated();
}
}
/** tester si le canal spécifié existe */
function exists(CapacitorChannel $channel): bool {
return $this->tableExists($channel->getTableName());
}
protected function beforeReset(CapacitorChannel $channel): void {
$db = $this->db;
$name = $channel->getName();
$db->exec([
"delete",
"from" => _migration::MIGRATION_TABLE,
"where" => [
"channel" => $name,
],
]);
$db->exec([
"delete",
"from" => static::CATALOG_TABLE,
"where" => [
"name" => $name,
],
]);
}
/** supprimer le canal spécifié */
function reset(CapacitorChannel $channel, bool $recreate=false): void {
$this->beforeReset($channel);
$this->db()->exec([
"drop table if exists",
$channel->getTableName(),
]);
$channel->setCreated(false);
if ($recreate) $this->autocreate($channel);
}
/**
* charger une valeur dans le canal
*
* Après avoir calculé les valeurs des clés supplémentaires
* avec {@link CapacitorChannel::getItemValues()}, l'une des deux fonctions
* {@link CapacitorChannel::onCreate()} ou {@link CapacitorChannel::onUpdate()}
* est appelée en fonction du type d'opération: création ou mise à jour
*
* Ensuite, si $func !== null, la fonction est appelée avec la signature de
* {@link CapacitorChannel::onCreate()} ou {@link CapacitorChannel::onUpdate()}
* en fonction du type d'opération: création ou mise à jour
*
* Dans les deux cas, si la fonction retourne un tableau, il est utilisé pour
* modifier les valeurs insérées/mises à jour. De plus, $row obtient la
* valeur finale des données insérées/mises à jour
*
* Si $args est renseigné, il est ajouté aux arguments utilisés pour appeler
* les méthodes {@link CapacitorChannel::getItemValues()},
* {@link CapacitorChannel::onCreate()} et/ou
* {@link CapacitorChannel::onUpdate()}
*
* @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(CapacitorChannel $channel, $item, $func=null, ?array $args=null, ?array &$row=null): int {
$channel->initCapacitor($this);
$tableName = $channel->getTableName();
$db = $this->db();
$args ??= [];
$row = func::call([$channel, "getItemValues"], $item, ...$args);
if ($row === [false]) return 0;
if ($row !== null && array_key_exists("item", $row)) {
$item = A::pop($row, "item");
}
$raw = cl::merge(
$channel->getSum("item", $item),
$this->serialize($channel, $row));
$praw = null;
$rowIds = $this->getRowIds($channel, $raw, $primaryKeys);
if ($rowIds !== null) {
# modification
$praw = $db->one([
"select",
"from" => $tableName,
"where" => $rowIds,
]);
}
$now = date("Y-m-d H:i:s");
$insert = null;
if ($praw === null) {
# création
$raw = cl::merge($raw, [
"created_" => $now,
"modified_" => $now,
]);
$insert = true;
$initFunc = func::with([$channel, "onCreate"], $args);
$row = $this->unserialize($channel, $raw);
$prow = null;
} else {
# modification
# intégrer autant que possible les valeurs de praw dans raw, de façon que
# l'utilisateur puisse voir clairement ce qui a été modifié
if ($channel->_wasSumModified("item", $raw, $praw)) {
$insert = false;
$raw = cl::merge($praw, $raw, [
"modified_" => $now,
]);
} else {
$raw = cl::merge($praw, $raw);
}
$initFunc = func::with([$channel, "onUpdate"], $args);
$row = $this->unserialize($channel, $raw);
$prow = $this->unserialize($channel, $praw);
}
$updates = $initFunc->prependArgs([$item, $row, $prow])->invoke();
if ($updates === [false]) return 0;
if (is_array($updates) && $updates) {
if ($insert === null) $insert = false;
if (!array_key_exists("modified_", $updates)) {
$updates["modified_"] = $now;
}
$row = cl::merge($row, $updates);
$raw = cl::merge($raw, $this->serialize($channel, $updates));
}
if ($func !== null) {
$updates = func::with($func, $args)
->prependArgs([$item, $row, $prow])
->bind($channel)
->invoke();
if ($updates === [false]) return 0;
if (is_array($updates) && $updates) {
if ($insert === null) $insert = false;
if (!array_key_exists("modified_", $updates)) {
$updates["modified_"] = $now;
}
$row = cl::merge($row, $updates);
$raw = cl::merge($raw, $this->serialize($channel, $updates));
}
}
# aucune modification
if ($insert === null) return 0;
# si on est déjà dans une transaction, désactiver la gestion des transactions
$manageTransactions = $channel->isManageTransactions() && !$db->inTransaction();
if ($manageTransactions) {
$commited = false;
$db->beginTransaction();
}
if ($func !== null) {
$commited = false;
try {
func::call($func, $this);
if ($commit) {
$this->commit();
$commited = true;
$nbModified = 0;
try {
if ($insert) {
$id = $db->exec([
"insert",
"into" => $tableName,
"values" => $raw,
]);
if (count($primaryKeys) == 1 && $rowIds === null) {
# mettre à jour avec l'id généré
$row[$primaryKeys[0]] = $id;
}
$nbModified = 1;
} else {
# calculer ce qui a changé pour ne mettre à jour que le nécessaire
$updates = [];
foreach ($raw as $col => $value) {
if (array_key_exists($col, $rowIds)) {
# ne jamais mettre à jour la clé primaire
continue;
}
if (!cv::equals($value, $praw[$col] ?? null)) {
$updates[$col] = $value;
}
}
if (count($updates) == 1 && array_key_first($updates) == "modified_") {
# si l'unique modification porte sur la date de modification, alors
# la ligne n'est pas modifiée. ce cas se présente quand on altère la
# valeur de $item
$updates = null;
}
if ($updates) {
$db->exec([
"update",
"table" => $tableName,
"values" => $updates,
"where" => $rowIds,
]);
$nbModified = 1;
}
} finally {
if ($commit && !$commited) $this->rollback();
}
if ($manageTransactions) {
$db->commit();
$commited = true;
}
return $nbModified;
} finally {
if ($manageTransactions && !$commited) $db->rollback();
}
}
protected function beforeEndTransaction(): void {
if ($this->subManageTransactions !== null) {
foreach ($this->subChannels as $channel) {
$name = $channel->getName();
$channel->setManageTransactions($this->subManageTransactions[$name]);
/**
* décharger les données du canal spécifié. seul la valeur de $item est
* fournie
*/
function discharge(CapacitorChannel $channel, bool $reset=true): Traversable {
$channel->initCapacitor($this);
$raws = $this->db()->all([
"select item__",
"from" => $channel->getTableName(),
]);
foreach ($raws as $raw) {
yield unserialize($raw['item__']);
}
if ($reset) $this->reset($channel);
}
protected function convertValue2row(CapacitorChannel $channel, array $filter, array $cols): array {
$index = 0;
$fixed = [];
foreach ($filter as $key => $value) {
if ($key === $index) {
$index++;
if (is_array($value)) {
$value = $this->convertValue2row($channel, $value, $cols);
}
$fixed[] = $value;
} else {
$col = "${key}__";
if (array_key_exists($col, $cols)) {
# colonne sérialisée
$fixed[$col] = $channel->serialize($value);
} else {
$fixed[$key] = $value;
}
}
$this->subManageTransactions = null;
}
return $fixed;
}
protected function verifixFilter(CapacitorChannel $channel, &$filter): void {
if ($filter !== null && !is_array($filter)) {
$primaryKeys = $this->getPrimaryKeys($channel);
$id = $filter;
$channel->verifixId($id);
$filter = [$primaryKeys[0] => $id];
}
$cols = $this->getColumnDefinitions($channel);
if ($filter !== null) {
$filter = $this->convertValue2row($channel, $filter, $cols);
}
}
function commit(): void {
$this->beforeEndTransaction();
/** indiquer le nombre d'éléments du canal spécifié */
function count(CapacitorChannel $channel, $filter): int {
$channel->initCapacitor($this);
$this->verifixFilter($channel, $filter);
return $this->db()->get([
"select count(*)",
"from" => $channel->getTableName(),
"where" => $filter,
]);
}
/**
* obtenir la ligne correspondant au filtre sur le canal spécifié
*
* si $filter n'est pas un tableau, il est transformé en ["id_" => $filter]
*/
function one(CapacitorChannel $channel, $filter, ?array $mergeQuery=null): ?array {
$channel->initCapacitor($this);
$this->verifixFilter($channel, $filter);
$raw = $this->db()->one(cl::merge([
"select",
"from" => $channel->getTableName(),
"where" => $filter,
], $mergeQuery));
return $this->unserialize($channel, $raw);
}
/**
* obtenir les lignes correspondant au filtre sur le canal spécifié
*
* si $filter n'est pas un tableau, il est transformé en ["id_" => $filter]
*/
function all(CapacitorChannel $channel, $filter, ?array $mergeQuery=null): Traversable {
$channel->initCapacitor($this);
$this->verifixFilter($channel, $filter);
$raws = $this->db()->all(cl::merge([
"select",
"from" => $channel->getTableName(),
"where" => $filter,
], $mergeQuery), null, $this->getPrimaryKeys($channel));
foreach ($raws as $key => $raw) {
yield $key => $this->unserialize($channel, $raw);
}
}
/**
* 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 de {@link CapacitorChannel::onEach()}
* si la fonction retourne un tableau, il est utilisé pour mettre à jour la
* ligne
*
* @param int $nbUpdated reçoit le nombre de lignes mises à jour
* @return int le nombre de lignes parcourues
*/
function each(CapacitorChannel $channel, $filter, $func=null, ?array $args=null, ?array $mergeQuery=null, ?int &$nbUpdated=null): int {
$channel->initCapacitor($this);
if ($func === null) $func = CapacitorChannel::onEach;
$onEach = func::with($func)->bind($channel);
$db = $this->db();
if ($db->inTransaction()) $db->commit();
}
function rollback(): void {
$this->beforeEndTransaction();
$db = $this->db();
if ($db->inTransaction()) $db->rollback();
}
function exists(): bool {
return $this->storage->_exists($this->channel);
}
function ensureExists(): void {
$this->storage->_ensureExists($this->channel);
}
function reset(bool $recreate=false): void {
$this->storage->_reset($this->channel, $recreate);
}
function charge($item, $func=null, ?array $args=null, ?array &$row=null): int {
if ($this->subChannels !== null) $this->beginTransaction();
return $this->storage->_charge($this->channel, $item, $func, $args, $row);
}
function chargeAll(?iterable $items, $func=null, ?array $args=null): int {
# si on est déjà dans une transaction, désactiver la gestion des transactions
$manageTransactions = $channel->isManageTransactions() && !$db->inTransaction();
if ($manageTransactions) {
$commited = false;
$db->beginTransaction();
$commitThreshold = $channel->getEachCommitThreshold();
}
$count = 0;
if ($items !== null) {
if ($func !== null) {
$func = func::with($func, $args)->bind($this->channel);
$nbUpdated = 0;
$tableName = $channel->getTableName();
try {
$args ??= [];
$rows = $this->all($channel, $filter, $mergeQuery);
foreach ($rows as $row) {
$rowIds = $this->getRowIds($channel, $row);
$updates = $onEach->invoke([$row, ...$args]);
if ($updates === [false]) {
break;
} elseif ($updates !== null) {
if (!array_key_exists("modified_", $updates)) {
$updates["modified_"] = date("Y-m-d H:i:s");
}
$nbUpdated += $db->exec([
"update",
"table" => $tableName,
"values" => $this->serialize($channel, $updates),
"where" => $rowIds,
]);
if ($manageTransactions && $commitThreshold !== null) {
$commitThreshold--;
if ($commitThreshold <= 0) {
$db->commit();
$db->beginTransaction();
$commitThreshold = $channel->getEachCommitThreshold();
}
}
}
$count++;
}
foreach ($items as $item) {
$count += $this->charge($item, $func);
if ($manageTransactions) {
$db->commit();
$commited = true;
}
return $count;
} finally {
if ($manageTransactions && !$commited) $db->rollback();
}
return $count;
}
function discharge(bool $reset=true): Traversable {
return $this->storage->_discharge($this->channel, $reset);
/**
* supprimer tous les éléments correspondant au filtre et pour lesquels la
* fonction retourne une valeur vraie si elle est spécifiée
*
* $filter permet de filtrer parmi les élements chargés
*
* $func est appelé avec la signature de {@link CapacitorChannel::onDelete()}
* si la fonction retourne un tableau, il est utilisé pour mettre à jour la
* ligne
*
* @return int le nombre de lignes parcourues
*/
function delete(CapacitorChannel $channel, $filter, $func=null, ?array $args=null): int {
$channel->initCapacitor($this);
if ($func === null) $func = CapacitorChannel::onDelete;
$onDelete = func::with($func)->bind($channel);
$db = $this->db();
# si on est déjà dans une transaction, désactiver la gestion des transactions
$manageTransactions = $channel->isManageTransactions() && !$db->inTransaction();
if ($manageTransactions) {
$commited = false;
$db->beginTransaction();
$commitThreshold = $channel->getEachCommitThreshold();
}
$count = 0;
$tableName = $channel->getTableName();
try {
$args ??= [];
$rows = $this->all($channel, $filter);
foreach ($rows as $row) {
$rowIds = $this->getRowIds($channel, $row);
$shouldDelete = boolval($onDelete->invoke([$row, ...$args]));
if ($shouldDelete) {
$db->exec([
"delete",
"from" => $tableName,
"where" => $rowIds,
]);
if ($manageTransactions && $commitThreshold !== null) {
$commitThreshold--;
if ($commitThreshold <= 0) {
$db->commit();
$db->beginTransaction();
$commitThreshold = $channel->getEachCommitThreshold();
}
}
}
$count++;
}
if ($manageTransactions) {
$db->commit();
$commited = true;
}
return $count;
} finally {
if ($manageTransactions && !$commited) $db->rollback();
}
}
function count($filter=null): int {
return $this->storage->_count($this->channel, $filter);
}
function one($filter, ?array $mergeQuery=null): ?array {
return $this->storage->_one($this->channel, $filter, $mergeQuery);
}
function all($filter, ?array $mergeQuery=null): Traversable {
return $this->storage->_all($this->channel, $filter, $mergeQuery);
}
function each($filter, $func=null, ?array $args=null, ?array $mergeQuery=null, ?int &$nbUpdated=null): int {
if ($this->subChannels !== null) $this->beginTransaction();
return $this->storage->_each($this->channel, $filter, $func, $args, $mergeQuery, $nbUpdated);
}
function delete($filter, $func=null, ?array $args=null): int {
if ($this->subChannels !== null) $this->beginTransaction();
return $this->storage->_delete($this->channel, $filter, $func, $args);
}
function dbAll(array $query, ?array $params=null): iterable {
$primaryKeys = $this->channel->getPrimaryKeys();
return $this->storage->db()->all(cl::merge([
"select",
"from" => $this->getTableName(),
], $query), $params, $primaryKeys);
}
function dbOne(array $query, ?array $params=null): ?array {
return $this->storage->db()->one(cl::merge([
"select",
"from" => $this->getTableName(),
], $query), $params);
}
/** @return int|false */
function dbUpdate(array $query, ?array $params=null) {
return $this->storage->db()->exec(cl::merge([
"update",
"table" => $this->getTableName(),
], $query), $params);
}
function close(): void {
$this->storage->close();
}
abstract function close(): void;
}

View File

@ -1,18 +1,23 @@
<?php
namespace nulib\db;
use nulib\app\app;
use nulib\cl;
use nulib\php\func;
use nulib\str;
use nulib\ValueException;
use Traversable;
/**
* Class CapacitorChannel: un canal d'une instance de {@link ICapacitor}
* Class CapacitorChannel: un canal de données
*/
class CapacitorChannel implements ITransactor {
const NAME = null;
const TABLE_NAME = null;
const AUTOCREATE = null;
protected function COLUMN_DEFINITIONS(): ?array {
return static::COLUMN_DEFINITIONS;
} const COLUMN_DEFINITIONS = null;
@ -50,16 +55,20 @@ class CapacitorChannel implements ITransactor {
return $eachCommitThreshold;
}
function __construct(?string $name=null, ?int $eachCommitThreshold=null, ?bool $manageTransactions=null) {
$name ??= static::NAME;
$tableName ??= static::TABLE_NAME;
function __construct(?array $params=null) {
$this->capacitor = null;
$name = $params["name"] ?? static::NAME;
$tableName = $params["tableName"] ?? static::TABLE_NAME;
self::verifix_name($name, $tableName);
$this->name = $name;
$this->tableName = $tableName;
$this->manageTransactions = $manageTransactions ?? static::MANAGE_TRANSACTIONS;
$this->eachCommitThreshold = self::verifix_eachCommitThreshold($eachCommitThreshold);
$autocreate = $params["autocreate"] ?? null;
$autocreate ??= !app::get()->isProductionMode();
$this->created = !$autocreate;
$this->setup = false;
$this->created = false;
$columnDefinitions = $this->COLUMN_DEFINITIONS();
$primaryKeys = cl::withn(static::PRIMARY_KEYS);
$migration = cl::withn(static::MIGRATION);
@ -117,6 +126,13 @@ class CapacitorChannel implements ITransactor {
$this->columnDefinitions = $columnDefinitions;
$this->primaryKeys = $primaryKeys;
$this->migration = $migration;
$manageTransactions = $params["manageTransactions"] ?? static::MANAGE_TRANSACTIONS;
$this->manageTransactions = $manageTransactions;
$eachCommitThreshold = $params["eachCommitThreshold"] ?? null;
$eachCommitThreshold = self::verifix_eachCommitThreshold($eachCommitThreshold);
$this->eachCommitThreshold = $eachCommitThreshold;
}
protected string $name;
@ -131,40 +147,6 @@ class CapacitorChannel implements ITransactor {
return $this->tableName;
}
/**
* @var bool indiquer si les modifications de each doivent être gérées dans
* une transaction. si false, l'utilisateur doit lui même gérer la
* transaction.
*/
protected bool $manageTransactions;
function isManageTransactions(): bool {
return $this->manageTransactions;
}
function setManageTransactions(bool $manageTransactions=true): self {
$this->manageTransactions = $manageTransactions;
return $this;
}
/**
* @var ?int nombre maximum de modifications dans une transaction avant un
* commit automatique dans {@link Capacitor::each()}. Utiliser null pour
* désactiver la fonctionnalité.
*
* ce paramètre n'a d'effet que si $manageTransactions==true
*/
protected ?int $eachCommitThreshold;
function getEachCommitThreshold(): ?int {
return $this->eachCommitThreshold;
}
function setEachCommitThreshold(?int $eachCommitThreshold=null): self {
$this->eachCommitThreshold = self::verifix_eachCommitThreshold($eachCommitThreshold);
return $this;
}
/**
* initialiser ce channel avant sa première utilisation.
*/
@ -319,6 +301,40 @@ class CapacitorChannel implements ITransactor {
return $sum !== $psum;
}
/**
* @var bool indiquer si les modifications de each doivent être gérées dans
* une transaction. si false, l'utilisateur doit lui même gérer la
* transaction.
*/
protected bool $manageTransactions;
function isManageTransactions(): bool {
return $this->manageTransactions;
}
function setManageTransactions(bool $manageTransactions=true): self {
$this->manageTransactions = $manageTransactions;
return $this;
}
/**
* @var ?int nombre maximum de modifications dans une transaction avant un
* commit automatique dans {@link Capacitor::each()}. Utiliser null pour
* désactiver la fonctionnalité.
*
* ce paramètre n'a d'effet que si $manageTransactions==true
*/
protected ?int $eachCommitThreshold;
function getEachCommitThreshold(): ?int {
return $this->eachCommitThreshold;
}
function setEachCommitThreshold(?int $eachCommitThreshold=null): self {
$this->eachCommitThreshold = self::verifix_eachCommitThreshold($eachCommitThreshold);
return $this;
}
/**
* méthode appelée lors du chargement avec {@link Capacitor::charge()} pour
* créer un nouvel élément
@ -400,24 +416,20 @@ class CapacitorChannel implements ITransactor {
#############################################################################
# Méthodes déléguées pour des workflows centrés sur le channel
/**
* @var Capacitor|null instance de Capacitor par laquelle cette instance est
* utilisée
*/
protected ?Capacitor $capacitor;
function getCapacitor(): ?Capacitor {
return $this->capacitor;
}
function setCapacitor(Capacitor $capacitor): self {
$this->capacitor = $capacitor;
function initCapacitor(Capacitor $capacitor, bool $autocreate=true): self {
if ($this->capacitor === null) $this->capacitor = $capacitor;
if ($autocreate) $this->capacitor->autocreate($this);
return $this;
}
function initStorage(CapacitorStorage $storage): self {
new Capacitor($storage, $this);
return $this;
function db(): IDatabase {
return $this->capacitor->db();
}
function ensureLive(): self {
@ -425,52 +437,117 @@ class CapacitorChannel implements ITransactor {
return $this;
}
function willUpdate(...$transactors): ITransactor {
return $this->capacitor->willUpdate(...$transactors);
function getCreateSql(): string {
return $this->capacitor->getMigration($this)->getSql(get_class($this), $this->db());
}
/** @var CapacitorChannel[] */
protected ?array $subChannels = null;
protected ?array $subManageTransactions = null;
function willUpdate(...$channels): self {
if ($this->subChannels === null) {
# désactiver la gestion des transaction sur le channel local aussi
$this->subChannels[] = $this;
}
if ($channels) {
foreach ($channels as $channel) {
if ($channel instanceof CapacitorChannel) {
$this->subChannels[] = $channel;
} else {
throw ValueException::invalid_type($channel, CapacitorChannel::class);
}
}
}
return $this;
}
function inTransaction(): bool {
return $this->capacitor->inTransaction();
return $this->db()->inTransaction();
}
function beginTransaction(?callable $func=null, bool $commit=true): void {
$this->capacitor->beginTransaction($func, $commit);
$db = $this->db();
if ($this->subChannels !== null) {
# on gère des subchannels: ne débuter la transaction que si ce n'est déjà fait
if ($this->subManageTransactions === null) {
foreach ($this->subChannels as $channel) {
$name = $channel->getName();
$this->subManageTransactions ??= [];
if (!array_key_exists($name, $this->subManageTransactions)) {
$this->subManageTransactions[$name] = $channel->isManageTransactions();
}
$channel->setManageTransactions(false);
}
if (!$db->inTransaction()) $db->beginTransaction();
}
} elseif (!$db->inTransaction()) {
$db->beginTransaction();
}
if ($func !== null) {
$commited = false;
try {
func::call($func, $this);
if ($commit) {
$this->commit();
$commited = true;
}
} finally {
if ($commit && !$commited) $this->rollback();
}
}
}
protected function beforeEndTransaction(): void {
if ($this->subManageTransactions !== null) {
foreach ($this->subChannels as $channel) {
$name = $channel->getName();
$channel->setManageTransactions($this->subManageTransactions[$name]);
}
$this->subManageTransactions = null;
}
}
function commit(): void {
$this->capacitor->commit();
$this->beforeEndTransaction();
$db = $this->db();
if ($db->inTransaction()) $db->commit();
}
function rollback(): void {
$this->capacitor->rollback();
}
function db(): IDatabase {
return $this->capacitor->getStorage()->db();
$this->beforeEndTransaction();
$db = $this->db();
if ($db->inTransaction()) $db->rollback();
}
function exists(): bool {
return $this->capacitor->exists();
}
function ensureExists(): void {
$this->capacitor->ensureExists();
return $this->capacitor->exists($this);
}
function reset(bool $recreate=false): void {
$this->capacitor->reset($recreate);
$this->capacitor->reset($this, $recreate);
}
function charge($item, $func=null, ?array $args=null, ?array &$row=null): int {
return $this->capacitor->charge($item, $func, $args, $row);
return $this->capacitor->charge($this, $item, $func, $args, $row);
}
function chargeAll(?iterable $items, $func=null, ?array $args=null): int {
return $this->capacitor->chargeAll($items, $func, $args);
$count = 0;
if ($items !== null) {
if ($func !== null) {
$func = func::with($func, $args)->bind($this);
}
foreach ($items as $item) {
$count += $this->charge($item, $func);
}
}
return $count;
}
function discharge(bool $reset=true): Traversable {
return $this->capacitor->discharge($reset);
return $this->capacitor->discharge($this, $reset);
}
/**
@ -496,40 +573,50 @@ class CapacitorChannel implements ITransactor {
function count($filter=null): int {
$this->verifixFilter($filter);
return $this->capacitor->count($filter);
return $this->capacitor->count($this, $filter);
}
function one($filter, ?array $mergeQuery=null): ?array {
$this->verifixFilter($filter);
return $this->capacitor->one($filter, $mergeQuery);
return $this->capacitor->one($this, $filter, $mergeQuery);
}
function all($filter, ?array $mergeQuery=null): Traversable {
$this->verifixFilter($filter);
return $this->capacitor->all($filter, $mergeQuery);
return $this->capacitor->all($this, $filter, $mergeQuery);
}
function each($filter, $func=null, ?array $args=null, ?array $mergeQuery=null, ?int &$nbUpdated=null): int {
$this->verifixFilter($filter);
return $this->capacitor->each($filter, $func, $args, $mergeQuery, $nbUpdated);
return $this->capacitor->each($this, $filter, $func, $args, $mergeQuery, $nbUpdated);
}
function delete($filter, $func=null, ?array $args=null): int {
$this->verifixFilter($filter);
return $this->capacitor->delete($filter, $func, $args);
return $this->capacitor->delete($this, $filter, $func, $args);
}
function dbAll(array $query, ?array $params=null): iterable {
return $this->capacitor->dbAll($query, $params);
$primaryKeys = $this->getPrimaryKeys();
return $this->capacitor->db()->all(cl::merge([
"select",
"from" => $this->getTableName(),
], $query), $params, $primaryKeys);
}
function dbOne(array $query, ?array $params=null): ?array {
return $this->capacitor->dbOne($query, $params);
return $this->capacitor->db()->one(cl::merge([
"select",
"from" => $this->getTableName(),
], $query), $params);
}
/** @return int|false */
function dbUpdate(array $query, ?array $params=null) {
return $this->capacitor->dbUpdate($query, $params);
return $this->capacitor->db()->exec(cl::merge([
"update",
"table" => $this->getTableName(),
], $query), $params);
}
function close(): void {

View File

@ -1,770 +0,0 @@
<?php
namespace nulib\db;
use nulib\A;
use nulib\cl;
use nulib\cv;
use nulib\db\_private\_migration;
use nulib\exceptions;
use nulib\php\func;
use Traversable;
/**
* Class CapacitorStorage: objet permettant d'accumuler des données pour les
* réutiliser plus tard
*/
abstract class CapacitorStorage {
abstract function db(): IDatabase;
function ensureLive(): self {
$this->db()->ensureLive();
return $this;
}
/** @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 {
CapacitorChannel::verifix_name($name);
$channel = $this->channels[$name] ?? null;
if ($channel === null) {
$channel = $this->addChannel(new CapacitorChannel($name));
}
return $channel;
}
const PRIMARY_KEY_DEFINITION = [
"id_" => "genserial",
];
# les définitions sont par défaut pour MariaDB/MySQL
const SERDATA_DEFINITION = "mediumtext";
const SERSUM_DEFINITION = "varchar(40)";
const SERTS_DEFINITION = "datetime";
const GENSERIAL_DEFINITION = "integer primary key auto_increment";
const GENLIC_DEFINITION = "varchar(80)";
const GENLIB_DEFINITION = "varchar(255)";
const GENTEXT_DEFINITION = "mediumtext";
const GENBOOL_DEFINITION = "integer(1) default 0";
const GENUUID_DEFINITION = "varchar(36)";
protected static function gencol($def): string {
if (!is_string($def)) $def = strval($def);
$def = trim($def);
$parts = preg_split('/\s+/', $def, 2);
if (count($parts) == 2) {
$def = $parts[0];
$rest = " $parts[1]";
} else {
$rest = null;
}
switch ($def) {
case "serdata": $def = static::SERDATA_DEFINITION; break;
case "sersum": $def = static::SERSUM_DEFINITION; break;
case "serts": $def = static::SERTS_DEFINITION; break;
case "genserial": $def = static::GENSERIAL_DEFINITION; break;
case "genlic": $def = static::GENLIC_DEFINITION; break;
case "genlib": $def = static::GENLIB_DEFINITION; break;
case "gentext": $def = static::GENTEXT_DEFINITION; break;
case "genbool": $def = static::GENBOOL_DEFINITION; break;
case "genuuid": $def = static::GENUUID_DEFINITION; break;
}
return "$def$rest";
}
const COLUMN_DEFINITIONS = [
"item__" => "serdata",
"item__sum_" => "sersum",
"created_" => "serts",
"modified_" => "serts",
];
protected function ColumnDefinitions(CapacitorChannel $channel, bool $ignoreMigrations=false): array {
$definitions = [];
if ($channel->getPrimaryKeys() === null) {
$definitions[] = static::PRIMARY_KEY_DEFINITION;
}
$definitions[] = $channel->getColumnDefinitions();
$definitions[] = static::COLUMN_DEFINITIONS;
# forcer les définitions sans clé à la fin (sqlite requière par exemple que
# primary key (columns) soit à la fin)
$tmp = cl::merge(...$definitions);
$definitions = [];
$constraints = [];
$index = 0;
foreach ($tmp as $col => $def) {
if ($col === $index) {
$index++;
if (is_array($def)) {
if (!$ignoreMigrations) {
$mdefs = $def;
$mindex = 0;
foreach ($mdefs as $mcol => $mdef) {
if ($mcol === $mindex) {
$mindex++;
} else {
if ($mdef) {
$definitions[$mcol] = self::gencol($mdef);
} else {
unset($definitions[$mcol]);
}
}
}
}
} else {
$constraints[] = $def;
}
} else {
$definitions[$col] = self::gencol($def);
}
}
return cl::merge($definitions, $constraints);
}
protected function getMigration(CapacitorChannel $channel): ?array {
return $channel->getMigration($this->db()->getPrefix());
}
/** sérialiser les valeurs qui doivent l'être dans $row */
protected function serialize(CapacitorChannel $channel, ?array $row): ?array {
if ($row === null) return null;
$cols = $this->ColumnDefinitions($channel);
$index = 0;
$raw = [];
foreach (array_keys($cols) as $col) {
$key = $col;
if ($key === $index) {
$index++;
} elseif ($channel->isSerialCol($key)) {
[$serialCol, $sumCol] = $channel->getSumCols($key);
if (array_key_exists($key, $row)) {
$sum = $channel->getSum($key, $row[$key]);
$raw[$serialCol] = $sum[$serialCol];
if (array_key_exists($sumCol, $cols)) {
$raw[$sumCol] = $sum[$sumCol];
}
}
} elseif (array_key_exists($key, $row)) {
$raw[$col] = $row[$key];
}
}
return $raw;
}
/** désérialiser les valeurs qui doivent l'être dans $values */
protected function unserialize(CapacitorChannel $channel, ?array $raw): ?array {
if ($raw === null) return null;
$cols = $this->ColumnDefinitions($channel);
$index = 0;
$row = [];
foreach (array_keys($cols) as $col) {
$key = $col;
if ($key === $index) {
$index++;
} elseif (!array_key_exists($col, $raw)) {
} elseif ($channel->isSerialCol($key)) {
$value = $raw[$col];
if ($value !== null) $value = $channel->unserialize($value);
$row[$key] = $value;
} else {
$row[$key] = $raw[$col];
}
}
return $row;
}
function getPrimaryKeys(CapacitorChannel $channel): array {
$primaryKeys = $channel->getPrimaryKeys();
if ($primaryKeys === null) $primaryKeys = ["id_"];
return $primaryKeys;
}
function getRowIds(CapacitorChannel $channel, ?array $row, ?array &$primaryKeys=null): ?array {
$primaryKeys = $this->getPrimaryKeys($channel);
$rowIds = cl::select($row, $primaryKeys);
if (cl::all_n($rowIds)) return null;
else return $rowIds;
}
protected function _createSql(CapacitorChannel $channel): array {
return [
"create table if not exists",
"table" => $channel->getTableName(),
"cols" => $this->ColumnDefinitions($channel, true),
];
}
abstract protected function tableExists(string $tableName): bool;
const METADATA_TABLE = "_metadata";
const METADATA_COLS = [
"name" => "varchar not null primary key",
"value" => "varchar",
];
protected function _prepareMetadata(): void {
if (!$this->tableExists(static::METADATA_TABLE)) {
$db = $this->db();
$db->exec([
"drop table if exists",
"table" => self::CHANNELS_TABLE,
]);
$db->exec([
"drop table if exists",
"table" => _migration::MIGRATION_TABLE,
]);
$db->exec([
"create table",
"table" => static::METADATA_TABLE,
"cols" => static::METADATA_COLS,
]);
$db->exec([
"insert",
"into" => static::METADATA_TABLE,
"values" => [
"name" => "version",
"value" => "1",
],
]);
}
}
abstract function _getMigration(CapacitorChannel $channel): _migration;
const CHANNELS_TABLE = "_channels";
const CHANNELS_COLS = [
"name" => "varchar not null primary key",
"table_name" => "varchar",
"class_name" => "varchar",
];
function channelExists(string $name, ?array &$raw=null): bool {
$raw = $this->db()->one([
"select",
"from" => static::CHANNELS_TABLE,
"where" => ["name" => $name],
]);
return $raw !== null;
}
function getChannels(): iterable {
return $this->db()->all([
"select",
"from" => static::CHANNELS_TABLE,
]);
}
protected function _createChannelsSql(): array {
return [
"create table if not exists",
"table" => static::CHANNELS_TABLE,
"cols" => static::CHANNELS_COLS,
];
}
protected function _addToChannelsSql(CapacitorChannel $channel): array {
return [
"insert",
"into" => static::CHANNELS_TABLE,
"values" => [
"name" => $channel->getName(),
"table_name" => $channel->getTableName(),
"class_name" => get_class($channel),
],
];
}
protected function _afterCreate(CapacitorChannel $channel): void {
$db = $this->db();
$db->exec($this->_createChannelsSql());
$db->exec($this->_addToChannelsSql($channel));
}
protected function _create(CapacitorChannel $channel): void {
$channel->ensureSetup();
if (!$channel->isCreated()) {
$this->_prepareMetadata();
$this->_getMigration($channel)->migrate($this->db());
$this->_afterCreate($channel);
$channel->setCreated();
}
}
/** tester si le canal spécifié existe */
function _exists(CapacitorChannel $channel): bool {
return $this->tableExists($channel->getTableName());
}
function exists(?string $channel): bool {
return $this->_exists($this->getChannel($channel));
}
/** s'assurer que le canal spécifié existe */
function _ensureExists(CapacitorChannel $channel): void {
$this->_create($channel);
}
function ensureExists(?string $channel): void {
$this->_ensureExists($this->getChannel($channel));
}
protected function _beforeReset(CapacitorChannel $channel): void {
$db = $this->db;
$name = $channel->getName();
$db->exec([
"delete",
"from" => _migration::MIGRATION_TABLE,
"where" => [
"channel" => $name,
],
]);
$db->exec([
"delete",
"from" => static::CHANNELS_TABLE,
"where" => [
"name" => $name,
],
]);
}
/** supprimer le canal spécifié */
function _reset(CapacitorChannel $channel, bool $recreate=false): void {
$this->_beforeReset($channel);
$this->db()->exec([
"drop table if exists",
$channel->getTableName(),
]);
$channel->setCreated(false);
if ($recreate) $this->_ensureExists($channel);
}
function reset(?string $channel, bool $recreate=false): void {
$this->_reset($this->getChannel($channel), $recreate);
}
/**
* charger une valeur dans le canal
*
* Après avoir calculé les valeurs des clés supplémentaires
* avec {@link CapacitorChannel::getItemValues()}, l'une des deux fonctions
* {@link CapacitorChannel::onCreate()} ou {@link CapacitorChannel::onUpdate()}
* est appelée en fonction du type d'opération: création ou mise à jour
*
* Ensuite, si $func !== null, la fonction est appelée avec la signature de
* {@link CapacitorChannel::onCreate()} ou {@link CapacitorChannel::onUpdate()}
* en fonction du type d'opération: création ou mise à jour
*
* Dans les deux cas, si la fonction retourne un tableau, il est utilisé pour
* modifier les valeurs insérées/mises à jour. De plus, $row obtient la
* valeur finale des données insérées/mises à jour
*
* Si $args est renseigné, il est ajouté aux arguments utilisés pour appeler
* les méthodes {@link CapacitorChannel::getItemValues()},
* {@link CapacitorChannel::onCreate()} et/ou
* {@link CapacitorChannel::onUpdate()}
*
* @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(CapacitorChannel $channel, $item, $func, ?array $args, ?array &$row=null): int {
$this->_create($channel);
$tableName = $channel->getTableName();
$db = $this->db();
$args ??= [];
$row = func::call([$channel, "getItemValues"], $item, ...$args);
if ($row === [false]) return 0;
if ($row !== null && array_key_exists("item", $row)) {
$item = A::pop($row, "item");
}
$raw = cl::merge(
$channel->getSum("item", $item),
$this->serialize($channel, $row));
$praw = null;
$rowIds = $this->getRowIds($channel, $raw, $primaryKeys);
if ($rowIds !== null) {
# modification
$praw = $db->one([
"select",
"from" => $tableName,
"where" => $rowIds,
]);
}
$now = date("Y-m-d H:i:s");
$insert = null;
if ($praw === null) {
# création
$raw = cl::merge($raw, [
"created_" => $now,
"modified_" => $now,
]);
$insert = true;
$initFunc = func::with([$channel, "onCreate"], $args);
$row = $this->unserialize($channel, $raw);
$prow = null;
} else {
# modification
# intégrer autant que possible les valeurs de praw dans raw, de façon que
# l'utilisateur puisse voir clairement ce qui a été modifié
if ($channel->_wasSumModified("item", $raw, $praw)) {
$insert = false;
$raw = cl::merge($praw, $raw, [
"modified_" => $now,
]);
} else {
$raw = cl::merge($praw, $raw);
}
$initFunc = func::with([$channel, "onUpdate"], $args);
$row = $this->unserialize($channel, $raw);
$prow = $this->unserialize($channel, $praw);
}
$updates = $initFunc->prependArgs([$item, $row, $prow])->invoke();
if ($updates === [false]) return 0;
if (is_array($updates) && $updates) {
if ($insert === null) $insert = false;
if (!array_key_exists("modified_", $updates)) {
$updates["modified_"] = $now;
}
$row = cl::merge($row, $updates);
$raw = cl::merge($raw, $this->serialize($channel, $updates));
}
if ($func !== null) {
$updates = func::with($func, $args)
->prependArgs([$item, $row, $prow])
->bind($channel)
->invoke();
if ($updates === [false]) return 0;
if (is_array($updates) && $updates) {
if ($insert === null) $insert = false;
if (!array_key_exists("modified_", $updates)) {
$updates["modified_"] = $now;
}
$row = cl::merge($row, $updates);
$raw = cl::merge($raw, $this->serialize($channel, $updates));
}
}
# aucune modification
if ($insert === null) return 0;
# si on est déjà dans une transaction, désactiver la gestion des transactions
$manageTransactions = $channel->isManageTransactions() && !$db->inTransaction();
if ($manageTransactions) {
$commited = false;
$db->beginTransaction();
}
$nbModified = 0;
try {
if ($insert) {
$id = $db->exec([
"insert",
"into" => $tableName,
"values" => $raw,
]);
if (count($primaryKeys) == 1 && $rowIds === null) {
# mettre à jour avec l'id généré
$row[$primaryKeys[0]] = $id;
}
$nbModified = 1;
} else {
# calculer ce qui a changé pour ne mettre à jour que le nécessaire
$updates = [];
foreach ($raw as $col => $value) {
if (array_key_exists($col, $rowIds)) {
# ne jamais mettre à jour la clé primaire
continue;
}
if (!cv::equals($value, $praw[$col] ?? null)) {
$updates[$col] = $value;
}
}
if (count($updates) == 1 && array_key_first($updates) == "modified_") {
# si l'unique modification porte sur la date de modification, alors
# la ligne n'est pas modifiée. ce cas se présente quand on altère la
# valeur de $item
$updates = null;
}
if ($updates) {
$db->exec([
"update",
"table" => $tableName,
"values" => $updates,
"where" => $rowIds,
]);
$nbModified = 1;
}
}
if ($manageTransactions) {
$db->commit();
$commited = true;
}
return $nbModified;
} finally {
if ($manageTransactions && !$commited) $db->rollback();
}
}
function charge(?string $channel, $item, $func=null, ?array $args=null, ?array &$row=null): int {
return $this->_charge($this->getChannel($channel), $item, $func, $args, $row);
}
/**
* décharger les données du canal spécifié. seul la valeur de $item est
* fournie
*/
function _discharge(CapacitorChannel $channel, bool $reset=true): Traversable {
$this->_create($channel);
$raws = $this->db()->all([
"select item__",
"from" => $channel->getTableName(),
]);
foreach ($raws as $raw) {
yield unserialize($raw['item__']);
}
if ($reset) $this->_reset($channel);
}
function discharge(?string $channel, bool $reset=true): Traversable {
return $this->_discharge($this->getChannel($channel), $reset);
}
protected function _convertValue2row(CapacitorChannel $channel, array $filter, array $cols): array {
$index = 0;
$fixed = [];
foreach ($filter as $key => $value) {
if ($key === $index) {
$index++;
if (is_array($value)) {
$value = $this->_convertValue2row($channel, $value, $cols);
}
$fixed[] = $value;
} else {
$col = "${key}__";
if (array_key_exists($col, $cols)) {
# colonne sérialisée
$fixed[$col] = $channel->serialize($value);
} else {
$fixed[$key] = $value;
}
}
}
return $fixed;
}
protected function verifixFilter(CapacitorChannel $channel, &$filter): void {
if ($filter !== null && !is_array($filter)) {
$primaryKeys = $this->getPrimaryKeys($channel);
$id = $filter;
$channel->verifixId($id);
$filter = [$primaryKeys[0] => $id];
}
$cols = $this->ColumnDefinitions($channel);
if ($filter !== null) {
$filter = $this->_convertValue2row($channel, $filter, $cols);
}
}
/** indiquer le nombre d'éléments du canal spécifié */
function _count(CapacitorChannel $channel, $filter): int {
$this->_create($channel);
$this->verifixFilter($channel, $filter);
return $this->db()->get([
"select count(*)",
"from" => $channel->getTableName(),
"where" => $filter,
]);
}
function count(?string $channel, $filter=null): int {
return $this->_count($this->getChannel($channel), $filter);
}
/**
* obtenir la ligne correspondant au filtre sur le canal spécifié
*
* si $filter n'est pas un tableau, il est transformé en ["id_" => $filter]
*/
function _one(CapacitorChannel $channel, $filter, ?array $mergeQuery=null): ?array {
if ($filter === null) throw exceptions::null_value("filter");
$this->_create($channel);
$this->verifixFilter($channel, $filter);
$raw = $this->db()->one(cl::merge([
"select",
"from" => $channel->getTableName(),
"where" => $filter,
], $mergeQuery));
return $this->unserialize($channel, $raw);
}
function one(?string $channel, $filter, ?array $mergeQuery=null): ?array {
return $this->_one($this->getChannel($channel), $filter, $mergeQuery);
}
/**
* obtenir les lignes correspondant au filtre sur le canal spécifié
*
* si $filter n'est pas un tableau, il est transformé en ["id_" => $filter]
*/
function _all(CapacitorChannel $channel, $filter, ?array $mergeQuery=null): Traversable {
$this->_create($channel);
$this->verifixFilter($channel, $filter);
$raws = $this->db()->all(cl::merge([
"select",
"from" => $channel->getTableName(),
"where" => $filter,
], $mergeQuery), null, $this->getPrimaryKeys($channel));
foreach ($raws as $key => $raw) {
yield $key => $this->unserialize($channel, $raw);
}
}
function all(?string $channel, $filter, $mergeQuery=null): Traversable {
return $this->_all($this->getChannel($channel), $filter, $mergeQuery);
}
/**
* 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 de {@link CapacitorChannel::onEach()}
* si la fonction retourne un tableau, il est utilisé pour mettre à jour la
* ligne
*
* @param int $nbUpdated reçoit le nombre de lignes mises à jour
* @return int le nombre de lignes parcourues
*/
function _each(CapacitorChannel $channel, $filter, $func, ?array $args, ?array $mergeQuery=null, ?int &$nbUpdated=null): int {
$this->_create($channel);
if ($func === null) $func = CapacitorChannel::onEach;
$onEach = func::with($func)->bind($channel);
$db = $this->db();
# si on est déjà dans une transaction, désactiver la gestion des transactions
$manageTransactions = $channel->isManageTransactions() && !$db->inTransaction();
if ($manageTransactions) {
$commited = false;
$db->beginTransaction();
$commitThreshold = $channel->getEachCommitThreshold();
}
$count = 0;
$nbUpdated = 0;
$tableName = $channel->getTableName();
try {
$args ??= [];
$rows = $this->_all($channel, $filter, $mergeQuery);
foreach ($rows as $row) {
$rowIds = $this->getRowIds($channel, $row);
$updates = $onEach->invoke([$row, ...$args]);
if ($updates === [false]) {
break;
} elseif ($updates !== null) {
if (!array_key_exists("modified_", $updates)) {
$updates["modified_"] = date("Y-m-d H:i:s");
}
$nbUpdated += $db->exec([
"update",
"table" => $tableName,
"values" => $this->serialize($channel, $updates),
"where" => $rowIds,
]);
if ($manageTransactions && $commitThreshold !== null) {
$commitThreshold--;
if ($commitThreshold <= 0) {
$db->commit();
$db->beginTransaction();
$commitThreshold = $channel->getEachCommitThreshold();
}
}
}
$count++;
}
if ($manageTransactions) {
$db->commit();
$commited = true;
}
return $count;
} finally {
if ($manageTransactions && !$commited) $db->rollback();
}
}
function each(?string $channel, $filter, $func=null, ?array $args=null, ?array $mergeQuery=null, ?int &$nbUpdated=null): int {
return $this->_each($this->getChannel($channel), $filter, $func, $args, $mergeQuery, $nbUpdated);
}
/**
* supprimer tous les éléments correspondant au filtre et pour lesquels la
* fonction retourne une valeur vraie si elle est spécifiée
*
* $filter permet de filtrer parmi les élements chargés
*
* $func est appelé avec la signature de {@link CapacitorChannel::onDelete()}
* si la fonction retourne un tableau, il est utilisé pour mettre à jour la
* ligne
*
* @return int le nombre de lignes parcourues
*/
function _delete(CapacitorChannel $channel, $filter, $func, ?array $args): int {
$this->_create($channel);
if ($func === null) $func = CapacitorChannel::onDelete;
$onDelete = func::with($func)->bind($channel);
$db = $this->db();
# si on est déjà dans une transaction, désactiver la gestion des transactions
$manageTransactions = $channel->isManageTransactions() && !$db->inTransaction();
if ($manageTransactions) {
$commited = false;
$db->beginTransaction();
$commitThreshold = $channel->getEachCommitThreshold();
}
$count = 0;
$tableName = $channel->getTableName();
try {
$args ??= [];
$rows = $this->_all($channel, $filter);
foreach ($rows as $row) {
$rowIds = $this->getRowIds($channel, $row);
$shouldDelete = boolval($onDelete->invoke([$row, ...$args]));
if ($shouldDelete) {
$db->exec([
"delete",
"from" => $tableName,
"where" => $rowIds,
]);
if ($manageTransactions && $commitThreshold !== null) {
$commitThreshold--;
if ($commitThreshold <= 0) {
$db->commit();
$db->beginTransaction();
$commitThreshold = $channel->getEachCommitThreshold();
}
}
}
$count++;
}
if ($manageTransactions) {
$db->commit();
$commited = true;
}
return $count;
} finally {
if ($manageTransactions && !$commited) $db->rollback();
}
}
function delete(?string $channel, $filter, $func=null, ?array $args=null): int {
return $this->_delete($this->getChannel($channel), $filter, $func, $args);
}
abstract function close(): void;
}

View File

@ -17,7 +17,7 @@ interface IDatabase extends ITransactor {
* transactions en cours sont perdues. cette méthode est donc prévue pour
* vérifier la validité de la connexion avant de lancer une transaction
*/
function ensureLive(): self;
function ensure(): self;
/**
* - si c'est un insert, retourner l'identifiant autogénéré de la ligne

View File

@ -1,5 +1,11 @@
# db
# db/Capacitor
La source peut être un iterable
---
charge() permet de spécifier la clé associée avec la valeur chargée, et
discharge() retourne les valeurs avec la clé primaire

View File

@ -1,14 +1,14 @@
<?php
namespace nulib\db\_private;
use nulib\exceptions;
use nulib\ValueException;
abstract class _base extends _common {
protected static function verifix(&$sql, ?array &$bindings=null, ?array &$meta=null): void {
if (is_array($sql)) {
$prefix = $sql[0] ?? null;
if ($prefix === null) {
throw exceptions::invalid_type($sql, "cette requête sql");
throw new ValueException("requête invalide");
} elseif (_create::isa($prefix)) {
$sql = _create::parse($sql, $bindings);
$meta = ["isa" => "create", "type" => "ddl"];
@ -28,7 +28,7 @@ abstract class _base extends _common {
$sql = _generic::parse($sql, $bindings);
$meta = ["isa" => "generic", "type" => null];
} else {
throw exceptions::invalid_type($sql, "cette requête sql");
throw ValueException::invalid_kind($sql, "query");
}
} else {
if (!is_string($sql)) $sql = strval($sql);

View File

@ -2,8 +2,8 @@
namespace nulib\db\_private;
use nulib\cl;
use nulib\exceptions;
use nulib\str;
use nulib\ValueException;
class _common {
protected static function consume(string $pattern, string &$string, ?array &$ms=null): bool {
@ -249,7 +249,7 @@ class _common {
protected static function check_eof(string $tmpsql, string $usersql): void {
self::consume(';\s*', $tmpsql);
if ($tmpsql) {
throw exceptions::invalid_value($usersql, "cette requête sql");
throw new ValueException("unexpected value at end: $usersql");
}
}
}

View File

@ -2,7 +2,7 @@
namespace nulib\db\_private;
use nulib\cl;
use nulib\exceptions;
use nulib\ValueException;
class _insert extends _common {
const SCHEMA = [
@ -44,7 +44,7 @@ class _insert extends _common {
} elseif ($into !== null) {
$sql[] = $into;
} else {
throw exceptions::invalid_value($usersql, "cette requête sql", "il faut spécifier la table");
throw new ValueException("expected table name: $usersql");
}
## cols & values

View File

@ -2,8 +2,8 @@
namespace nulib\db\_private;
use nulib\cl;
use nulib\exceptions;
use nulib\str;
use nulib\ValueException;
class _select extends _common {
const SCHEMA = [
@ -101,7 +101,7 @@ class _select extends _common {
$sql[] = "from";
$sql[] = $from;
} else {
throw exceptions::invalid_value($usersql, "cette requête sql", "il faut spécifier la table");
throw new ValueException("expected table name: $usersql");
}
## where

View File

@ -25,14 +25,11 @@ class conds {
/**
* retourner une condition "like" si la valeur s'y prête
*
* - si la valeur fait moins de $likeThreshold caractères, faire une recherche
* exacte en retournant ["=", $value]
*
*
* - les espaces sont remplacés par %
*
* si $partial
* - si $partial et que $value ne contient pas d'espaces, rajouter un % à la
* fin
*/
static function like($value, bool $partial=false, ?int $likeThreshold=null) {
if ($value === false || $value === null) return $value;

View File

@ -6,22 +6,11 @@ use nulib\db\pdo\Pdo;
class Mysql extends Pdo {
const PREFIX = "mysql";
static function config_setTimeout(self $pdo): void {
$pdo->_exec("SET session wait_timeout=28800");
$pdo->_exec("SET session interactive_timeout=28800");
}
const CONFIG_setTimeout = [self::class, "config_setTimeout"];
static function config_unbufferedQueries(self $mysql): void {
$mysql->db->setAttribute(\PDO::MYSQL_ATTR_USE_BUFFERED_QUERY, false);
}
const CONFIG_unbufferedQueries = [self::class, "config_unbufferedQueries"];
const DEFAULT_CONFIG = [
...parent::DEFAULT_CONFIG,
self::CONFIG_setTimeout,
];
function getDbname(): ?string {
$url = $this->dbconn["name"] ?? null;
if ($url !== null && preg_match('/^mysql(?::|.*;)dbname=([^;]+)/i', $url, $ms)) {

View File

@ -3,12 +3,22 @@ namespace nulib\db\mysql;
use nulib\cl;
use nulib\db\CapacitorChannel;
use nulib\db\CapacitorStorage;
use nulib\db\Capacitor;
/**
* Class MysqlStorage
*/
class MysqlStorage extends CapacitorStorage {
class MysqlCapacitor extends Capacitor {
const CDATA_DEFINITION = "mediumtext";
const CSUM_DEFINITION = "varchar(40)";
const CTIMESTAMP_DEFINITION = "datetime";
const GSERIAL_DEFINITION = "integer primary key auto_increment";
const GLIC_DEFINITION = "varchar(80)";
const GLIB_DEFINITION = "varchar(255)";
const GTEXT_DEFINITION = "mediumtext";
const GBOOL_DEFINITION = "integer(1) default 0";
const GUUID_DEFINITION = "varchar(36)";
function __construct($mysql) {
$this->db = Mysql::with($mysql);
}
@ -36,21 +46,21 @@ class MysqlStorage extends CapacitorStorage {
"value" => "varchar(255)",
];
function _getMigration(CapacitorChannel $channel): _mysqlMigration {
function getMigration(CapacitorChannel $channel): _mysqlMigration {
$migrations = cl::merge([
"0init" => [$this->_createSql($channel)],
"0init" => [$this->getCreateChannelSql($channel)],
], $channel->getMigration($this->db->getPrefix()));
return new _mysqlMigration($migrations, $channel->getName());
}
const CHANNELS_COLS = [
const CATALOG_COLS = [
"name" => "varchar(255) not null primary key",
"table_name" => "varchar(64)",
"class_name" => "varchar(255)",
];
protected function _addToChannelsSql(CapacitorChannel $channel): array {
return cl::merge(parent::_addToChannelsSql($channel), [
protected function addToCatalogSql(CapacitorChannel $channel): array {
return cl::merge(parent::addToCatalogSql($channel), [
"suffix" => "on duplicate key update name = name",
]);
}

View File

@ -6,8 +6,8 @@ use nulib\db\_private\_config;
use nulib\db\_private\Tvalues;
use nulib\db\IDatabase;
use nulib\db\ITransactor;
use nulib\exceptions;
use nulib\php\func;
use nulib\ValueException;
class Pdo implements IDatabase {
use Tvalues;
@ -28,7 +28,6 @@ class Pdo implements IDatabase {
"options" => $pdo->options,
"config" => $pdo->config,
"migration" => $pdo->migration,
"autocheck" => $pdo->autocheck,
], $params));
} else {
return new static($pdo, $params);
@ -42,7 +41,7 @@ class Pdo implements IDatabase {
const CONFIG_errmodeException_lowerCase = [self::class, "config_errmodeException_lowerCase"];
protected const OPTIONS = [
\PDO::ATTR_PERSISTENT => false,
\PDO::ATTR_PERSISTENT => true,
];
protected const DEFAULT_CONFIG = [
@ -53,10 +52,6 @@ class Pdo implements IDatabase {
protected const MIGRATION = null;
protected const AUTOCHECK = true;
protected const AUTOOPEN = true;
const dbconn_SCHEMA = [
"name" => "string",
"user" => "?string",
@ -69,8 +64,7 @@ class Pdo implements IDatabase {
"replace_config" => ["?array|callable"],
"config" => ["?array|callable"],
"migration" => ["?array|string|callable"],
"autocheck" => ["bool", self::AUTOCHECK],
"autoopen" => ["bool", self::AUTOOPEN],
"auto_open" => ["bool", true],
];
function __construct($dbconn=null, ?array $params=null) {
@ -102,8 +96,8 @@ class Pdo implements IDatabase {
# migrations
$this->migration = $params["migration"] ?? static::MIGRATION;
#
$this->autocheck = $params["autocheck"] ?? static::AUTOCHECK;
if ($params["autoopen"] ?? static::AUTOOPEN) {
$defaultAutoOpen = self::params_SCHEMA["auto_open"][1];
if ($params["auto_open"] ?? $defaultAutoOpen) {
$this->open();
}
}
@ -119,8 +113,6 @@ class Pdo implements IDatabase {
/** @var array|string|callable */
protected $migration;
protected bool $autocheck;
protected ?\PDO $db = null;
function getSql($query, ?array $params=null): string {
@ -171,7 +163,7 @@ class Pdo implements IDatabase {
const SQL_CHECK_LIVE = "select 1";
function ensureLive(): self {
function ensure(): self {
try {
$this->_query(static::SQL_CHECK_LIVE);
} catch (\PDOException $e) {
@ -203,7 +195,7 @@ class Pdo implements IDatabase {
$this->transactors[] = $transactor;
$transactor->willUpdate();
} else {
throw exceptions::invalid_type($transactor, "transactor", ITransactor::class);
throw ValueException::invalid_type($transactor, ITransactor::class);
}
}
return $this;
@ -214,9 +206,6 @@ class Pdo implements IDatabase {
}
function beginTransaction(?callable $func=null, bool $commit=true): void {
# s'assurer que la connexion à la BDD est active avant de commencer une
# transaction
if ($this->autocheck) $this->ensureLive();
$this->db()->beginTransaction();
if ($this->transactors !== null) {
foreach ($this->transactors as $transactor) {

View File

@ -6,8 +6,8 @@ use nulib\db\_private\_config;
use nulib\db\_private\Tvalues;
use nulib\db\IDatabase;
use nulib\db\ITransactor;
use nulib\exceptions;
use nulib\php\func;
use nulib\ValueException;
class Pgsql implements IDatabase {
use Tvalues;
@ -34,6 +34,7 @@ class Pgsql implements IDatabase {
}
}
protected const OPTIONS = [
# XXX désactiver les connexions persistantes par défaut
# pour réactiver par défaut, il faudrait vérifier la connexion à chaque fois
@ -48,18 +49,13 @@ class Pgsql implements IDatabase {
const MIGRATION = null;
protected const AUTOCHECK = true;
protected const AUTOOPEN = true;
const params_SCHEMA = [
"dbconn" => ["array"],
"options" => ["?array|callable"],
"replace_config" => ["?array|callable"],
"config" => ["?array|callable"],
"migration" => ["?array|string|callable"],
"autocheck" => ["bool", self::AUTOCHECK],
"autoopen" => ["bool", self::AUTOOPEN],
"auto_open" => ["bool", true],
];
const dbconn_SCHEMA = [
@ -117,8 +113,8 @@ class Pgsql implements IDatabase {
# migrations
$this->migration = $params["migration"] ?? static::MIGRATION;
#
$this->autocheck = $params["autocheck"] ?? static::AUTOCHECK;
if ($params["autoopen"] ?? static::AUTOOPEN) {
$defaultAutoOpen = self::params_SCHEMA["auto_open"][1];
if ($params["auto_open"] ?? $defaultAutoOpen) {
$this->open();
}
}
@ -134,8 +130,6 @@ class Pgsql implements IDatabase {
/** @var array|string|callable */
protected $migration;
protected bool $autocheck;
/** @var resource */
protected $db = null;
@ -215,7 +209,7 @@ class Pgsql implements IDatabase {
const SQL_CHECK_LIVE = "select 1";
function ensureLive(): self {
function ensure(): self {
try {
$this->_query(static::SQL_CHECK_LIVE);
} catch (\PDOException $e) {
@ -253,7 +247,7 @@ class Pgsql implements IDatabase {
$this->transactors[] = $transactor;
$transactor->willUpdate();
} else {
throw exceptions::invalid_type($transactor, "transactor", ITransactor::class);
throw ValueException::invalid_type($transactor, ITransactor::class);
}
}
return $this;
@ -273,9 +267,6 @@ class Pgsql implements IDatabase {
}
function beginTransaction(?callable $func=null, bool $commit=true): void {
# s'assurer que la connexion à la BDD est active avant de commencer une
# transaction
if ($this->autocheck) $this->ensureLive();
$this->_exec("begin");
if ($this->transactors !== null) {
foreach ($this->transactors as $transactor) {

View File

@ -3,16 +3,18 @@ namespace nulib\db\pgsql;
use nulib\cl;
use nulib\db\CapacitorChannel;
use nulib\db\CapacitorStorage;
use nulib\db\Capacitor;
class PgsqlStorage extends CapacitorStorage {
const SERDATA_DEFINITION = "text";
const SERSUM_DEFINITION = "varchar(40)";
const SERTS_DEFINITION = "timestamp";
const GENSERIAL_DEFINITION = "serial primary key";
const GENTEXT_DEFINITION = "text";
const GENBOOL_DEFINITION = "boolean default false";
const GENUUID_DEFINITION = "uuid";
class PgsqlCapacitor extends Capacitor {
const CDATA_DEFINITION = "text";
const CSUM_DEFINITION = "varchar(40)";
const CTIMESTAMP_DEFINITION = "timestamp";
const GSERIAL_DEFINITION = "serial primary key";
const GLIC_DEFINITION = "varchar(80)";
const GLIB_DEFINITION = "varchar(255)";
const GTEXT_DEFINITION = "text";
const GBOOL_DEFINITION = "boolean default false";
const GUUID_DEFINITION = "uuid";
function __construct($pgsql) {
$this->db = Pgsql::with($pgsql);
@ -41,15 +43,15 @@ class PgsqlStorage extends CapacitorStorage {
return $found !== null;
}
function _getMigration(CapacitorChannel $channel): _pgsqlMigration {
function getMigration(CapacitorChannel $channel): _pgsqlMigration {
$migrations = cl::merge([
"0init" => [$this->_createSql($channel)],
"0init" => [$this->getCreateChannelSql($channel)],
], $channel->getMigration($this->db->getPrefix()));
return new _pgsqlMigration($migrations, $channel->getName());
}
protected function _addToChannelsSql(CapacitorChannel $channel): array {
return cl::merge(parent::_addToChannelsSql($channel), [
protected function addToCatalogSql(CapacitorChannel $channel): array {
return cl::merge(parent::addToCatalogSql($channel), [
"suffix" => "on conflict (name) do nothing",
]);
}

View File

@ -7,8 +7,8 @@ use nulib\db\_private\_config;
use nulib\db\_private\Tvalues;
use nulib\db\IDatabase;
use nulib\db\ITransactor;
use nulib\exceptions;
use nulib\php\func;
use nulib\ValueException;
use SQLite3;
use SQLite3Result;
use SQLite3Stmt;
@ -80,10 +80,6 @@ class Sqlite implements IDatabase {
const MIGRATION = null;
protected const AUTOCHECK = true;
protected const AUTOOPEN = true;
const params_SCHEMA = [
"file" => ["string", ""],
"flags" => ["int", SQLITE3_OPEN_READWRITE + SQLITE3_OPEN_CREATE],
@ -92,8 +88,7 @@ class Sqlite implements IDatabase {
"replace_config" => ["?array|callable"],
"config" => ["?array|callable"],
"migration" => ["?array|string|callable"],
"autocheck" => ["bool", self::AUTOCHECK],
"autoopen" => ["bool", self::AUTOOPEN],
"auto_open" => ["bool", true],
];
function __construct(?string $file=null, ?array $params=null) {
@ -122,9 +117,9 @@ class Sqlite implements IDatabase {
# migrations
$this->migration = $params["migration"] ?? static::MIGRATION;
#
$defaultAutoOpen = self::params_SCHEMA["auto_open"][1];
$this->inTransaction = false;
$this->autocheck = $params["autocheck"] ?? static::AUTOCHECK;
if ($params["autoopen"] ?? static::AUTOOPEN) {
if ($params["auto_open"] ?? $defaultAutoOpen) {
$this->open();
}
}
@ -152,8 +147,6 @@ class Sqlite implements IDatabase {
/** @var array|string|callable */
protected $migration;
protected bool $autocheck;
/** @var SQLite3 */
protected $db;
@ -215,7 +208,7 @@ class Sqlite implements IDatabase {
const SQL_CHECK_LIVE = "select 1";
function ensureLive(): self {
function ensure(): self {
try {
$this->_query(static::SQL_CHECK_LIVE);
} catch (\PDOException $e) {
@ -254,7 +247,7 @@ class Sqlite implements IDatabase {
$this->transactors[] = $transactor;
$transactor->willUpdate();
} else {
throw exceptions::invalid_type($transactor, "transactor", ITransactor::class);
throw ValueException::invalid_type($transactor, ITransactor::class);
}
}
return $this;
@ -266,9 +259,6 @@ class Sqlite implements IDatabase {
}
function beginTransaction(?callable $func=null, bool $commit=true): void {
# s'assurer que la connexion à la BDD est active avant de commencer une
# transaction
if ($this->autocheck) $this->ensureLive();
$this->db()->exec("begin");
$this->inTransaction = true;
if ($this->transactors !== null) {

View File

@ -3,13 +3,21 @@ namespace nulib\db\sqlite;
use nulib\cl;
use nulib\db\CapacitorChannel;
use nulib\db\CapacitorStorage;
use nulib\db\Capacitor;
/**
* Class SqliteStorage
*/
class SqliteStorage extends CapacitorStorage {
const GENSERIAL_DEFINITION = "integer primary key autoincrement";
class SqliteCapacitor extends Capacitor {
const CDATA_DEFINITION = "mediumtext";
const CSUM_DEFINITION = "varchar(40)";
const CTIMESTAMP_DEFINITION = "datetime";
const GSERIAL_DEFINITION = "integer primary key autoincrement";
const GLIC_DEFINITION = "varchar(80)";
const GLIB_DEFINITION = "varchar(255)";
const GTEXT_DEFINITION = "mediumtext";
const GBOOL_DEFINITION = "integer(1) default 0";
const GUUID_DEFINITION = "varchar(36)";
function __construct($sqlite) {
$this->db = Sqlite::with($sqlite);
@ -31,30 +39,30 @@ class SqliteStorage extends CapacitorStorage {
return $found !== null;
}
function _getMigration(CapacitorChannel $channel): _sqliteMigration {
function getMigration(CapacitorChannel $channel): _sqliteMigration {
$migrations = cl::merge([
"0init" => [$this->_createSql($channel)],
"0init" => [$this->getCreateChannelSql($channel)],
], $channel->getMigration($this->db->getPrefix()));
return new _sqliteMigration($migrations, $channel->getName());
}
protected function _addToChannelsSql(CapacitorChannel $channel): array {
$sql = parent::_addToChannelsSql($channel);
protected function addToCatalogSql(CapacitorChannel $channel): array {
$sql = parent::addToCatalogSql($channel);
$sql[0] = "insert or ignore";
return $sql;
}
protected function _afterCreate(CapacitorChannel $channel): void {
protected function afterCreate(CapacitorChannel $channel): void {
$db = $this->db;
if (!$this->tableExists(static::CHANNELS_TABLE)) {
if (!$this->tableExists(static::CATALOG_TABLE)) {
# ne pas créer si la table existe déjà, pour éviter d'avoir besoin d'un
# verrou en écriture
$db->exec($this->_createChannelsSql());
$db->exec($this->getCreateCatalogSql());
}
if (!$this->channelExists($channel->getName())) {
if (!$this->isInCatalog(["name" => $channel->getName()])) {
# ne pas insérer si la ligne existe déjà, pour éviter d'avoir besoin d'un
# verrou en écriture
$db->exec($this->_addToChannelsSql($channel));
$db->exec($this->addToCatalogSql($channel));
}
}

View File

@ -1,253 +0,0 @@
<?php
namespace nulib;
use nulib\php\content\c;
use nulib\text\Word;
use Throwable;
/**
* Class exceptions: répertoire d'exceptions normalisées
*/
class exceptions {
/** @param Throwable|ExceptionShadow $e */
public static function get_user_message($e): ?string {
if ($e instanceof UserException) $userMessage = $e->getUserMessage();
elseif ($e instanceof ExceptionShadow) $userMessage = $e->getUserMessage();
else return null;
if ($userMessage === null) return null;
else return c::to_string($userMessage);
}
/** @param Throwable|ExceptionShadow $e */
public static function get_tech_message($e): ?string {
if ($e instanceof UserException) $techMessage = $e->getTechMessage();
elseif ($e instanceof ExceptionShadow) $techMessage = $e->getTechMessage();
else return null;
if ($techMessage === null) return null;
else return c::to_string($techMessage);
}
/** @param Throwable|ExceptionShadow $e */
public static function get_message($e): string {
if ($e instanceof UserException) $userMessage = $e->getUserMessage();
elseif ($e instanceof ExceptionShadow) $userMessage = $e->getUserMessage();
else return $e->getMessage();
return c::to_string($userMessage);
}
/** @param Throwable|ExceptionShadow $e */
public static final function get_summary($e, bool $includePrevious = true): string {
$parts = [];
$first = true;
while ($e !== null) {
$message = self::get_message($e);
if (!$message) $message = "(no message)";
$techMessage = self::get_tech_message($e);
if ($techMessage) $message .= " |$techMessage|";
if ($first) $first = false;
else $parts[] = ", caused by ";
if ($e instanceof ExceptionShadow) $class = $e->getClass();
else $class = get_class($e);
$parts[] = "$class: $message";
$e = $includePrevious ? $e->getPrevious() : null;
}
return implode("", $parts);
}
/** @param Throwable|ExceptionShadow $e */
public static final function get_traceback($e): string {
$tbs = [];
$previous = false;
while ($e !== null) {
if (!$previous) {
$efile = $e->getFile();
$eline = $e->getLine();
$tbs[] = "at $efile($eline)";
} else {
$tbs[] = "~~ caused by: " . self::get_summary($e, false);
}
$tbs[] = $e->getTraceAsString();
$e = $e->getPrevious();
$previous = true;
#XXX il faudrait ne pas réinclure les lignes communes aux exceptions qui
# ont déjà été affichées
}
return implode("\n", $tbs);
}
#############################################################################
const EXCEPTION = ValueException::class;
const WORD = "la valeur#s";
protected static Word $word;
protected static function word(): Word {
return self::$word ??= new Word(static::WORD);
}
static function value($value): string {
if (is_object($value)) {
return "<".get_class($value).">";
} elseif (is_array($value)) {
$values = $value;
$parts = [];
$index = 0;
foreach ($values as $key => $value) {
if ($key === $index) {
$index++;
$parts[] = self::value($value);
} else {
$parts[] = "$key=>".self::value($value);
}
}
return "[".implode(", ", $parts)."]";
} elseif (is_string($value)) {
return $value;
} else {
return var_export($value, true);
}
}
static function generic($value, ?string $kind, ?string $cause, ?string $reason=null, ?Throwable $previous=null): UserException {
$msg = "";
if ($value !== null) {
$msg .= self::value($value);
$msg .= ": ";
}
$kind ??= self::word()->_ce();
$msg .= $kind;
$cause ??= "est invalide";
if ($cause) $msg .= " $cause";
if ($reason) $msg .= ": $reason";
$code = $previous !== null? $previous->getCode(): 0;
$class = static::EXCEPTION;
return new $class($msg, $code, $previous);
}
/**
* indiquer qu'une valeur est invalide pour une raison générique
*/
static function invalid_value($value, ?string $kind=null, ?string $reason=null, ?Throwable $previous=null): UserException {
return self::generic($value, $kind, null, $reason, $previous);
}
/**
* spécialisation de {@link self::invalid_value()} qui permet d'indiquer les
* types attendus
*/
static function invalid_type($value, ?string $kind=null, $expectedTypes=null, ?Throwable $previous=null): UserException {
if ($kind !== null) $pronom = "il";
else $pronom = self::word()->pronom();
$expectedTypes = cl::withn($expectedTypes);
if (!$expectedTypes) {
$reason = null;
} elseif (count($expectedTypes) == 1) {
$reason = "$pronom doit être du type suivant: ";
} else {
$reason = "$pronom doit être d'un des types suivants: ";
}
$reason .= implode(", ", $expectedTypes);
return self::invalid_value($value, $kind, $reason, $previous);
}
static function invalid_format($value, ?string $kind=null, $expectedFormats=null, ?Throwable $previous=null): UserException {
if ($kind !== null) $pronom = "il";
else $pronom = self::word()->pronom();
$expectedFormats = cl::withn($expectedFormats);
if (!$expectedFormats) {
$reason = null;
} elseif (count($expectedFormats) == 1) {
$reason = "$pronom doit être au format suivant: ";
} else {
$reason = "$pronom doit être dans l'un des formats suivants: ";
}
$reason .= implode(", ", $expectedFormats);
return self::invalid_value($value, $kind, $reason, $previous);
}
static function forbidden_value($value, ?string $kind=null, $allowedValues=null, ?Throwable $previous=null): UserException {
if ($kind !== null) $pronom = "il";
else $pronom = self::word()->pronom();
$allowedValues = cl::withn($allowedValues);
if (!$allowedValues) $reason = null;
else $reason = "$pronom doit faire partie de cette liste: ";
$reason .= implode(", ", $allowedValues);
return self::invalid_value($value, $kind, $reason, $previous);
}
static function out_of_range($value, ?string $kind=null, ?int $min=null, ?int $max=null, ?Throwable $previous=null): UserException {
if ($kind !== null) {
$pronom = "il";
$compris = "compris";
$superieur = "supérieur";
$inferieur = "inférieur";
} else {
$word = self::word();
$pronom = $word->pronom();
$compris = $word->isFeminin()? "comprise": "compris";
$superieur = $word->isFeminin()? "supérieure": "supérieur";
$inferieur = $word->isFeminin()? "inférieure": "inférieur";
}
if ($min !== null && $max !== null) {
$reason = "$pronom doit être $compris entre $min et $max";
} else if ($min !== null) {
$reason = "$pronom doit être $superieur à $min";
} elseif ($max !== null) {
$reason = "$pronom doit être $inferieur à $max";
} else {
$reason = null;
}
return self::invalid_value($value, $kind, $reason, $previous);
}
static function null_value(?string $kind=null, ?string $reason=null, ?Throwable $previous=null): UserException {
if ($kind !== null) $nul = "null";
else $nul = self::word()->isFeminin()? "nulle": "nul";
return self::generic(null, $kind, "ne doit pas être $nul", $reason, $previous);
}
static function missing_value_message(?int $amount=null, ?string $kind=null): string {
$message = "il manque ";
if ($kind !== null) {
if ($amount !== null) $message = "$amount $kind";
else $message = $kind;
} else {
if ($amount !== null) $message .= self::word()->q($amount);
else $message .= self::word()->_un();
}
return $message;
}
/**
* indiquer qu'une valeur est manquante
*/
static function missing_value(?int $amount=null, ?string $kind=null, ?string $reason=null, ?Throwable $previous=null): UserException {
$reason ??= self::missing_value_message($amount, $kind);
$class = static::EXCEPTION;
return new $class($reason, null, $previous);
}
static function unexpected_value_message(?int $amount=null, ?string $kind=null): string {
if ($amount !== null) {
if ($kind !== null) $kind = "$amount $kind";
else $kind = self::word()->q($amount);
$message = "il y a $kind en trop";
} else {
if ($kind !== null) $kind = "de $kind";
else $kind = self::word()->_de(2);
$message = "il y a trop $kind";
}
return $message;
}
/**
* indiquer qu'une valeur est en trop
*/
static function unexpected_value(?int $amount=null, ?string $kind=null, ?string $reason=null, ?Throwable $previous=null): UserException {
$reason ??= self::unexpected_value_message($amount, $kind);
$class = static::EXCEPTION;
return new $class($reason, null, $previous);
}
}

View File

@ -1,7 +1,7 @@
<?php
namespace nulib\file;
use nulib\exceptions;
use nulib\ValueException;
class SharedFile extends FileWriter {
const USE_LOCKING = true;
@ -9,7 +9,7 @@ class SharedFile extends FileWriter {
const DEFAULT_MODE = "c+b";
function __construct($file, ?string $mode=null, ?bool $throwOnError=null, ?bool $allowLocking=null) {
if ($file === null) throw exceptions::null_value("file");
if ($file === null) throw ValueException::null("file");
parent::__construct($file, $mode, $throwOnError, $allowLocking);
}
}

View File

@ -1,14 +1,14 @@
<?php
namespace nulib\file;
use nulib\exceptions;
use nulib\file\csv\csv_flavours;
use nulib\NoMoreDataException;
use nulib\os\EOFException;
use nulib\os\IOException;
use nulib\php\iter\AbstractIterator;
use nulib\ref\ref_csv;
use nulib\ref\file\csv\ref_csv;
use nulib\str;
use nulib\ValueException;
/**
* Class Stream: lecture/écriture générique dans un flux
@ -61,7 +61,7 @@ class Stream extends AbstractIterator implements IReader, IWriter {
protected $stat;
function __construct($fd, bool $close=true, ?bool $throwOnError=null, ?bool $useLocking=null) {
if ($fd === null) throw exceptions::null_value("resource");
if ($fd === null) throw ValueException::null("resource");
$this->fd = $fd;
$this->close = $close;
$this->throwOnError = $throwOnError ?? static::THROW_ON_ERROR;

View File

@ -2,7 +2,7 @@
namespace nulib\file\csv;
use nulib\cl;
use nulib\ref\ref_csv;
use nulib\ref\file\csv\ref_csv;
use nulib\str;
class csv_flavours {

View File

@ -2,10 +2,10 @@
namespace nulib\file\tab;
use nulib\cl;
use nulib\exceptions;
use nulib\file\csv\CsvBuilder;
use nulib\file\web\Upload;
use nulib\os\path;
use nulib\ValueException;
trait TAbstractBuilder {
/** @param Upload|string|array $builder */
@ -32,7 +32,7 @@ trait TAbstractBuilder {
} elseif (is_array($builder)) {
$params = cl::merge($builder, $params);
} elseif ($builder !== null) {
throw exceptions::invalid_type($builder, "builder", self::class);
throw ValueException::invalid_type($builder, self::class);
}
$output = $params["output"] ?? null;

View File

@ -2,10 +2,10 @@
namespace nulib\file\tab;
use nulib\cl;
use nulib\exceptions;
use nulib\file\csv\CsvReader;
use nulib\file\web\Upload;
use nulib\os\path;
use nulib\ValueException;
trait TAbstractReader {
/** @param Upload|string|array $reader */
@ -31,7 +31,7 @@ trait TAbstractReader {
} elseif (is_array($reader)) {
$params = cl::merge($reader, $params);
} elseif ($reader !== null) {
throw exceptions::invalid_type($reader, "reader", self::class);
throw ValueException::invalid_type($reader, self::class);
}
$input = $params["input"] ?? null;

View File

@ -3,6 +3,7 @@ namespace nulib\mail;
use nulib\cv;
use nulib\str;
use nulib\ValueException;
use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
class MailTemplate {
@ -18,8 +19,8 @@ class MailTemplate {
$texprs = $mail["exprs"] ?? [];
$this->el = new ExpressionLanguage();
$this->subject = cv::not_null($tsubject, "subject");
$this->body = cv::not_null($tbody, "body");
ValueException::check_null($this->subject = $tsubject, "subject");
ValueException::check_null($this->body = $tbody, "body");
$exprs = [];
# Commencer par extraire les expressions de la forme {name}
if (preg_match_all('/\{([a-zA-Z_][a-zA-Z0-9_.-]*)}/', $this->body, $mss, PREG_SET_ORDER)) {

View File

@ -1,12 +1,11 @@
<?php
namespace nulib\mail;
use nulib\app\config;
use nulib\cl;
use nulib\cv;
use nulib\exceptions;
use nulib\output\msg;
use nulib\str;
use nulib\ValueException;
use PHPMailer\PHPMailer\PHPMailer;
use PHPMailer\PHPMailer\SMTP;
@ -21,7 +20,6 @@ class mailer {
return true;
} else {
switch (strval($value)) {
case "":
case "0":
case "no":
case "off":
@ -58,40 +56,25 @@ class mailer {
"secure" => "?string",
];
static function resolve_params(?array $params=null): array {
$envParams = [
"backend" => cv::vn(getenv("NULIB_MAIL_BACKEND")),
"debug" => cv::vn(getenv("NULIB_MAIL_DEBUG")),
"host" => cv::vn(getenv("NULIB_MAIL_HOST")),
"port" => cv::vn(getenv("NULIB_MAIL_PORT")),
"auth" => cv::vn(getenv("NULIB_MAIL_AUTH")),
"username" => cv::vn(getenv("NULIB_MAIL_USERNAME")),
"password" => cv::vn(getenv("NULIB_MAIL_PASSWORD")),
"secure" => cv::vn(getenv("NULIB_MAIL_SECURE")),
];
$configParams = config::k("mailer");
foreach (array_keys(self::SCHEMA) as $key) {
$params[$key] ??= $envParams[$key] ?? null;
$params[$key] ??= $configParams[$key] ?? null;
}
return $params;
}
static function get(?array $params=null, ?bool $exceptions=null): PHPMailer {
$params = self::resolve_params($params);
$mailer = new PHPMailer($exceptions);
$mailer->setLanguage("fr");
$mailer->CharSet = PHPMailer::CHARSET_UTF8;
# backend
$backend = $params["backend"] ?? "smtp";
$backend = $params["backend"] ?? null;
$backend ??= cv::vn(getenv("NULIB_MAIL_BACKEND"));
$backend ??= "smtp";
switch ($backend) {
case "smtp":
# host, port
# host
$host = $params["host"] ?? null;
$port = $params["port"] ?? 25;
$host ??= cv::vn(getenv("NULIB_MAIL_HOST"));
# port
$port = $params["port"] ?? null;
$port ??= cv::vn(getenv("NULIB_MAIL_PORT"));
$port ??= 25;
if ($host === null) {
throw exceptions::null_value("host");
throw new ValueException("mail host is required");
}
msg::debug("new PHPMailer using SMTP to $host:$port");
$mailer->isSMTP();
@ -107,15 +90,17 @@ class mailer {
$mailer->isSendmail();
break;
default:
throw exceptions::forbidden_value($backend, "backend", ["smtp", "phpmail", "sendmail"]);
throw ValueException::invalid_value($backend, "mailer backend");
}
# debug
$debug = $params["debug"] ?? SMTP::DEBUG_OFF;
$debug = $params["debug"] ?? null;
$debug ??= cv::vn(getenv("NULIB_MAIL_DEBUG"));
$debug ??= SMTP::DEBUG_OFF;
if (is_int($debug)) {
if ($debug < SMTP::DEBUG_OFF) $debug = SMTP::DEBUG_OFF;
elseif ($debug > SMTP::DEBUG_LOWLEVEL) $debug = SMTP::DEBUG_LOWLEVEL;
} elseif (!self::is_bool($debug)) {
throw exceptions::invalid_type($debug, "debug", ["int", "bool"]);
throw ValueException::invalid_value($debug, "debug mode");
}
$mailer->SMTPDebug = $debug;
# auth, username, password
@ -145,10 +130,7 @@ class mailer {
$mailer->SMTPSecure = $secure;
break;
default:
throw exceptions::forbidden_value($secure, "secure", [
PHPMailer::ENCRYPTION_SMTPS,
PHPMailer::ENCRYPTION_STARTTLS,
]);
throw ValueException::invalid_value($secure, "encryption mode");
}
}
@ -190,7 +172,7 @@ class mailer {
$tos = str::join(",", $tos);
msg::debug("Sending to $tos");
if (!$mailer->send()) {
throw new MailerException("erreur d'envoi du mail", $mailer->ErrorInfo);
throw new MailerException("Une erreur s'est produite pendant l'envoi du mail", $mailer->ErrorInfo);
}
}

View File

@ -100,8 +100,8 @@ interface IMessenger {
* terminer le chapitre en cours. toutes les actions en cours sont terminées
* avec un résultat neutre.
*
* @param bool $all terminer *tous* les chapitres ainsi que la section en
* cours
* @param bool $all faut-il terminer *tous* les chapitres ainsi que la section
* en cours?
*/
function end(bool $all=false): void;
}

View File

@ -2,8 +2,8 @@
namespace nulib\output;
use nulib\cl;
use nulib\exceptions;
use nulib\str;
use nulib\ValueException;
/**
* Class _messenger: classe de base pour say, log et msg
@ -15,7 +15,7 @@ abstract class _messenger {
static function set_messenger_class(string $msg_class, ?array $params=null) {
if (!is_subclass_of($msg_class, IMessenger::class)) {
throw exceptions::invalid_type($msg_class, $kind, IMessenger::class);
throw ValueException::invalid_class($msg_class, IMessenger::class);
}
static::set_messenger(new $msg_class($params));
}

View File

@ -2,8 +2,8 @@
namespace nulib\output;
use nulib\app\app;
use nulib\exceptions;
use nulib\output\std\ProxyMessenger;
use nulib\ValueException;
/**
* Class console: afficher un message sur la console
@ -63,7 +63,7 @@ class console extends _messenger {
]);
break;
default:
throw exceptions::forbidden_value($verbosity, $kind, ["silent", "quiet", "normal", "verbose", "debug"]);
throw ValueException::invalid_value($verbosity, "verbosity");
}
}

View File

@ -4,9 +4,9 @@ namespace nulib\output\std;
use Exception;
use nulib\A;
use nulib\cl;
use nulib\exceptions;
use nulib\ExceptionShadow;
use nulib\output\IMessenger;
use nulib\UserException;
use Throwable;
class StdMessenger implements _IMessenger {
@ -236,11 +236,9 @@ class StdMessenger implements _IMessenger {
return $indentLevel;
}
protected function _printTitle(
?string $linePrefix, int $level,
string $type, $content,
int $indentLevel, StdOutput $out
): void {
protected function _printTitle(?string $linePrefix, int $level,
string $type, $content,
int $indentLevel, StdOutput $out): void {
$prefixes = self::GENERIC_PREFIXES[$level][$type];
if ($prefixes[0]) $out->print();
$content = cl::with($content);
@ -286,12 +284,10 @@ class StdMessenger implements _IMessenger {
}
}
protected function _printAction(
?string $linePrefix, int $level,
bool $printContent, $content,
bool $printResult, ?bool $rsuccess, $rcontent,
int $indentLevel, StdOutput $out
): void {
protected function _printAction(?string $linePrefix, int $level,
bool $printContent, $content,
bool $printResult, ?bool $rsuccess, $rcontent,
int $indentLevel, StdOutput $out): void {
$color = $out->isColor();
if ($rsuccess === true) $type = "success";
elseif ($rsuccess === false) $type = "failure";
@ -361,11 +357,9 @@ class StdMessenger implements _IMessenger {
}
}
protected function _printGeneric(
?string $linePrefix, int $level,
string $type, $content,
int $indentLevel, StdOutput $out
): void {
protected function _printGeneric(?string $linePrefix, int $level,
string $type, $content,
int $indentLevel, StdOutput $out): void {
$prefixes = self::GENERIC_PREFIXES[$level][$type];
$content = cl::with($content);
if ($out->isColor()) {
@ -396,11 +390,7 @@ class StdMessenger implements _IMessenger {
}
}
protected function _printGenericOrException(
?int $level,
string $type, $content,
int $indentLevel, StdOutput $out
): void {
protected function _printGenericOrException(?int $level, string $type, $content, int $indentLevel, StdOutput $out): void {
$linePrefix = $this->getLinePrefix();
# si $content contient des exceptions, les afficher avec un level moindre
$exceptions = null;
@ -431,18 +421,27 @@ class StdMessenger implements _IMessenger {
$level1 = $this->decrLevel($level);
$showTraceback = $this->checkLevel($level1);
foreach ($exceptions as $exception) {
# tout d'abord message
$message = exceptions::get_message($exception);
if ($showContent) {
# tout d'abord userMessage
if ($exception instanceof UserException) {
$userMessage = UserException::get_user_message($exception);
$userMessage ??= "Une erreur technique s'est produite";
$showSummary = true;
} else {
$userMessage = UserException::get_summary($exception);
$showSummary = false;
}
if ($userMessage !== null && $showContent) {
if ($printActions) { $this->printActions(); $printActions = false; }
$this->_printGeneric($linePrefix, $level, $type, $message, $indentLevel, $out);
$this->_printGeneric($linePrefix, $level, $type, $userMessage, $indentLevel, $out);
}
# puis summary et traceback
if ($showTraceback) {
if ($printActions) { $this->printActions(); $printActions = false; }
$summary = exceptions::get_summary($exception, false);
$this->_printGeneric($linePrefix, $level1, $type, $summary, $indentLevel, $out);
$traceback = exceptions::get_traceback($exception);
if ($showSummary) {
$summary = UserException::get_summary($exception);
$this->_printGeneric($linePrefix, $level1, $type, $summary, $indentLevel, $out);
}
$traceback = UserException::get_traceback($exception);
$this->_printGeneric($linePrefix, $level1, $type, $traceback, $indentLevel, $out);
}
}

View File

@ -6,8 +6,8 @@ use Exception;
use nulib\A;
use nulib\cl;
use nulib\cv;
use nulib\exceptions;
use nulib\StateException;
use nulib\ValueException;
use ReflectionClass;
use ReflectionFunction;
use ReflectionMethod;
@ -446,7 +446,11 @@ class func {
const TYPE_STATIC = self::TYPE_METHOD | self::FLAG_STATIC;
protected static function not_a_callable($func, ?string $reason) {
throw exceptions::invalid_type($func, null, "callable");
if ($reason === null) {
$msg = var_export($func, true);
$reason = "$msg: not a callable";
}
return new ValueException($reason);
}
private static function _with($func, ?array $args=null, bool $strict=true, ?string &$reason=null): ?self {
@ -600,7 +604,7 @@ class func {
$mask = $staticOnly? self::MASK_PS: self::MASK_P;
$expected = $staticOnly? self::METHOD_PS: self::METHOD_P;
} else {
throw exceptions::invalid_type($class_or_object, null, ["class", "object"]);
throw new ValueException("$class_or_object: vous devez spécifier une classe ou un objet");
}
$methods = [];
foreach ($c->getMethods() as $m) {
@ -773,7 +777,7 @@ class func {
if (is_object($object) && !($this->flags & self::FLAG_STATIC)) {
if (is_object($c)) $c = get_class($c);
if (is_string($c) && !($object instanceof $c)) {
throw exceptions::invalid_type($object, "object", $c);
throw ValueException::invalid_type($object, $c);
}
$this->object = $object;
$this->bound = true;

View File

@ -1,7 +1,7 @@
<?php
namespace nulib\php\time;
use DateTimeInterface;
use DateTimeZone;
/**
* Class Date: une date
@ -9,14 +9,9 @@ use DateTimeInterface;
class Date extends DateTime {
const DEFAULT_FORMAT = "d/m/Y";
protected function fix(DateTimeInterface $datetime): \DateTimeInterface {
return $datetime->setTime(0, 0);
}
/** @return MutableDate|self */
function clone(bool $mutable=false): DateTimeInterface {
if ($mutable) return new MutableDate($this);
else return clone $this;
function __construct($datetime="now", DateTimeZone $timezone=null) {
parent::__construct($datetime, $timezone);
$this->setTime(0, 0);
}
function format($format=self::DEFAULT_FORMAT): string {

View File

@ -1,10 +1,10 @@
<?php
namespace nulib\php\time;
use DateTimeImmutable;
use DateTimeInterface;
use DateTimeZone;
use InvalidArgumentException;
use nulib\str;
/**
* Class DateTime: une date et une heure
@ -24,78 +24,252 @@ use InvalidArgumentException;
* @property-read string $YmdHMS
* @property-read string $YmdHMSZ
*/
class DateTime extends \DateTimeImmutable {
use _TDateTime;
class DateTime extends \DateTime {
static function with($datetime): self {
if ($datetime instanceof static) return $datetime;
else return new static($datetime);
}
static function withn($datetime): ?self {
if ($datetime === null) return null;
elseif ($datetime instanceof static) return $datetime;
else return new static($datetime);
}
static function ensure(&$datetime): void {
$datetime = static::withn($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+))?$/';
const YMDHISZ_PATTERN = '/^((?:\d{2})?\d{2})-?(\d{2})-?(\d{2})[tT](\d{2}):?(\d{2}):?(\d{2})?([zZ]|\+\d{2}:?\d{2})?$/';
protected static function get_value(array $datetime, ?string $key, ?string $k, ?int $index) {
$value = null;
if ($value === null && $key !== null) $value = $datetime[$key] ?? null;
if ($value === null && $k !== null) $value = $datetime[$k] ?? null;
if ($value === null && $index !== null) $value = $datetime[$index] ?? null;
return $value;
}
private static function parse_int(array $datetime, ?string $key, ?string $k, ?int $index, ?int &$part, bool $required=true, ?int $default=null): bool {
$part = null;
$value = self::get_value($datetime, $key, $k, $index);
if ($value === null) {
if ($required && $default === null) return false;
$part = $default;
return true;
}
if (is_numeric($value)) {
$part = intval($value);
return true;
}
return false;
}
private static function parse_str(array $datetime, ?string $key, ?string $k, ?int $index, ?string &$part, bool $required = true, ?string $default=null): bool {
$part = null;
$value = self::get_value($datetime, $key, $k, $index);
if ($value === null) {
if ($required && $default === null) return false;
$part = $default;
return true;
}
if (is_string($value)) {
$part = $value;
return true;
}
return false;
}
protected static function parse_array(array $datetime): ?array {
if (!self::parse_int($datetime, "year", "Y", 0, $year)) return null;
if (!self::parse_int($datetime, "month", "m", 1, $month)) return null;
if (!self::parse_int($datetime, "day", "d", 2, $day)) return null;
self::parse_int($datetime, "hour", "H", 3, $hour, false);
self::parse_int($datetime, "minute", "M", 4, $minute, false);
self::parse_int($datetime, "second", "S", 5, $second, false);
self::parse_str($datetime, "tz", null, 6, $tz, false);
return [$year, $month, $day, $hour, $minute, $second, $tz];
}
static function isa($datetime): bool {
if ($datetime === null) return false;
if ($datetime instanceof DateTimeInterface) return true;
if (is_int($datetime)) return true;
if (is_string($datetime)) {
return preg_match(self::DMY_PATTERN, $datetime) ||
preg_match(self::YMD_PATTERN, $datetime) ||
preg_match(self::DMYHIS_PATTERN, $datetime) ||
preg_match(self::YMDHISZ_PATTERN, $datetime);
}
if (is_array($datetime)) {
return self::parse_array($datetime) !== null;
}
return false;
}
static function isa_datetime($datetime, bool $frOnly=false): bool {
if ($datetime === null) return false;
if ($datetime instanceof DateTimeInterface) return true;
if (is_int($datetime)) return true;
if (is_string($datetime)) {
return preg_match(self::DMYHIS_PATTERN, $datetime) ||
(!$frOnly && preg_match(self::YMDHISZ_PATTERN, $datetime));
}
if (is_array($datetime)) {
return self::parse_array($datetime) !== null;
}
return false;
}
static function isa_date($date, bool $frOnly=false): bool {
if ($date === null) return false;
if ($date instanceof DateTimeInterface) return true;
if (is_int($date)) return true;
if (is_string($date)) {
return preg_match(self::DMY_PATTERN, $date) ||
(!$frOnly && preg_match(self::YMD_PATTERN, $date));
}
if (is_array($date)) {
return self::parse_array($date) !== null;
}
return false;
}
/** retourner le nombre de secondes depuis minuit */
static function _nbsecs_format(\DateTime $datetime): string {
[$h, $m, $s] = explode(",", $datetime->format("H,i,s"));
return $h * 3600 + $m * 60 + $s;
}
static function _YmdHMSZ_format(\DateTime $datetime): string {
$YmdHMS = $datetime->format("Ymd\\THis");
$Z = $datetime->format("P");
if ($Z === "+00:00") $Z = "Z";
return "$YmdHMS$Z";
}
const DEFAULT_FORMAT = "d/m/Y H:i:s";
const INT_FORMATS = [
"year" => "Y",
"month" => "m",
"day" => "d",
"hour" => "H",
"minute" => "i",
"second" => "s",
"wday" => "N",
"wnum" => "W",
"nbsecs" => [self::class, "_nbsecs_format"],
];
const STRING_FORMATS = [
"timezone" => "P",
"datetime" => "d/m/Y H:i:s",
"date" => "d/m/Y",
"Ymd" => "Ymd",
"YmdHMS" => "Ymd\\THis",
"YmdHMSZ" => [self::class, "_YmdHMSZ_format"],
];
/**
* $datetime est une spécification de date, avec ou sans fuseau horaire
* corriger une année à deux chiffres qui est située dans le passé et
* retourner l'année à 4 chiffres.
*
* si $datetime ne contient pas de fuseau horaire, elle est réputée être dans
* le fuseau $timezone, qui est le fuseau local par défaut
*
* si $datetime contient un fuseau horaire et si $forceTimezone est vrai,
* alors $datetime est réexprimée dans le fuseau $timezone.
* si $timezone est null alors $forceTimezone vaut vrai par défaut.
*
* datetime | timezone | forceTimezone | résultat
* -----------------|----------|---------------|---------
* datetime | any | any | datetime+localtz
* datetime+origtz | null | null | datetime+localtz
* datetime+origtz | null | true | datetime+localtz
* datetime+origtz | null | false | datetime+origtz
* datetime+origtz | newtz | null | datetime+origtz
* datetime+origtz | newtz | false | datetime+origtz
* datetime+origtz | newtz | true | datetime+newtz
* par exemple, si l'année courante est 2019, alors:
* - fix_past_year('18') === '2018'
* - fix_past_year('19') === '1919'
* - fix_past_year('20') === '1920'
*/
function __construct($datetime=null, DateTimeZone $timezone=null, ?bool $forceTimezone=null) {
$resetTimezone = null;
$forceTimezone ??= $timezone === null;
if ($forceTimezone) {
$resetTimezone = $timezone ?? new DateTimeZone(date_default_timezone_get());
static function fix_past_year(int $year): int {
if ($year < 100) {
$y = getdate(); $y = $y["year"];
$r = $y % 100;
$c = $y - $r;
if ($year >= $r) $year += $c - 100;
else $year += $c;
}
return $year;
}
/**
* corriger une année à deux chiffres et retourner l'année à 4 chiffres.
* l'année charnière entre année passée et année future est 70
*
* par exemple, si l'année courante est 2019, alors:
* - fix_past_year('18') === '2018'
* - fix_past_year('19') === '2019'
* - fix_past_year('20') === '2020'
* - fix_past_year('69') === '2069'
* - fix_past_year('70') === '1970'
* - fix_past_year('71') === '1971'
*/
static function fix_any_year(int $year): int {
if ($year < 100) {
$y = intval(date("Y"));
$r = $y % 100;
$c = $y - $r;
if ($year >= 70) $year += $c - 100;
else $year += $c;
}
return $year;
}
static function fix_z(?string $Z): ?string {
$Z = strtoupper($Z);
str::del_prefix($Z, "+");
if (preg_match('/^\d{4}$/', $Z)) {
$Z = substr($Z, 0, 2).":".substr($Z, 2);
}
if ($Z === "Z" || $Z === "UTC" || $Z === "00:00") return "UTC";
return "GMT+$Z";
}
function __construct($datetime="now", DateTimeZone $timezone=null, ?bool $forceLocalTimezone=null) {
$forceLocalTimezone ??= $timezone === null;
if ($forceLocalTimezone) {
$setTimezone = $timezone;
$timezone = null;
}
$datetime ??= "now";
if ($datetime instanceof DateTimeImmutable) {
$datetime = \DateTime::createFromImmutable($datetime);
} elseif ($datetime instanceof \DateTime) {
$datetime = clone $datetime;
#XXX sous PHP 8, remplacer les deux commandes ci-dessus par
# DateTime::createFromInterface
if ($datetime instanceof \DateTimeInterface) {
$timezone ??= $datetime->getTimezone();
parent::__construct();
$this->setTimestamp($datetime->getTimestamp());
$this->setTimezone($timezone);
} elseif (is_int($datetime)) {
$timestamp = $datetime;
$datetime = new \DateTime("now", $timezone);
$datetime->setTimestamp($timestamp);
parent::__construct("now", $timezone);
$this->setTimestamp($datetime);
} elseif (is_string($datetime)) {
$Y = $H = $Z = null;
if (preg_match(_utils::DMY_PATTERN, $datetime, $ms)) {
if (preg_match(self::DMY_PATTERN, $datetime, $ms)) {
$Y = $ms[3] ?? null;
if ($Y !== null) $Y = _utils::fix_any_year(intval($Y));
if ($Y !== null) $Y = self::fix_any_year(intval($Y));
else $Y = intval(date("Y"));
$m = intval($ms[2]);
$d = intval($ms[1]);
} elseif (preg_match(_utils::YMD_PATTERN, $datetime, $ms)) {
} elseif (preg_match(self::YMD_PATTERN, $datetime, $ms)) {
$Y = $ms[1];
if (strlen($Y) == 2) $Y = _utils::fix_any_year(intval($ms[1]));
if (strlen($Y) == 2) $Y = self::fix_any_year(intval($ms[1]));
else $Y = intval($Y);
$m = intval($ms[2]);
$d = intval($ms[3]);
} elseif (preg_match(_utils::DMYHIS_PATTERN, $datetime, $ms)) {
} elseif (preg_match(self::DMYHIS_PATTERN, $datetime, $ms)) {
$Y = $ms[3];
if ($Y !== null) $Y = _utils::fix_any_year(intval($Y));
if ($Y !== null) $Y = self::fix_any_year(intval($Y));
else $Y = intval(date("Y"));
$m = intval($ms[2]);
$d = intval($ms[1]);
$H = intval($ms[4]);
$M = intval($ms[5]);
$S = intval($ms[6] ?? 0);
} elseif (preg_match(_utils::YMDHISZ_PATTERN, $datetime, $ms)) {
} elseif (preg_match(self::YMDHISZ_PATTERN, $datetime, $ms)) {
$Y = $ms[1];
if (strlen($Y) == 2) $Y = _utils::fix_any_year(intval($ms[1]));
if (strlen($Y) == 2) $Y = self::fix_any_year(intval($ms[1]));
else $Y = intval($Y);
$m = intval($ms[2]);
$d = intval($ms[3]);
@ -107,61 +281,73 @@ class DateTime extends \DateTimeImmutable {
if ($Y !== null) {
if ($H === null) $datetime = sprintf("%04d-%02d-%02d", $Y, $m, $d);
else $datetime = sprintf("%04d-%02d-%02d %02d:%02d:%02d", $Y, $m, $d, $H, $M, $S);
if ($Z !== null) $timezone = new DateTimeZone(_utils::fix_z($Z));
if ($Z !== null) $timezone ??= new DateTimeZone(self::fix_z($Z));
}
$datetime = new \DateTime($datetime, $timezone);
parent::__construct($datetime, $timezone);
} elseif (is_array($datetime) && ($datetime = _utils::parse_array($datetime)) !== null) {
} elseif (is_array($datetime) && ($datetime = self::parse_array($datetime)) !== null) {
$setTimezone = $timezone;
$timezone = null;
[$Y, $m, $d, $H, $M, $S, $Z] = $datetime;
if ($H === null && $M === null && $S === null) {
$datetime = sprintf("%04d-%02d-%02d", $Y, $m, $d);
} else {
$datetime = sprintf("%04d-%02d-%02d %02d:%02d:%02d", $Y, $m, $d, $H ?? 0, $M ?? 0, $S ?? 0);
}
if ($Z !== null) $timezone = new DateTimeZone(_utils::fix_z($Z));
$datetime = new \DateTime($datetime, $timezone);
}
if ($Z !== null) $timezone ??= new DateTimeZone(self::fix_z($Z));
parent::__construct($datetime, $timezone);
if ($datetime instanceof DateTimeInterface) {
if ($resetTimezone !== null) $datetime->setTimezone($resetTimezone);
$datetime = $this->fix($datetime);
parent::__construct($datetime->format("Y-m-d\\TH:i:s.uP"));
} else {
} else {
throw new InvalidArgumentException("datetime must be a string or an instance of DateTimeInterface");
}
if ($forceLocalTimezone) {
$setTimezone ??= new DateTimeZone(date_default_timezone_get());
$this->setTimezone($setTimezone);
}
}
protected function fix(DateTimeInterface $datetime): DateTimeInterface {
return $datetime;
function clone(): self {
return clone $this;
}
/** @return MutableDateTime|self */
function clone(bool $mutable=false): DateTimeInterface {
if ($mutable) return new MutableDateTime($this);
else return clone $this;
function diff($target, $absolute=false): DateInterval {
return new DateInterval(parent::diff($target, $absolute));
}
function format($format=self::DEFAULT_FORMAT): string {
if (array_key_exists($format, self::INT_FORMATS)) {
$format = self::INT_FORMATS[$format];
} elseif (array_key_exists($format, self::STRING_FORMATS)) {
$format = self::STRING_FORMATS[$format];
}
if (is_callable($format)) return $format($this);
else return \DateTime::format($format);
}
/**
* modifier cet objet pour que l'heure soit à 00:00:00 ce qui le rend propice
* à l'utilisation comme borne inférieure d'une période
*/
function getStartOfDay(): self {
return new static($this->setTime(0, 0));
function wrapStartOfDay(): self {
$this->setTime(0, 0);
return $this;
}
/**
* modifier cet objet pour que l'heure soit à 23:59:59.999999 ce qui le rend
* propice à l'utilisation comme borne supérieure d'une période
*/
function getEndOfDay(): self {
return new static($this->setTime(23, 59, 59, 999999));
function wrapEndOfDay(): self {
$this->setTime(23, 59, 59, 999999);
return $this;
}
function getPrevDay(int $nbDays=1, bool $skipWeekend=false): self {
if ($nbDays == 1 && $skipWeekend && $this->wday == 1) {
$nbDays = 3;
$nbdays = 3;
}
return new static($this->sub(new \DateInterval("P${nbDays}D")));
return static::with($this->sub(new \DateInterval("P${nbDays}D")));
}
function getNextDay(int $nbDays=1, bool $skipWeekend=false): self {
@ -169,6 +355,35 @@ class DateTime extends \DateTimeImmutable {
$wday = $this->wday;
if ($wday > 5) $nbDays = 8 - $this->wday;
}
return new static($this->add(new \DateInterval("P${nbDays}D")));
return static::with($this->add(new \DateInterval("P${nbDays}D")));
}
function getElapsedAt(?DateTime $now=null, ?int $resolution=null): string {
return Elapsed::format_at($this, $now, $resolution);
}
function getElapsedSince(?DateTime $now=null, ?int $resolution=null): string {
return Elapsed::format_since($this, $now, $resolution);
}
function getElapsedDelay(?DateTime $now=null, ?int $resolution=null): string {
return Elapsed::format_delay($this, $now, $resolution);
}
function __toString(): string {
return $this->format();
}
function __get($name) {
if (array_key_exists($name, self::INT_FORMATS)) {
$format = self::INT_FORMATS[$name];
if (is_callable($format)) return intval($format($this));
else return intval($this->format($format));
} elseif (array_key_exists($name, self::STRING_FORMATS)) {
$format = self::STRING_FORMATS[$name];
if (is_callable($format)) return $format($this);
else return $this->format($format);
}
throw new InvalidArgumentException("Unknown property $name");
}
}

View File

@ -39,7 +39,8 @@ class Delay {
"s" => [1, 0],
];
protected static function compute_dest(int $x, string $u, ?int $y, MutableDateTime $dest): array {
static function compute_dest(int $x, string $u, ?int $y, ?DateTimeInterface $from): array {
$dest = DateTime::with($from)->clone();
$yu = null;
switch ($u) {
case "w":
@ -88,9 +89,9 @@ class Delay {
}
function __construct($delay, ?DateTimeInterface $from=null) {
$from = MutableDateTime::with($from)->clone(true);
if ($from === null) $from = new DateTime();
if ($delay === "INF") {
$dest = $from;
$dest = DateTime::with($from)->clone();
$dest->add(new DateInterval("P9999Y"));
$repr = "INF";
} elseif (is_int($delay)) {
@ -125,11 +126,11 @@ class Delay {
[$this->dest, $this->repr] = $data;
}
/** @var MutableDateTime */
/** @var DateTime */
protected $dest;
function getDest(): DateTime {
return $this->dest->clone();
return $this->dest;
}
function addDuration($duration) {
@ -156,7 +157,7 @@ class Delay {
}
protected function _getDiff(?DateTimeInterface $now=null): \DateInterval {
$now ??= new \DateTime();
if ($now === null) $now = new DateTime();
return $this->dest->diff($now);
}

View File

@ -1,8 +1,6 @@
<?php
namespace nulib\php\time;
use DateTimeInterface;
/**
* Class Elapsed: durée entre deux événements
*/
@ -126,19 +124,19 @@ class Elapsed {
return self::format_generic($prefix, $d, 0, 0);
}
static function format_at(DateTimeInterface $start, ?DateTimeInterface $now=null, ?int $resolution=null): string {
static function format_at(DateTime $start, ?DateTime $now=null, ?int $resolution=null): string {
$now ??= new DateTime();
$seconds = $now->getTimestamp() - $start->getTimestamp();
return (new self($seconds, $resolution))->formatAt();
}
static function format_since(DateTimeInterface $start, ?DateTimeInterface $now=null, ?int $resolution=null): string {
static function format_since(DateTime $start, ?DateTime $now=null, ?int $resolution=null): string {
$now ??= new DateTime();
$seconds = $now->getTimestamp() - $start->getTimestamp();
return (new self($seconds, $resolution))->formatSince();
}
static function format_delay(DateTimeInterface $start, ?DateTimeInterface $now=null, ?int $resolution=null): string {
static function format_delay(DateTime $start, ?DateTime $now=null, ?int $resolution=null): string {
$now ??= new DateTime();
$seconds = $now->getTimestamp() - $start->getTimestamp();
return (new self($seconds, $resolution))->formatDelay();

View File

@ -1,24 +0,0 @@
<?php
namespace nulib\php\time;
use DateTimeInterface;
use DateTimeZone;
class MutableDate extends MutableDateTime {
const DEFAULT_FORMAT = "d/m/Y";
function __construct($datetime="now", DateTimeZone $timezone=null) {
parent::__construct($datetime, $timezone);
$this->setTime(0, 0);
}
/** @return Date|self */
function clone(bool $mutable=false): DateTimeInterface {
if ($mutable) return clone $this;
else return new Date($this);
}
function format($format=self::DEFAULT_FORMAT): string {
return parent::format($format);
}
}

View File

@ -1,180 +0,0 @@
<?php
namespace nulib\php\time;
use DateTimeInterface;
use DateTimeZone;
use InvalidArgumentException;
/**
* Class DateTime: une date et une heure mutables
*
* @property-read int $year
* @property-read int $month
* @property-read int $day
* @property-read int $hour
* @property-read int $minute
* @property-read int $second
* @property-read int $wday
* @property-read int $wnum
* @property-read string $timezone
* @property-read string $datetime
* @property-read string $date
* @property-read string $Ymd
* @property-read string $YmdHMS
* @property-read string $YmdHMSZ
*/
class MutableDateTime extends \DateTime {
use _TDateTime;
const DEFAULT_FORMAT = "d/m/Y H:i:s";
/**
* $datetime est une spécification de date, avec ou sans fuseau horaire
*
* si $datetime ne contient pas de fuseau horaire, elle est réputée être dans
* le fuseau $timezone, qui est le fuseau local par défaut
*
* si $datetime contient un fuseau horaire et si $forceTimezone est vrai,
* alors $datetime est réexprimée dans le fuseau $timezone.
* si $timezone est null alors $forceTimezone vaut vrai par défaut.
*
* datetime | timezone | forceTimezone | résultat
* -----------------|----------|---------------|---------
* datetime | any | any | datetime+localtz
* datetime+origtz | null | null | datetime+localtz
* datetime+origtz | null | true | datetime+localtz
* datetime+origtz | null | false | datetime+origtz
* datetime+origtz | newtz | null | datetime+origtz
* datetime+origtz | newtz | false | datetime+origtz
* datetime+origtz | newtz | true | datetime+newtz
*/
function __construct($datetime=null, DateTimeZone $timezone=null, ?bool $forceTimezone=null) {
$resetTimezone = null;
$forceTimezone ??= $timezone === null;
if ($forceTimezone) {
$resetTimezone = $timezone ?? new DateTimeZone(date_default_timezone_get());
$timezone = null;
}
$datetime ??= "now";
if ($datetime instanceof \DateTimeInterface) {
$timezone ??= $datetime->getTimezone();
parent::__construct();
$this->setTimestamp($datetime->getTimestamp());
$this->setTimezone($timezone);
} elseif (is_int($datetime)) {
parent::__construct("now", $timezone);
$this->setTimestamp($datetime);
} elseif (is_string($datetime)) {
$Y = $H = $Z = null;
if (preg_match(_utils::DMY_PATTERN, $datetime, $ms)) {
$Y = $ms[3] ?? null;
if ($Y !== null) $Y = _utils::fix_any_year(intval($Y));
else $Y = intval(date("Y"));
$m = intval($ms[2]);
$d = intval($ms[1]);
} elseif (preg_match(_utils::YMD_PATTERN, $datetime, $ms)) {
$Y = $ms[1];
if (strlen($Y) == 2) $Y = _utils::fix_any_year(intval($ms[1]));
else $Y = intval($Y);
$m = intval($ms[2]);
$d = intval($ms[3]);
} elseif (preg_match(_utils::DMYHIS_PATTERN, $datetime, $ms)) {
$Y = $ms[3];
if ($Y !== null) $Y = _utils::fix_any_year(intval($Y));
else $Y = intval(date("Y"));
$m = intval($ms[2]);
$d = intval($ms[1]);
$H = intval($ms[4]);
$M = intval($ms[5]);
$S = intval($ms[6] ?? 0);
} elseif (preg_match(_utils::YMDHISZ_PATTERN, $datetime, $ms)) {
$Y = $ms[1];
if (strlen($Y) == 2) $Y = _utils::fix_any_year(intval($ms[1]));
else $Y = intval($Y);
$m = intval($ms[2]);
$d = intval($ms[3]);
$H = intval($ms[4]);
$M = intval($ms[5]);
$S = intval($ms[6] ?? 0);
$Z = $ms[7] ?? null;
}
if ($Y !== null) {
if ($H === null) $datetime = sprintf("%04d-%02d-%02d", $Y, $m, $d);
else $datetime = sprintf("%04d-%02d-%02d %02d:%02d:%02d", $Y, $m, $d, $H, $M, $S);
if ($Z !== null) $timezone = new DateTimeZone(_utils::fix_z($Z));
}
parent::__construct($datetime, $timezone);
} elseif (is_array($datetime) && ($datetime = _utils::parse_array($datetime)) !== null) {
[$Y, $m, $d, $H, $M, $S, $Z] = $datetime;
if ($H === null && $M === null && $S === null) {
$datetime = sprintf("%04d-%02d-%02d", $Y, $m, $d);
} else {
$datetime = sprintf("%04d-%02d-%02d %02d:%02d:%02d", $Y, $m, $d, $H ?? 0, $M ?? 0, $S ?? 0);
}
if ($Z !== null) $timezone = new DateTimeZone(_utils::fix_z($Z));
parent::__construct($datetime, $timezone);
} else {
throw new InvalidArgumentException("datetime must be a string or an instance of DateTimeInterface");
}
if ($resetTimezone !== null) $this->setTimezone($resetTimezone);
}
/** @return DateTime|self */
function clone(bool $mutable=false): DateTimeInterface {
if ($mutable) return clone $this;
else return new DateTime($this);
}
/**
* modifier cet objet pour que l'heure soit à 00:00:00 ce qui le rend propice
* à l'utilisation comme borne inférieure d'une période
*/
function setStartOfDay(): self {
$this->setTime(0, 0);
return $this;
}
function getStartOfDay(): self {
return $this->clone(true)->setStartOfDay();
}
/**
* modifier cet objet pour que l'heure soit à 23:59:59.999999 ce qui le rend
* propice à l'utilisation comme borne supérieure d'une période
*/
function setEndOfDay(): self {
$this->setTime(23, 59, 59, 999999);
return $this;
}
function getEndOfDay(): self {
return $this->clone(true)->setEndOfDay();
}
function setPrevDay(int $nbDays=1, bool $skipWeekend=false): self {
if ($nbDays == 1 && $skipWeekend && $this->wday == 1) {
$nbDays = 3;
}
$this->sub(new \DateInterval("P${nbDays}D"));
return $this;
}
function getPrevDay(int $nbDays=1, bool $skipWeekend=false): self {
return $this->clone(true)->setPrevDay($nbDays, $skipWeekend);
}
function setNextDay(int $nbDays=1, bool $skipWeekend=false): self {
if ($nbDays == 1 && $skipWeekend) {
$wday = $this->wday;
if ($wday > 5) $nbDays = 8 - $this->wday;
}
$this->add(new \DateInterval("P${nbDays}D"));
return $this;
}
function getNextDay(int $nbDays=1, bool $skipWeekend=false): self {
return $this->clone(true)->setNextDay($nbDays, $skipWeekend);
}
}

View File

@ -1,103 +0,0 @@
<?php
namespace nulib\php\time;
use DateTimeInterface;
use InvalidArgumentException;
trait _TDateTime {
static function with($datetime): self {
if ($datetime instanceof static) return $datetime;
else return new static($datetime);
}
static function withn($datetime): ?self {
if ($datetime === null) return null;
elseif ($datetime instanceof static) return $datetime;
else return new static($datetime);
}
static function ensure(&$datetime): void {
$datetime = static::withn($datetime);
}
static function isa($datetime): bool {
if ($datetime === null) return false;
if ($datetime instanceof DateTimeInterface) return true;
if (is_int($datetime)) return true;
if (is_string($datetime)) {
return preg_match(_utils::DMY_PATTERN, $datetime) ||
preg_match(_utils::YMD_PATTERN, $datetime) ||
preg_match(_utils::DMYHIS_PATTERN, $datetime) ||
preg_match(_utils::YMDHISZ_PATTERN, $datetime);
}
if (is_array($datetime)) return _utils::parse_array($datetime) !== null;
return false;
}
static function isa_datetime($datetime, bool $frOnly=false): bool {
if ($datetime === null) return false;
if ($datetime instanceof DateTimeInterface) return true;
if (is_int($datetime)) return true;
if (is_string($datetime)) {
return preg_match(_utils::DMYHIS_PATTERN, $datetime) ||
(!$frOnly && preg_match(_utils::YMDHISZ_PATTERN, $datetime));
}
if (is_array($datetime)) return _utils::parse_array($datetime) !== null;
return false;
}
static function isa_date($date, bool $frOnly=false): bool {
if ($date === null) return false;
if ($date instanceof DateTimeInterface) return true;
if (is_int($date)) return true;
if (is_string($date)) {
return preg_match(_utils::DMY_PATTERN, $date) ||
(!$frOnly && preg_match(_utils::YMD_PATTERN, $date));
}
if (is_array($date)) return _utils::parse_array($date) !== null;
return false;
}
function diff($target, $absolute=false): DateInterval {
return new DateInterval(parent::diff($target, $absolute));
}
function format($format=self::DEFAULT_FORMAT): string {
if (array_key_exists($format, _utils::INT_FORMATS)) {
$format = _utils::INT_FORMATS[$format];
} elseif (array_key_exists($format, _utils::STRING_FORMATS)) {
$format = _utils::STRING_FORMATS[$format];
}
if (is_callable($format)) return $format($this);
else return parent::format($format);
}
function __toString(): string {
return $this->format();
}
function __get($name) {
if (array_key_exists($name, _utils::INT_FORMATS)) {
$format = _utils::INT_FORMATS[$name];
if (is_callable($format)) return intval($format($this));
else return intval($this->format($format));
} elseif (array_key_exists($name, _utils::STRING_FORMATS)) {
$format = _utils::STRING_FORMATS[$name];
if (is_callable($format)) return $format($this);
else return $this->format($format);
}
throw new InvalidArgumentException("Unknown property $name");
}
function getElapsedAt(?DateTimeInterface $now=null, ?int $resolution=null): string {
return Elapsed::format_at($this, $now, $resolution);
}
function getElapsedSince(?DateTimeInterface $now=null, ?int $resolution=null): string {
return Elapsed::format_since($this, $now, $resolution);
}
function getElapsedDelay(?DateTimeInterface $now=null, ?int $resolution=null): string {
return Elapsed::format_delay($this, $now, $resolution);
}
}

View File

@ -1,147 +0,0 @@
<?php
namespace nulib\php\time;
use nulib\str;
class _utils {
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+))?$/';
const YMDHISZ_PATTERN = '/^((?:\d{2})?\d{2})-?(\d{2})-?(\d{2})[tT](\d{2}):?(\d{2}):?(\d{2})?([zZ]|\+\d{2}:?\d{2})?$/';
/** retourner le nombre de secondes depuis minuit */
static function _nbsecs_format(\DateTimeInterface $datetime): string {
[$h, $m, $s] = explode(",", $datetime->format("H,i,s"));
return $h * 3600 + $m * 60 + $s;
}
static function _YmdHMSZ_format(\DateTimeInterface $datetime): string {
$YmdHMS = $datetime->format("Ymd\\THis");
$Z = $datetime->format("P");
if ($Z === "+00:00") $Z = "Z";
return "$YmdHMS$Z";
}
const INT_FORMATS = [
"year" => "Y",
"month" => "m",
"day" => "d",
"hour" => "H",
"minute" => "i",
"second" => "s",
"wday" => "N",
"wnum" => "W",
"nbsecs" => [self::class, "_nbsecs_format"],
];
const STRING_FORMATS = [
"timezone" => "P",
"datetime" => "d/m/Y H:i:s",
"date" => "d/m/Y",
"Ymd" => "Ymd",
"YmdHMS" => "Ymd\\THis",
"YmdHMSZ" => [self::class, "_YmdHMSZ_format"],
];
/**
* corriger une année à deux chiffres qui est située dans le passé et
* retourner l'année à 4 chiffres.
*
* par exemple, si l'année courante est 2019, alors:
* - fix_past_year('18') === '2018'
* - fix_past_year('19') === '1919'
* - fix_past_year('20') === '1920'
*/
static function fix_past_year(int $year): int {
if ($year < 100) {
$y = getdate();
$y = $y["year"];
$r = $y % 100;
$c = $y - $r;
if ($year >= $r) $year += $c - 100;
else $year += $c;
}
return $year;
}
/**
* corriger une année à deux chiffres et retourner l'année à 4 chiffres.
* l'année charnière entre année passée et année future est 70
*
* par exemple, si l'année courante est 2019, alors:
* - fix_past_year('18') === '2018'
* - fix_past_year('19') === '2019'
* - fix_past_year('20') === '2020'
* - fix_past_year('69') === '2069'
* - fix_past_year('70') === '1970'
* - fix_past_year('71') === '1971'
*/
static function fix_any_year(int $year): int {
if ($year < 100) {
$y = intval(date("Y"));
$r = $y % 100;
$c = $y - $r;
if ($year >= 70) $year += $c - 100;
else $year += $c;
}
return $year;
}
static function fix_z(?string $Z): ?string {
$Z = strtoupper($Z);
str::del_prefix($Z, "+");
if (preg_match('/^\d{4}$/', $Z)) {
$Z = substr($Z, 0, 2) . ":" . substr($Z, 2);
}
if ($Z === "Z" || $Z === "UTC" || $Z === "00:00") return "UTC";
return "GMT+$Z";
}
static function get_value(array $datetime, ?string $key, ?string $k, ?int $index) {
$value = null;
if ($value === null && $key !== null) $value = $datetime[$key] ?? null;
if ($value === null && $k !== null) $value = $datetime[$k] ?? null;
if ($value === null && $index !== null) $value = $datetime[$index] ?? null;
return $value;
}
static function parse_int(array $datetime, ?string $key, ?string $k, ?int $index, ?int &$part, bool $required = true, ?int $default = null): bool {
$part = null;
$value = self::get_value($datetime, $key, $k, $index);
if ($value === null) {
if ($required && $default === null) return false;
$part = $default;
return true;
}
if (is_numeric($value)) {
$part = intval($value);
return true;
}
return false;
}
static function parse_str(array $datetime, ?string $key, ?string $k, ?int $index, ?string &$part, bool $required = true, ?string $default = null): bool {
$part = null;
$value = self::get_value($datetime, $key, $k, $index);
if ($value === null) {
if ($required && $default === null) return false;
$part = $default;
return true;
}
if (is_string($value)) {
$part = $value;
return true;
}
return false;
}
static function parse_array(array $datetime): ?array {
if (!self::parse_int($datetime, "year", "Y", 0, $year)) return null;
if (!self::parse_int($datetime, "month", "m", 1, $month)) return null;
if (!self::parse_int($datetime, "day", "d", 2, $day)) return null;
self::parse_int($datetime, "hour", "H", 3, $hour, false);
self::parse_int($datetime, "minute", "M", 4, $minute, false);
self::parse_int($datetime, "second", "S", 5, $second, false);
self::parse_str($datetime, "tz", null, 6, $tz, false);
return [$year, $month, $day, $hour, $minute, $second, $tz];
}
}

View File

@ -6,11 +6,11 @@ namespace nulib\ref\cli;
*/
class ref_args {
const DEFS_SCHEMA = [
"merges" => ["?array", null, "liste de tableaux contenant des paramètres et des options par défaut"],
"merge" => ["?array", null, "tableau contenant des paramètres et des options par défaut",
# si merges et merge sont spécifiés tous les deux, "merge" est mergé après "merges"
"set_defaults" => [null, null, "tableau contenant des paramètres et des options par défaut"],
"merge_arrays" => [null, null, "liste de tableaux à merger à celui-ci avant de calculer la liste effective des options"],
"merge" => [null, null, "tableau à merger à celui-ci avant de calculer la liste effective des options",
# si merge_arrays et merge sont spécifiés tous les deux, "merge" est mergé après "merge_arrays"
],
"merge_after" => ["?array", null, "tableau contenant des paramètres et des options supplémentaires"],
"prefix" => [null, null, "texte à afficher avant l'aide générée automatiquement"],
"name" => [null, null, "nom du programme, utilisé pour l'affichage de l'aide"],
"purpose" => [null, null, "courte description de l'objet de ce programme"],
@ -51,34 +51,34 @@ class ref_args {
];
const DEF_SCHEMA = [
"merges" => ["array", null, "liste de tableaux contenant des paramètres et des options par défaut"],
"merge" => ["array", null, "tableau contenant des paramètres et des options par défaut",
# si merges et merge sont spécifiés tous les deux, "merge" est mergé après "merges"
"set_defaults" => [null, null, "tableau contenant des paramètres par défaut"],
"merge_arrays" => [null, null, "liste de tableaux à merger à celui-ci"],
"merge" => [null, null, "tableau à merger à celui-ci",
# si merge_arrays et merge sont spécifiés tous les deux, "merge" est mergé après "merge_arrays"
],
"merge_after" => ["array", null, "tableau contenant des paramètres et des options supplémentaires"],
"extends" => ["string", null, "option que cette définition étend"],
"add" => ["array", null, "options à rajouter"],
"remove" => ["array", null, "options à enlever"],
"show" => ["bool", true, "faut-il afficher cette option par défaut?"],
"disabled" => ["bool", false, "cette option est-elle désactivée?"],
"arg" => ["?string|int|bool", null, "type de l'argument attendu par l'option"],
"args" => ["?array", null, "type des arguments attendus par l'option",
"kind" => [null, null, "type de définition: 'option' ou 'command'"],
"arg" => [null, null, "type de l'argument attendu par l'option"],
"args" => [null, null, "type des arguments attendus par l'option",
# si args est spécifié, arg est ignoré
],
"argsdesc" => ["?string", null, "description textuelle des arguments, utilisé pour l'affichage de l'aide"],
"type" => ["schema", null, "type dans lequel convertir les arguments avant de les fournir à l'utilisateur"],
"ensure_array" => ["bool", false, "forcer la destination à être un tableau"],
"action" => ["callable", null, "fonction à appeler quand cette option est utilisée",
"argsdesc" => [null, null, "description textuelle des arguments, utilisé pour l'affichage de l'aide"],
"type" => [null, null, "types dans lesquels convertir les arguments avant de les fournir à l'utilisateur"],
"action" => [null, null, "fonction à appeler quand cette option est utilisée",
# la signature de la fonction est ($value, $name, $arg, $dest, $def)
],
"inverse" => ["bool", false, "décrémenter la destination au lieu de l'incrémenter pour une option sans argument"],
"name" => ["?string", null, "propriété ou clé à initialiser en réponse à l'utilisation de cette option",
"name" => [null, null, "propriété ou clé à initialiser en réponse à l'utilisation de cette option",
# le nom à spécifier est au format under_score, qui est transformée en camelCase si la destination est un objet
],
"property" => ["?string", null, "comme name mais force l'utilisation d'une propriété"],
"key" => ["?key", null, "comme name mais force l'utilisation d'une clé"],
"property" => [null, null, "comme name mais force l'utilisation d'une propriété"],
"key" => [null, null, "comme name mais force l'utilisation d'une clé"],
"inverse" => ["bool", false, "décrémenter la destination au lieu de l'incrémenter pour une option sans argument"],
"value" => ["mixed", null, "valeur à forcer au lieu d'incrémenter la destination"],
"ensure_array" => [null, null, "forcer la destination à être un tableau"],
"help" => [null, null, "description de cette option, utilisé pour l'affichage de l'aide"],
"cmd_args" => [null, null, "définition des sous-options pour une commande"],
# ces valeurs sont calculées
"cmd_defs" => [null, null, "(interne) liste des définitions correspondant au paramètre options"],
];
const ARGS_ALLOWED_VALUES = ["value", "path", "dir", "file", "host"];

View File

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

View File

@ -1,5 +1,5 @@
<?php
namespace nulib\ref;
namespace nulib\ref\php;
class ref_func {
const CALL_ALL_PARAMS_SCHEMA = [

View File

@ -1,11 +0,0 @@
<?php
namespace nulib\ref;
/**
* Class ref_cache: référence des durées de mise en cache
*/
class ref_cache {
const MINUTE = 60;
const HOUR = 60 * self::MINUTE;
const DAY = 24 * self::HOUR;
}

View File

@ -1,15 +0,0 @@
<?php
namespace nulib\ref;
class ref_jquery {
const HAVE_JQUERY = true;
function printJquery(): void {
?>
<script type="text/javascript">
jQuery.noConflict()(function($) {
});
</script>
<?php
}
}

View File

@ -1,22 +0,0 @@
<?php
namespace nulib\ref;
/**
* Class ref_profiles: noms de profils normalisés
*/
class ref_profiles {
const PROD = "prod";
const TEST = "test";
const DEVEL = "devel";
const PROFILES = [
self::PROD,
self::TEST,
self::DEVEL,
];
const PRODUCTION_MODES = [
self::PROD => true,
self::TEST => true,
];
}

View File

@ -1,5 +1,5 @@
<?php
namespace nulib\ref;
namespace nulib\ref\web;
class ref_mimetypes {
const TXT = "text/plain";

View File

@ -438,7 +438,7 @@ class str {
} elseif (preg_match(self::CAMEL_PATTERN2, $camel, $ms, PREG_OFFSET_CAPTURE)) {
# préfixe en minuscule
} else {
throw exceptions::invalid_type($camel, $kind, "camel string");
throw ValueException::invalid_kind($camel, "camel string");
}
$parts[] = strtolower($ms[1][0]);
$index = intval($ms[1][1]) + strlen($ms[1][0]);

View File

@ -28,23 +28,17 @@ use nulib\ValueException;
*/
class Word {
/** @var bool le mot est-il féminin? */
private bool $fem;
/** @var string article "aucun", "aucune" */
private ?string $aucun;
/** @var string article "un", "une" */
private ?string $un;
private $fem;
/** @var string article "le", "la", "l'" */
private ?string $le;
private $le;
/** @var string article "ce", "cet", "cette" */
private ?string $ce;
/** @var string article "de", "d'" */
private ?string $de;
private $ce;
/** @var string article "du", "de la", "de l'" */
private ?string $du;
private $du;
/** @var string article "au", "à la", "à l'" */
private ?string $au;
private $au;
/** @var string le mot sans article */
private string $w;
private $w;
function __construct(string $spec, bool $adjective=false) {
if (preg_match('/^f([eé]m(inin)?)?\s*:\s*/iu', $spec, $ms)) {
@ -63,40 +57,28 @@ class Word {
$fem = null;
}
if (preg_match('/^l\'\s*/i', $spec, $ms) && $fem !== null) {
$aucun = $fem? "aucune ": "aucun ";
$un = $fem? "une ": "un ";
$le = "l'";
$ce = "cet ";
$de = "d'";
$du = "de l'";
$au = "à l'";
$spec = substr($spec, strlen($ms[0]));
} elseif (preg_match('/^la\s+/i', $spec, $ms)) {
$fem = true;
$aucun = "aucune ";
$un = "une ";
$le = "la ";
$ce = "cette ";
$de = "de ";
$du = "de la ";
$au = "à la ";
$spec = substr($spec, strlen($ms[0]));
} elseif (preg_match('/^le\s+/i', $spec, $ms)) {
$fem = false;
$aucun = "aucun ";
$un = "un ";
$le = "le ";
$ce = "ce ";
$de = "de ";
$du = "du ";
$au = "au ";
$spec = substr($spec, strlen($ms[0]));
} else {
$aucun = null;
$un = null;
$le = null;
$ce = null;
$de = null;
$du = null;
$au = null;
}
@ -104,29 +86,18 @@ class Word {
# 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) {
} elseif ($le === null || $du === null || $au === null) {
throw new ValueException("Vous devez spécifier l'article du nom");
}
}
$this->fem = $fem;
$this->aucun = $aucun;
$this->un = $un;
$this->le = $le;
$this->ce = $ce;
$this->de = $de;
$this->du = $du;
$this->au = $au;
$this->w = $spec;
}
function isMasculin(): bool {
return !$this->fem;
}
function isFeminin(): bool {
return $this->fem;
}
/**
* retourner le mot sans article
*
@ -197,25 +168,15 @@ class Word {
return "$amount/$max ".$this->w($amount);
}
function pronom(): string {
return $this->fem? "elle": "il";
}
function articleAucun(): ?string {
return $this->un;
}
function articleUn(): ?string {
return $this->un;
}
/** retourner le mot avec l'article indéfini et la quantité */
function un(int $amount=1): string {
$abs_amount = abs($amount);
if ($abs_amount == 0) {
return $this->aucun.$this->w($amount);
$aucun = $this->fem? "aucune ": "aucun ";
return $aucun.$this->w($amount);
} elseif ($abs_amount == 1) {
return $this->un.$this->w($amount);
$un = $this->fem? "une ": "un ";
return $un.$this->w($amount);
} else {
return "les $amount ".$this->w($amount);
}
@ -232,10 +193,6 @@ class Word {
}
}
function articleLe(): ?string {
return $this->le;
}
function le(int $amount=1): string {
$abs_amount = abs($amount);
if ($abs_amount == 0) {
@ -257,10 +214,6 @@ class Word {
}
}
function articleCe(): string {
return $this->ce;
}
function ce(int $amount=1): string {
$abs_amount = abs($amount);
if ($abs_amount == 0) {
@ -282,18 +235,6 @@ class Word {
}
}
function articleDe(): string {
return $this->de;
}
function _de(int $amount=1): string {
return $this->de.$this->w($amount);
}
function articleDu(): string {
return $this->du;
}
function du(int $amount=1): string {
$abs_amount = abs($amount);
if ($abs_amount == 0) {
@ -315,10 +256,6 @@ class Word {
}
}
function articleAu(): string {
return $this->au;
}
function au(int $amount=1): string {
$abs_amount = abs($amount);
if ($abs_amount == 0) {

View File

@ -5,8 +5,9 @@ use nulib\UserException;
use Throwable;
class CurlException extends UserException {
function __construct($ch, $userMessage=null, $code=0, ?Throwable $previous=null) {
$userMessage ??= "erreur curl inconnue";
function __construct($ch, ?string $message=null, $code=0, ?Throwable $previous=null) {
if ($message === null) $message = "(unknown error)";
$userMessage = $message;
$techMessage = null;
if ($ch !== null) {
$parts = [];
@ -16,7 +17,6 @@ class CurlException extends UserException {
if ($error != "") $parts[] = "error: $error";
if ($parts) $techMessage = implode(", ", $parts);
}
parent::__construct($userMessage, $code, $previous);
$this->setTechMessage($techMessage);
parent::__construct($userMessage, $techMessage, $code, $previous);
}
}

View File

@ -12,11 +12,11 @@ class curl {
if (!isset($curlOptions[CURLOPT_RETURNTRANSFER])) $curlOptions[CURLOPT_RETURNTRANSFER] = true;
$extractHeaders = isset($curlOptions[CURLOPT_HEADER]) && $curlOptions[CURLOPT_HEADER];
$ch = curl_init();
if ($ch === false) throw new CurlException(null, "erreur curl lors de l'initialisation");
if ($ch === false) throw new CurlException(null, "init");
curl_setopt_array($ch, $curlOptions);
try {
$result = curl_exec($ch);
if ($result === false) throw new CurlException($ch, "erreur curl lors du téléchargement");
if ($result === false) throw new CurlException($ch);
if ($extractHeaders) {
$info = curl_getinfo($ch);
$headersSize = $info["header_size"];

View File

@ -1,5 +0,0 @@
# -*- coding: utf-8 mode: yaml -*- vim:sw=2:sts=2:et:ai:si:sta:fenc=utf-8
app:
mailer:
host: maildev.devel.self

View File

@ -1,7 +0,0 @@
# -*- coding: utf-8 mode: yaml -*- vim:sw=2:sts=2:et:ai:si:sta:fenc=utf-8
app:
mailer:
host: maildev.univ-reunion.fr
username: mel
password: gibson

View File

@ -3,7 +3,6 @@
require __DIR__.'/../vendor/autoload.php';
use nulib\app\cli\Application;
use nulib\cv;
use nulib\mail\mailer;
use nulib\ValueException;
@ -21,8 +20,10 @@ Application::run(new class extends Application {
protected $to, $cc, $bcc, $from;
function main() {
$subject = cv::not_null($this->args[0] ?? null, "subject");
$body = cv::not_null($this->args[1] ?? null, "body");
$subject = $this->args[0] ?? null;
ValueException::check_null($subject, "subject");
$body = $this->args[1] ?? null;
ValueException::check_null($body, "body");
mailer::send($this->to, $subject, $body, $this->cc, $this->bcc, $this->from);
}
});

View File

@ -1,73 +0,0 @@
#!/usr/bin/php
<?php
require __DIR__.'/../vendor/autoload.php';
use nulib\cache\DataCacheData;
use nulib\cache\CacheFile;
use nulib\ext\yaml;
use nulib\os\sh;
function show(string $prefix, CacheFile $cache, bool $dumpInfos=true): void {
Txx("$prefix=", $cache->get());
if ($dumpInfos) {
yaml::dump($cache->getInfos());
}
}
//system("rm -f *.cache .*.cache");
$what = [
"null",
"one",
"two",
"three",
];
$duration = 10;
if (in_array("null", $what)) {
$null = new CacheFile("null", null, [
"duration" => $duration,
]);
show("null", $null);
}
if (in_array("one", $what)) {
$one = new class("one", null, [
"duration" => $duration,
]) extends CacheFile {
protected function compute() {
return 1;
}
};
show("one", $one);
}
if (in_array("two", $what)) {
$two = new CacheFile("two", new DataCacheData(null, function () {
return 2;
}), [
"duration" => $duration,
]);
show("two", $two);
}
if (in_array("three", $what)) {
$data31 = new DataCacheData("data31name", function () {
return 31;
});
$data32 = new DataCacheData(null, function () {
return 32;
});
$three = new CacheFile("three", [
"data31" => $data31,
$data31, # name=data31name
"data32" => $data32,
$data32, # name=""
]);
Txx("three.0=", $three->get("data31"));
Txx("three.1=", $three->get("data31name"));
Txx("three.2=", $three->get("data32"));
Txx("three.3=", $three->get(""));
}

View File

@ -1,65 +0,0 @@
#!/usr/bin/php
<?php
require __DIR__."/../vendor/autoload.php";
use nulib\app\cli\Application;
use nulib\exceptions;
use nulib\output\msg;
use nulib\UserException;
Application::run(new class extends Application {
const ARGS = [
"purpose" => "tester l'affichage des exception",
"merge" => parent::ARGS,
];
function fart(): void {
throw new RuntimeException("fart");
}
function prout(): void {
try {
$this->fart();
} catch (Exception $e) {
throw new RuntimeException("prout", $e->getCode(), $e);
}
}
function main() {
try {
throw new Exception("exception normale");
} catch (Exception $e) {
msg::info("summary: ". exceptions::get_summary($e));
msg::error($e);
}
try {
try {
$this->prout();
} catch (Exception $e) {
throw new Exception("exception normale", $e->getCode(), $e);
}
} catch (Exception $e) {
msg::info("summary: ". exceptions::get_summary($e));
msg::error($e);
}
try {
throw exceptions::invalid_value("valeur", $kind)
->setTechMessage("message technique");
} catch (Exception $e) {
msg::info("summary: ". exceptions::get_summary($e));
msg::error($e);
}
try {
try {
$this->prout();
} catch (Exception $e) {
throw exceptions::invalid_value("valeur", $kind, null, $e)
->setTechMessage("message technique");
}
} catch (Exception $e) {
msg::info("summary: ". exceptions::get_summary($e));
msg::error($e);
}
}
});

View File

@ -5,7 +5,7 @@ use nulib\cl;
use nulib\db\Capacitor;
use nulib\db\CapacitorChannel;
use nulib\db\mysql\Mysql;
use nulib\db\mysql\MysqlStorage;
use nulib\db\mysql\MysqlCapacitor;
use nulib\output\msg;
use nulib\output\std\StdMessenger;
@ -35,7 +35,7 @@ class MyChannel extends CapacitorChannel {
}
}
new Capacitor(new MysqlStorage($db), $channel = new MyChannel());
new Capacitor(new MysqlCapacitor($db), $channel = new MyChannel());
$channel->charge("hello world");
$channel->charge(["bonjour monde"]);

View File

@ -5,7 +5,7 @@ use nulib\cl;
use nulib\db\Capacitor;
use nulib\db\CapacitorChannel;
use nulib\db\pgsql\Pgsql;
use nulib\db\pgsql\PgsqlStorage;
use nulib\db\pgsql\PgsqlCapacitor;
$db = new Pgsql([
"host" => "pegase-dre.self",
@ -34,7 +34,7 @@ class MyChannel extends CapacitorChannel {
}
}
new Capacitor(new PgsqlStorage($db), $channel = new MyChannel());
new Capacitor(new PgsqlCapacitor($db), $channel = new MyChannel());
$channel->charge("hello world");
$channel->charge(["bonjour monde"]);

View File

@ -5,7 +5,7 @@ use nulib\cl;
use nulib\db\Capacitor;
use nulib\db\CapacitorChannel;
use nulib\db\sqlite\Sqlite;
use nulib\db\sqlite\SqliteStorage;
use nulib\db\sqlite\SqliteCapacitor;
$db = new Sqlite(__DIR__.'/test_sqlite.db');
@ -27,7 +27,7 @@ class MyChannel extends CapacitorChannel {
}
}
new Capacitor(new SqliteStorage($db), $channel = new MyChannel());
new Capacitor(new SqliteCapacitor($db), $channel = new MyChannel());
$channel->charge("hello world");
$channel->charge(["bonjour monde"]);

View File

@ -1,6 +1,7 @@
<?php
namespace nulib\app\args;
namespace nulib\app\cli;
use nulib\app\args\Aodef;
use nulib\tests\TestCase;
class AodefTest extends TestCase {

View File

@ -1,6 +1,9 @@
<?php
namespace nulib\app\args;
namespace nulib\app\cli;
use nulib\app\args\Aogroup;
use nulib\app\args\Aolist;
use nulib\app\args\Aosection;
use nulib\tests\TestCase;
class AolistTest extends TestCase {

View File

@ -1,5 +1,5 @@
<?php
namespace nulib\app\args;
namespace nulib\app\cli;
use nulib\app\args\SimpleAolist;
use nulib\tests\TestCase;

View File

@ -1,5 +1,5 @@
<?php
namespace nulib\app\args;
namespace nulib\app\cli;
use nulib\app\args\SimpleArgsParser;
use nulib\tests\TestCase;
@ -172,28 +172,4 @@ class SimpleArgsParserTest extends TestCase {
self::assertTrue(true);
}
function testAutono() {
$parser = new SimpleArgsParser([
["-a", "--plouf"],
["-b", "--no-plouf"],
]);
$dest = [];
$parser->parse($dest, ["-aabb"]);
self::assertSame(["plouf" => 0, "args" => []], $dest);
$parser = new SimpleArgsParser([
["-a", "--plouf", "value" => true],
["-b", "--no-plouf", "value" => false],
]);
$dest = ["plouf" => null];
$parser->parse($dest, []);
self::assertSame(["plouf" => null, "args" => []], $dest);
$dest = ["plouf" => null];
$parser->parse($dest, ["-a"]);
self::assertSame(["plouf" => true, "args" => []], $dest);
$dest = ["plouf" => null];
$parser->parse($dest, ["-b"]);
self::assertSame(["plouf" => false, "args" => []], $dest);
}
}

View File

@ -1,38 +0,0 @@
<?php
namespace nulib\cache;
use nulib\output\msg;
class CursorChannelTest extends _TestCase {
const DATA = [
"fr" => ["a" => "un", "b" => "deux"],
"eng" => ["a" => "one", "b" => "two"],
["a" => 1, "b" => 2],
];
function testUsage() {
$channel = CursorChannel::with("numbers", self::DATA, self::$storage);
$count = 0;
foreach ($channel as $key => $item) {
msg::info("one: $key => {$item["a"]}");
$count++;
}
self::assertSame(3, $count);
}
function testAddColumns() {
$channel = (new class("numbers") extends CursorChannel {
const NAME = "numbersac";
const TABLE_NAME = self::NAME;
const ADD_COLUMNS = [
"a" => "varchar(30)",
];
})->initStorage(self::$storage)->rechargeAll(self::DATA);
$count = 0;
foreach ($channel as $key => $item) {
msg::info("one: $key => {$item["a"]}");
$count++;
}
self::assertSame(3, $count);
}
}

View File

@ -1,22 +0,0 @@
<?php
namespace nulib\cache;
use nulib\db\sqlite\Sqlite;
class SourceDb extends Sqlite {
const MIGRATION = [
"create table source (pk integer primary key autoincrement, s varchar(255), i integer, b boolean)",
[self::class, "fill_data"],
];
static function fill_data(Sqlite $db): void {
$db->exec("insert into source (s, i, b) values (null, null, null)");
$db->exec("insert into source (s, i, b) values ('false', 0, 0)");
$db->exec("insert into source (s, i, b) values ('first', 1, 1)");
$db->exec("insert into source (s, i, b) values ('second', 2, 1)");
}
public function __construct() {
parent::__construct(__DIR__."/source.db");
}
}

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