Compare commits

...

59 Commits

Author SHA1 Message Date
3c8ef57246 ajout install.sh 2025-08-22 12:27:34 +04:00
529633e3ba modifs.mineures sans commentaires 2025-08-21 18:39:46 +04:00
7d4c385492 afficher correctement une option facultative 2025-08-20 13:22:10 +04:00
26821f2a1a renommer TRUE en REAL 2025-08-20 13:03:14 +04:00
94feff75db calcul de MYTRUESELF 2025-08-20 13:01:26 +04:00
368de874b4 corriger ensure pour utiliser query 2025-08-20 10:37:32 +04:00
7587572f2c modifs.mineures sans commentaires 2025-08-11 12:23:43 +04:00
4208412189 modifs.mineures sans commentaires 2025-07-20 17:10:05 +04:00
52736d8af0 maj TODO 2025-07-17 12:04:16 +04:00
4a9eecbda8 modifs.mineures sans commentaires 2025-07-16 17:54:26 +04:00
8cbdf25372 modifs.mineures sans commentaires 2025-07-16 17:25:19 +04:00
c7e6571c08 méthode pour vérifier la validité d'une connexion 2025-07-16 11:51:42 +04:00
f221342c09 support arrêt de each() 2025-07-15 16:50:24 +04:00
6238e0b9ff modifs.mineures sans commentaires 2025-07-15 14:18:45 +04:00
8cc8baca10 ne plus changer la session par défaut 2025-07-15 10:03:11 +04:00
8f5c30c8c1 forcer le timeout au début 2025-07-10 18:06:19 +04:00
ad801d8486 modifs.mineures sans commentaires 2025-07-09 17:49:05 +04:00
2c45aa677a support distinc dans _select 2025-07-07 21:52:08 +04:00
1f14faf08c dbAll() support distinct 2025-07-07 21:42:25 +04:00
aeaf3eb1dd modifs.mineures sans commentaires 2025-07-02 10:51:52 +04:00
e3a9d342d0 modifs.mineures sans commentaires 2025-07-01 17:54:06 +04:00
9107399805 modifs.mineures sans commentaires 2025-07-01 15:02:02 +04:00
c0f330ca82 modifs.mineures sans commentaires 2025-06-30 14:28:49 +04:00
1b8b3cf27f modifs.mineures sans commentaires 2025-06-29 18:08:11 +04:00
48941f9f90 modifs.mineures sans commentaires 2025-06-27 18:30:15 +04:00
792e1ff965 ajout dbAll, dbOne 2025-06-25 17:14:19 +04:00
e90d5cf883 modifs.mineures sans commentaires 2025-06-25 10:26:19 +04:00
12fe3a65a5 bug 2025-06-25 08:43:58 +04:00
3e642b84bc runphp fait aussi wrapper 2025-06-25 06:39:40 +04:00
599d646372 pas d'analyse d'argument pour ci, cu, composer 2025-06-25 06:02:38 +04:00
933b6cec3b modifs.mineures sans commentaires 2025-06-24 15:47:45 +04:00
4f17d19609 support préfixe pour les migrations 2025-06-24 10:38:10 +04:00
b71e879823 la datetime est dans le timezone par défaut 2025-06-24 04:13:09 +04:00
2c7020f44d filtre de base 2025-06-24 02:18:18 +04:00
f34694e12d ajout de chargeAll() 2025-06-23 22:40:26 +04:00
5dc2d3d019 support content-length 2025-06-23 18:20:40 +04:00
ce337ce3bc modifs.mineures sans commentaires 2025-06-20 10:41:52 +04:00
0945a86763 maj doc 2025-06-19 17:42:18 +04:00
6bcd8d4cf6 ajout getCount() 2025-06-19 17:02:56 +04:00
70c82ae504 ajout jsondiff 2025-06-19 11:24:18 +04:00
8b7b318acd changement majeur dans Capacitor 2025-06-17 13:47:59 +04:00
9cf7ac908f possibilité de skip les premières lignes 2025-06-16 23:15:02 +04:00
a44da62b94 ajout genbool 2025-06-16 17:56:20 +04:00
04e7dab54e ajout string_reader 2025-06-16 17:56:16 +04:00
d704ce8c07 possibilité de changer items dans getItemValues 2025-06-12 15:54:20 +04:00
a82143a41b modifs.mineures sans commentaires 2025-06-12 08:08:51 +04:00
0d47b2a757 modifs.mineures sans commentaires 2025-06-05 15:38:06 +04:00
8fcf865bf0 maj projet 2025-06-05 14:56:10 +04:00
0467934a82 bug 2025-06-05 14:16:51 +04:00
5f4cdd2a76 bug 2025-06-05 12:08:52 +04:00
2f17d85121 ajout uuid 2025-06-05 11:56:38 +04:00
5ecc9ac3d7 ajout colonnes génériques 2025-06-04 19:35:23 +04:00
88436296e7 maj projet 2025-06-04 19:35:14 +04:00
8cab7b6064 modifs.mineures sans commentaires 2025-06-04 17:28:44 +04:00
9676be2dd6 inclure les template phpwrappers 2025-06-04 17:27:25 +04:00
c8f7f7cbaa maj doc 2025-06-04 16:28:04 +04:00
7b118682db maj doc 2025-06-03 10:24:32 +04:00
add9cfde3b <pman>Init changelog & version 0.6.1p82 2025-06-03 10:23:29 +04:00
2c63bcc1b3 <pman>Intégration de la branche rel74-0.6.1 2025-06-03 10:21:19 +04:00
74 changed files with 1539 additions and 656 deletions

1
.idea/nulib-base.iml generated

@ -4,6 +4,7 @@
<content url="file://$MODULE_DIR$"> <content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/php/src" isTestSource="false" packagePrefix="nulib\" /> <sourceFolder url="file://$MODULE_DIR$/php/src" isTestSource="false" packagePrefix="nulib\" />
<sourceFolder url="file://$MODULE_DIR$/php/tests" isTestSource="true" packagePrefix="nulib\" /> <sourceFolder url="file://$MODULE_DIR$/php/tests" isTestSource="true" packagePrefix="nulib\" />
<sourceFolder url="file://$MODULE_DIR$/tests" isTestSource="true" />
<excludeFolder url="file://$MODULE_DIR$/php/vendor" /> <excludeFolder url="file://$MODULE_DIR$/php/vendor" />
</content> </content>
<orderEntry type="inheritedJdk" /> <orderEntry type="inheritedJdk" />

14
.idea/php-test-framework.xml generated Normal file

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="PhpTestFrameworkVersionCache">
<tools_cache>
<tool tool_name="PHPUnit">
<cache>
<versions>
<info id="Local/php/vendor/autoload.php" version="9.6.23" />
</versions>
</cache>
</tool>
</tools_cache>
</component>
</project>

12
.idea/php.xml generated

@ -1,5 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="MessDetector">
<phpmd_settings>
<phpmd_by_interpreter asDefaultInterpreter="true" interpreter_id="846389f7-9fb5-4173-a868-1dc6b8fbb3fa" timeout="30000" />
</phpmd_settings>
</component>
<component name="MessDetectorOptionsConfiguration"> <component name="MessDetectorOptionsConfiguration">
<option name="transferred" value="true" /> <option name="transferred" value="true" />
</component> </component>
@ -10,6 +15,11 @@
<option name="highlightLevel" value="WARNING" /> <option name="highlightLevel" value="WARNING" />
<option name="transferred" value="true" /> <option name="transferred" value="true" />
</component> </component>
<component name="PhpCodeSniffer">
<phpcs_settings>
<phpcs_by_interpreter asDefaultInterpreter="true" interpreter_id="846389f7-9fb5-4173-a868-1dc6b8fbb3fa" timeout="30000" />
</phpcs_settings>
</component>
<component name="PhpIncludePathManager"> <component name="PhpIncludePathManager">
<include_path> <include_path>
<path value="$PROJECT_DIR$/php/vendor/symfony/polyfill-ctype" /> <path value="$PROJECT_DIR$/php/vendor/symfony/polyfill-ctype" />
@ -55,7 +65,7 @@
</component> </component>
<component name="PhpUnit"> <component name="PhpUnit">
<phpunit_settings> <phpunit_settings>
<PhpUnitSettings configuration_file_path="$PROJECT_DIR$/php/vendor/sebastian/object-enumerator/phpunit.xml" custom_loader_path="$PROJECT_DIR$/vendor/autoload.php" use_configuration_file="true" /> <PhpUnitSettings custom_loader_path="$PROJECT_DIR$/php/vendor/autoload.php" phpunit_phar_path="" />
</phpunit_settings> </phpunit_settings>
</component> </component>
<component name="PsalmOptionsConfiguration"> <component name="PsalmOptionsConfiguration">

10
.idea/phpunit.xml generated Normal file

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

2
.udir

@ -9,7 +9,7 @@ uinc_options=()
uinc_args=() uinc_args=()
preconfig_scripts=() preconfig_scripts=()
configure_variables=(dest) configure_variables=(dest)
configure_dest_for=(lib/profile.d/nulib-base) configure_dest_for=(lib/profile.d/nulib)
config_scripts=(lib/uinst/conf) config_scripts=(lib/uinst/conf)
install_profiles=true install_profiles=true
profiledir=lib/profile.d profiledir=lib/profile.d

@ -1,3 +1,5 @@
## Release 0.6.1p82 du 03/06/2025-10:22
## Release 0.6.1p74 du 03/06/2025-10:21 ## Release 0.6.1p74 du 03/06/2025-10:21
* `51ce95e` maj doc * `51ce95e` maj doc

@ -38,6 +38,8 @@ echo "commit=$commit"
# reprendre la valeur affichée par la précédente commande # reprendre la valeur affichée par la précédente commande
commit=XXX commit=XXX
pu
git checkout dev74 git checkout dev74
git cherry-pick "$commit" git cherry-pick "$commit"

16
TODO.md

@ -1,5 +1,6 @@
# nulib # nulib/bash
* [nulib/bash](bash/TODO.md)
* runners * runners
* [ ] rnlphp -- lancer un programme php avec la bonne version (+docker le cas échéant) * [ ] rnlphp -- lancer un programme php avec la bonne version (+docker le cas échéant)
* [ ] utilisable en shebang * [ ] utilisable en shebang
@ -12,4 +13,17 @@
* [ ] rnlsh -- lancer un shell avec les librairies bash / lancer un script * [ ] rnlsh -- lancer un shell avec les librairies bash / lancer un script
* MYTRUEDIR, MYTRUENAME, MYTRUESELF -- résoudre les liens symboliques * MYTRUEDIR, MYTRUENAME, MYTRUESELF -- résoudre les liens symboliques
# nulib/php
* [nulib](php/src/TODO.md)
* [nulib\app](php/src/app/TODO.md)
* [nulib\db](php/src/db/TODO.md)
* [nulib\os](php/src/os/TODO.md)
* [nulib\output](php/src/output/TODO.md)
* [nulib\php\time](php/src/php/time/TODO.md)
vrac:
* PID dans les logs
* build --ci
-*- coding: utf-8 mode: markdown -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8:noeol:binary -*- coding: utf-8 mode: markdown -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8:noeol:binary

@ -6,7 +6,8 @@ function __esection() {
local length="${COLUMNS:-80}" local length="${COLUMNS:-80}"
setx lsep=__complete "$prefix" "$length" - setx lsep=__complete "$prefix" "$length" -
recho "$COULEUR_BLEUE$lsep$COULEUR_NORMALE" recho "
$COULEUR_BLEUE$lsep$COULEUR_NORMALE"
[ -n "$*" ] || return 0 [ -n "$*" ] || return 0
length=$((length - 1)) length=$((length - 1))
setx -a lines=echo "$1" setx -a lines=echo "$1"

@ -6,7 +6,8 @@ function __esection() {
local length="${COLUMNS:-80}" local length="${COLUMNS:-80}"
setx lsep=__complete "$prefix" "$length" - setx lsep=__complete "$prefix" "$length" -
recho "$lsep" recho "
$lsep"
[ -n "$*" ] || return 0 [ -n "$*" ] || return 0
length=$((length - 1)) length=$((length - 1))
setx -a lines=echo "$1" setx -a lines=echo "$1"

@ -184,7 +184,7 @@ function __nulib_args_parse_args() {
*) die "Invalid arg definition: expected option, got '$1'" || return;; *) die "Invalid arg definition: expected option, got '$1'" || return;;
esac esac
# est-ce que l'option prend un argument? # est-ce que l'option prend un argument?
local __def __longdef __witharg __valdesc local __def __longdef __witharg __valdesc __defvaldesc
__witharg= __witharg=
for __def in "${__defs[@]}"; do for __def in "${__defs[@]}"; do
if [ "${__def%::*}" != "$__def" ]; then if [ "${__def%::*}" != "$__def" ]; then
@ -346,16 +346,19 @@ $prefix$usage"
fi fi
# est-ce que l'option prend un argument? # est-ce que l'option prend un argument?
__witharg= __witharg=
__valdesc=value __valdesc=
__defvaldesc=value
for __def in "${__defs[@]}"; do for __def in "${__defs[@]}"; do
if [ "${__def%::*}" != "$__def" ]; then if [ "${__def%::*}" != "$__def" ]; then
[ "$__witharg" != : ] && __witharg=:: [ "$__witharg" != : ] && __witharg=::
[ -n "${__def#*::}" ] && __valdesc="[${__def#*::}]" [ -n "${__def#*::}" ] && __valdesc="[${__def#*::}]"
__defvaldesc="[value]"
elif [ "${__def%:*}" != "$__def" ]; then elif [ "${__def%:*}" != "$__def" ]; then
__witharg=: __witharg=:
[ -n "${__def#*:}" ] && __valdesc="${__def#*:}" [ -n "${__def#*:}" ] && __valdesc="${__def#*:}"
fi fi
done done
[ -n "$__valdesc" ] || __valdesc="$__defvaldesc"
# description de l'option # description de l'option
local first=1 thelp tdesc local first=1 thelp tdesc
for __def in "${__defs[@]}"; do for __def in "${__defs[@]}"; do

@ -21,6 +21,13 @@ if [ -z "$NULIB_NO_INIT_ENV" ]; then
fi fi
[ -n "$NULIBDIR" ] || NULIBDIR="$MYDIR" [ -n "$NULIBDIR" ] || NULIBDIR="$MYDIR"
# Si le script courant est un lien, calculer le répertoire destination
if [ -n "$MYREALSELF" -a -n "$MYSELF" ]; then
MYREALSELF="$(readlink -f "$MYSELF")"
MYREALDIR="$(dirname -- "$MYREALSELF")"
MYREALNAME="$(basename -- "$MYREALSELF")"
fi
# Repertoire temporaire # Repertoire temporaire
[ -z "$TMPDIR" -a -d "$HOME/tmp" ] && TMPDIR="$HOME/tmp" [ -z "$TMPDIR" -a -d "$HOME/tmp" ] && TMPDIR="$HOME/tmp"
[ -z "$TMPDIR" ] && TMPDIR="${TMP:-${TEMP:-/tmp}}" [ -z "$TMPDIR" ] && TMPDIR="${TMP:-${TEMP:-/tmp}}"

147
bash/src/install.sh Normal file

@ -0,0 +1,147 @@
# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8
##@cooked nocomments
module: install "Outils de haut niveau pour installer des fichiers de configuration"
require: base
if [ -z "$NULIB_INSTALL_CONFIGURED" ]; then
# Faut-il afficher le nom des fichiers copié/créés?
export NULIB_INSTALL_VERBOSE=1
# Faut-il afficher les destinations avec ppath?
export NULIB_INSTALL_USES_PPATH=
fi
export NULIB_INSTALL_CONFIGURED=1
function ensure_exists() {
# Créer le fichier vide "$1" s'il n'existe pas déjà, avec les permissions
# $2(=644). retourner vrai si le fichier a été créé sans erreur
[ -f "$1" ] || {
if [ -n "$NULIB_INSTALL_VERBOSE" ]; then
if [ -n "$NULIB_INSTALL_USES_PPATH" ]; then
estep "$(ppath "$1")"
else
estep "$1"
fi
fi
mkdirof "$1"
local r=0
touch "$1" && chmod "${2:-644}" "$1" || r=$?
return $r
}
return 1
}
function __nulib_install_show_args() {
if [ -z "$NULIB_INSTALL_VERBOSE" ]; then
:
elif [ -n "$NULIB_INSTALL_USES_PPATH" ]; then
estep "$1 --> $(ppath "$2")${3:+/}"
else
estep "$1 --> $2${3:+/}"
fi
}
function copy_replace() {
# Copier de façon inconditionnelle le fichier $1 vers le fichier $2, en
# réinitialisation les permissions à la valeur $3
local src="$1" dest="$2"
local srcname="$(basename -- "$src")"
[ -d "$dest" ] && dest="$dest/$srcname"
mkdirof "$dest" || return 1
if [ -n "$NULIB_INSTALL_VERBOSE" ]; then
local destarg destname slash
destarg="$(dirname -- "$dest")"
destname="$(basename -- "$dest")"
if [ "$srcname" == "$destname" ]; then
slash=1
else
destarg="$destarg/$destname"
fi
__nulib_install_show_args "$srcname" "$destarg" "$slash"
fi
local r=0
if cp "$src" "$dest"; then
if [ -n "$3" ]; then
chmod "$3" "$dest" || r=$?
fi
fi
return $r
}
function copy_new() {
# Copier le fichier "$1" vers le fichier "$2", avec les permissions $3(=644)
# Ne pas écraser le fichier destination s'il existe déjà
# Retourner vrai si le fichier a été copié sans erreur
local src="$1" dest="$2"
[ -d "$dest" ] && dest="$dest/$(basename -- "$src")"
mkdirof "$dest" || return 1
if [ ! -e "$dest" ]; then
copy_replace "$src" "$dest" "$3"
else
return 1
fi
}
function copy_update() {
# Copier le fichier "$1" vers le fichier "$2", si $2 n'existe pas, ou si $1
# a été modifié par rapport à $2. Réinitialiser le cas échéant les
# permissions à la valeur $3
# Retourner vrai si le fichier a été copié sans erreur.
local src="$1" dest="$2"
[ -d "$dest" ] && dest="$dest/$(basename -- "$src")"
mkdirof "$dest" || return 1
if [ ! -e "$dest" ]; then
copy_replace "$src" "$dest" "$3"
elif testdiff "$src" "$dest"; then
copy_replace "$src" "$dest" "$3"
else
return 1
fi
}
COPY_UPDATE_ASK_DEFAULT=
function copy_update_ask() {
# Copier ou mettre à jour le fichier $1 vers le fichier $2.
# Si le fichier existe déjà, la différence est affichée, et une confirmation
# est demandée pour l'écrasement du fichier.
# si $1 commence par '-' alors on s'en sert comme option pour configurer le
# niveau d'interaction pour demander la confirmation. les paramètres sont
# alors décalés
# Retourner vrai si le fichier a été copié sans erreur.
local interopt=-c
if [[ "$1" == -* ]]; then
interopt="$1"
shift
fi
local src="$1" dest="$2"
[ -d "$dest" ] && dest="$dest/$(basename -- "$src")"
mkdirof "$dest" || return 1
[ -f "$dest" ] || copy_replace "$src" "$dest"
if testdiff "$src" "$dest"; then
check_interaction "$interopt" && diff -u "$dest" "$src"
if ask_yesno "$interopt" "Voulez-vous remplacer $(ppath "$dest") par la nouvelle version?" "${COPY_UPDATE_ASK_DEFAULT:-C}"; then
copy_replace "$src" "$dest" "$3"
return $?
elif ! check_interaction "$interopt"; then
ewarn "Les mises à jours suivantes sont disponibles:"
diff -u "$dest" "$src"
ewarn "Le fichier $(ppath "$dest") n'a pas été mis à jour"
fi
fi
return 1
}
function link_new() {
# Si $2 n'existe pas, créer le lien symbolique $2 pointant vers $1
[ -e "$2" ] && return 0
__nulib_install_show_args "$(basename -- "$2")" "$(dirname -- "$1")"
ln -s "$1" "$2"
}

@ -0,0 +1,14 @@
#!/usr/bin/php
<?php
require __DIR__ . "/../php/vendor/autoload.php";
use nulib\tools\pman\ComposerFile;
use nulib\tools\pman\ComposerPmanFile;
use nulib\ValueException;
$composer = new ComposerFile();
$deps = $composer->getLocalDeps();
foreach ($deps as $dep => $path) {
echo "$path\n";
}

@ -0,0 +1,22 @@
#!/usr/bin/php
<?php
require __DIR__ . "/../php/vendor/autoload.php";
use nulib\tools\pman\ComposerFile;
use nulib\tools\pman\ComposerPmanFile;
use nulib\ValueException;
$composer = new ComposerFile();
$config = new ComposerPmanFile();
if ($argc <= 1) {
throw new ValueException("Il faut spécifier le profil à sélectionner");
}
$profile = $argv[1];
$composer->selectProfile($profile, $config);
if (getenv("PMAN_COMPOSER_DEBUG")) {
$composer->print();
} else {
$composer->write();
}

@ -1,4 +1,12 @@
#!/bin/bash #!/bin/bash
# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8 # -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8
source "$(dirname -- "$0")/../load.sh" || exit 1
exec "$(dirname -- "$0")/pmer" --tech-merge -Bdev82 dev74 -a "git checkout dev74" "$@" dev74=1
args=(
"merger la branche dev74 dans la branche dev82"
-n,--no-dev74 dev74= "ne pas basculer sur la branche dev74 après la fusion"
)
parse_args "$@"; set -- "${args[@]}"
exec "$MYDIR/pmer" --tech-merge -Bdev82 dev74 ${dev74:+-a "git checkout dev74"} "$@"

@ -1,14 +0,0 @@
#!/usr/bin/php
<?php
require __DIR__ . "/../php/vendor/autoload.php";
use nulib\tools\pman\ComposerFile;
use nulib\tools\pman\ComposerPmanFile;
use nulib\ValueException;
$composer = new ComposerFile();
$deps = $composer->getLocalDeps();
foreach ($deps as $dep => $path) {
echo "$path\n";
}

@ -0,0 +1 @@
runphp

@ -1,22 +0,0 @@
#!/usr/bin/php
<?php
require __DIR__ . "/../php/vendor/autoload.php";
use nulib\tools\pman\ComposerFile;
use nulib\tools\pman\ComposerPmanFile;
use nulib\ValueException;
$composer = new ComposerFile();
$config = new ComposerPmanFile();
if ($argc <= 1) {
throw new ValueException("Il faut spécifier le profil à sélectionner");
}
$profile = $argv[1];
$composer->selectProfile($profile, $config);
if (getenv("PMAN_COMPOSER_DEBUG")) {
$composer->print();
} else {
$composer->write();
}

@ -0,0 +1 @@
runphp

24
bin/jsondiff Executable file

@ -0,0 +1,24 @@
#!/bin/bash
# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8
source "$(dirname -- "$0")/../load.sh" || exit 1
args=(
"comparer deux fichiers JSON"
"first.json second.json"
)
parse_args "$@"; set -- "${args[@]}"
first="$1"; shift
[ -n "$first" ] || die "vous devez spécifier le premier fichier"
second="$1"; shift
[ -n "$second" ] || die "vous devez spécifier le deuxième fichier"
if [ $# -gt 0 ]; then
options=("$@")
else
options=(-u)
fi
diff "${options[@]}" \
<(jq . <"$first") \
<(jq . <"$second")

@ -20,14 +20,25 @@ while true; do
cd .. cd ..
done done
if [ -z "$PROJDIR" ]; then export RUNPHP_MOUNT=
# s'il n'y a pas de projet, --bs est l'action par défaut if [ "$MYNAME" == composer ]; then
[ $# -gt 0 ] || set -- --bs --ue
elif [ "$MYNAME" == composer ]; then
set -- composer "$@" set -- composer "$@"
elif [[ "$MYNAME" == *.php ]]; then
# frontend pour une commande php
set -- php "$MYDIR/.$MYNAME" "$@"
# s'assurer que NULIBDIR est monté
RUNPHP_MOUNT="$NULIBDIR"
elif [ $# -eq 0 ]; then
# s'il n'y a pas de projet, --bs est l'action par défaut
[ -n "$PROJDIR" ] || set -- --bs --ue
else else
case "$1" in case "$1" in
*.php|*.phar) set -- php "$@";; *.php|*.phar)
set -- php "$@"
# s'assurer que le répertoire du script est monté
setx RUNPHP_MOUNT=dirname "$1"
setx RUNPHP_MOUNT=abspath "$RUNPHP_MOUNT"
;;
esac esac
fi fi

@ -24,6 +24,8 @@
}, },
"require-dev": { "require-dev": {
"nulib/tests": "^7.4", "nulib/tests": "^7.4",
"ext-mbstring": "*",
"ext-iconv": "*",
"ext-posix": "*", "ext-posix": "*",
"ext-pcntl": "*", "ext-pcntl": "*",
"ext-curl": "*", "ext-curl": "*",

28
composer.lock generated

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "2d630ab5ff0ffe6139447cf93c362ed7", "content-hash": "71744d15224f445d1aeefe16ec7d1099",
"packages": [ "packages": [
{ {
"name": "symfony/deprecation-contracts", "name": "symfony/deprecation-contracts",
@ -301,16 +301,16 @@
}, },
{ {
"name": "myclabs/deep-copy", "name": "myclabs/deep-copy",
"version": "1.13.1", "version": "1.13.3",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/myclabs/DeepCopy.git", "url": "https://github.com/myclabs/DeepCopy.git",
"reference": "1720ddd719e16cf0db4eb1c6eca108031636d46c" "reference": "faed855a7b5f4d4637717c2b3863e277116beb36"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/1720ddd719e16cf0db4eb1c6eca108031636d46c", "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/faed855a7b5f4d4637717c2b3863e277116beb36",
"reference": "1720ddd719e16cf0db4eb1c6eca108031636d46c", "reference": "faed855a7b5f4d4637717c2b3863e277116beb36",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -349,7 +349,7 @@
], ],
"support": { "support": {
"issues": "https://github.com/myclabs/DeepCopy/issues", "issues": "https://github.com/myclabs/DeepCopy/issues",
"source": "https://github.com/myclabs/DeepCopy/tree/1.13.1" "source": "https://github.com/myclabs/DeepCopy/tree/1.13.3"
}, },
"funding": [ "funding": [
{ {
@ -357,20 +357,20 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2025-04-29T12:36:36+00:00" "time": "2025-07-05T12:25:42+00:00"
}, },
{ {
"name": "nikic/php-parser", "name": "nikic/php-parser",
"version": "v5.4.0", "version": "v5.5.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/nikic/PHP-Parser.git", "url": "https://github.com/nikic/PHP-Parser.git",
"reference": "447a020a1f875a434d62f2a401f53b82a396e494" "reference": "ae59794362fe85e051a58ad36b289443f57be7a9"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/447a020a1f875a434d62f2a401f53b82a396e494", "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/ae59794362fe85e051a58ad36b289443f57be7a9",
"reference": "447a020a1f875a434d62f2a401f53b82a396e494", "reference": "ae59794362fe85e051a58ad36b289443f57be7a9",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -413,9 +413,9 @@
], ],
"support": { "support": {
"issues": "https://github.com/nikic/PHP-Parser/issues", "issues": "https://github.com/nikic/PHP-Parser/issues",
"source": "https://github.com/nikic/PHP-Parser/tree/v5.4.0" "source": "https://github.com/nikic/PHP-Parser/tree/v5.5.0"
}, },
"time": "2024-12-30T11:07:19+00:00" "time": "2025-05-31T08:24:38+00:00"
}, },
{ {
"name": "nulib/tests", "name": "nulib/tests",
@ -2027,6 +2027,8 @@
"php": "^7.4" "php": "^7.4"
}, },
"platform-dev": { "platform-dev": {
"ext-mbstring": "*",
"ext-iconv": "*",
"ext-posix": "*", "ext-posix": "*",
"ext-pcntl": "*", "ext-pcntl": "*",
"ext-curl": "*", "ext-curl": "*",

@ -218,10 +218,10 @@ class A {
static final function filter_f($dest): void { self::filter_if($dest, [cv::class, "f"]);} static final function filter_f($dest): void { self::filter_if($dest, [cv::class, "f"]);}
static final function filter_pt($dest): void { self::filter_if($dest, [cv::class, "pt"]);} static final function filter_pt($dest): void { self::filter_if($dest, [cv::class, "pt"]);}
static final function filter_pf($dest): void { self::filter_if($dest, [cv::class, "pf"]);} static final function filter_pf($dest): void { self::filter_if($dest, [cv::class, "pf"]);}
static final function filter_equals($dest, $value): void { self::filter_if($dest, cv::equals($value)); } static final function filter_equals($dest, $value): void { self::filter_if($dest, cv::Fequals($value)); }
static final function filter_not_equals($dest, $value): void { self::filter_if($dest, cv::not_equals($value)); } static final function filter_not_equals($dest, $value): void { self::filter_if($dest, cv::Fnot_equals($value)); }
static final function filter_same($dest, $value): void { self::filter_if($dest, cv::same($value)); } static final function filter_same($dest, $value): void { self::filter_if($dest, cv::Fsame($value)); }
static final function filter_not_same($dest, $value): void { self::filter_if($dest, cv::not_same($value)); } static final function filter_not_same($dest, $value): void { self::filter_if($dest, cv::Fnot_same($value)); }
############################################################################# #############################################################################

5
php/src/TODO.md Normal file

@ -0,0 +1,5 @@
# nulib
* [ ] support de UserException pour ExceptionShadow: distinguer userMessage et techMessage
-*- coding: utf-8 mode: markdown -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8:noeol:binary

9
php/src/app/TODO.md Normal file

@ -0,0 +1,9 @@
# nulib\app
* [ ] ajouter des méthodes normalisées `app::get_cachedir()` et
`app::get_cachefile($name)` avec la valeur par défaut
`cachedir = $vardir/cache`
* [ ] `app::action()` et `app::step()` appellent automatiquement
`app::_dispatch_signals()`
-*- coding: utf-8 mode: markdown -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8:noeol:binary

@ -2,6 +2,9 @@
namespace nulib\app; namespace nulib\app;
use nulib\A; use nulib\A;
use nulib\cl;
use nulib\cv;
use nulib\file;
use nulib\str; use nulib\str;
class args { class args {
@ -10,7 +13,10 @@ class args {
* - ["myArg" => $value] devient ["--my-arg", "$value"] * - ["myArg" => $value] devient ["--my-arg", "$value"]
* - ["myOpt" => true] devient ["--my-opt"] * - ["myOpt" => true] devient ["--my-opt"]
* - ["myOpt" => false] est omis * - ["myOpt" => false] est omis
* - les autres valeurs sont prises telles quelles * - les autres valeurs sont transformées en chaines puis ajoutée
*
* ainsi, ["myOpt" => "value", "myArg", "myBool" => true]
* devient ["--my-opt", "value", "myArg", "--my-bool"]
*/ */
static function from_array(?array $array): array { static function from_array(?array $array): array {
$args = []; $args = [];
@ -36,4 +42,205 @@ class args {
} }
return $args; return $args;
} }
private static function tint(string $value): int {
return intval($value);
}
private static function tbool(string $value): bool {
return boolval($value);
}
private static function tarray(string $value): ?array {
if ($value === "") return null;
$tmparray = explode(",", $value);
$array = null;
foreach ($tmparray as $tmpvalue) {
[$tmpkey, $tmpvalue] = str::split_pair($tmpvalue);
if ($tmpvalue === null) cv::swap($tmpkey, $tmpvalue);
if ($tmpkey === null) {
$array[] = $tmpvalue;
} else {
if (str::del_suffix($tmpkey, ":int")) {
$tmpvalue = self::tint($tmpvalue);
} elseif (str::del_suffix($tmpkey, ":bool")) {
$tmpvalue = self::tbool($tmpvalue);
}
$array[$tmpkey] = $tmpvalue;
}
}
return $array;
}
/**
* convertir une liste d'arguments en tableau qui est utilisable comme un
* filtre de base de données ou des données d'une requête REST. les arguments
* peuvent être de la forme:
* - "name=value"
* qui devient dans le tableau ["name" => "value"]
* - "+arg" ou "arg"
* qui devient dans le tableau ["arg" => true]
* - "-arg" ou "~arg"
* qui est stocké dans le tableau $query ["arg" => false]
*
* si $allow_file == true, les formes d'arguments suivantes sont reconnues
* aussi:
* - "name=@file" (1 argument) OU
* "name=@" "file" (2 arguments)
* qui deviennent ["name" => new FileReader(file)]
*/
static function build_query(?array $args, bool $allow_file=true): ?array {
$query = null;
$args ??= [];
$keys = array_keys($args);
$index = 0;
$count = count($keys);
while ($index < $count) {
$arg = $args[$keys[$index++]];
[$name, $value] = str::split_pair($arg, "=");
$checkType = true;
if ($value === null) {
if (str::del_prefix($name, "+")) {
$value = true;
} elseif (str::del_prefix($name, "-") || str::del_prefix($name, "~")) {
$value = false;
} else {
$value = true;
}
} elseif ($allow_file) {
if ($value === "@") {
$value = $args[$keys[$index++]];
$value = file::reader($value);
$checkType = false;
} elseif (substr($value, 0, 1) === "@") {
$value = substr($value, 1);
$value = file::reader($value);
$checkType = false;
}
}
if ($checkType) {
if (str::del_suffix($name, ":int")) {
if (str::del_suffix($name, ":array")) {
$value = array_map([self::class, "tint"], self::tarray($value));
} else {
$value = self::tint($value);
}
} elseif (str::del_suffix($name, ":bool")) {
if (str::del_suffix($name, ":array")) {
$value = array_map([self::class, "tbool"], self::tarray($value));
} else {
$value = self::tbool($value);
}
} elseif (str::del_suffix($name, ":array")) {
$value = self::tarray($value);
if (str::del_suffix($name, ":int")) {
$value = array_map([self::class, "tint"], $value);
} elseif (str::del_suffix($name, ":bool")) {
$value = array_map([self::class, "tbool"], $value);
}
}
}
if (cl::has($query, $name)) {
A::ensure_array($query[$name]);
$query[$name][] = $value;
} else {
$query[$name] = $value;
}
}
return $query;
}
/**
* convertir une liste d'arguments de façon qu'ils soient utilisables pour un
* appel de méthode. les arguments peuvent être de la forme:
* - "name=value"
* qui est stocké dans le tableau $query ["name" => "value"]
* il est possible de forcer le type de la valeur avec l'un des suffixes
* :int, :bool ou :array, e.g
* un entier: "name:int=42"
* un tableau de chaines: "name:array=a,b,c"
* un tableau d'entiers: "name:array:int=1,2,3"
* - "+arg"
* qui est stocké dans le tableau $query ["arg" => true]
* - "-arg" ou "~arg"
* qui est stocké dans le tableau $query ["arg" => false]
* - "array:sval,key:aval,..."
* qui devient l'argument ["sval", "key" => "aval", ...]
* il est possible de forcer le types des éléments avec le préfixe int: ou
* bool: e.g "array:int:1,2,3"
* - "int:value"
* qui devient l'argument intval("value")
* - "bool:value"
* qui devient l'argument boolval("value")
* - "value"
* qui devient l'argument "value"
*
* à la fin, la liste des arguments est retournée [$arguments...]
* si le tableau $query est renseigné, il est en premier dans la liste des
* arguments e.g [$query, $arguments...]
*/
static function build_method_args(?array $args): ?array {
$query = null;
$margs = [];
$args ??= [];
foreach ($args as $arg) {
[$name, $value] = str::split_pair($arg, "=");
if ($value === null) {
if (str::del_prefix($name, "+")) {
$value = true;
} elseif (str::del_prefix($name, "-") || str::del_prefix($name, "~")) {
$value = false;
} elseif (str::del_prefix($name, "int:")) {
$margs[] = self::tint($name);
continue;
} elseif (str::del_prefix($name, "bool:")) {
$margs[] = self::tbool($name);
continue;
} elseif (str::del_prefix($name, "array:")) {
if (str::del_prefix($name, "int:")) {
$map = [self::class, "tint"];
} elseif (str::del_prefix($name, "bool:")) {
$map = [self::class, "tbool"];
} else {
$map = null;
}
$value = self::tarray($name);
if ($map !== null) $value = array_map($map, $value);
$margs[] = $value;
continue;
} else {
$margs[] = $name;
continue;
}
}
if (str::del_suffix($name, ":int")) {
if (str::del_suffix($name, ":array")) {
$value = array_map([self::class, "tint"], self::tarray($value));
} else {
$value = self::tint($value);
}
} elseif (str::del_suffix($name, ":bool")) {
if (str::del_suffix($name, ":array")) {
$value = array_map([self::class, "tbool"], self::tarray($value));
} else {
$value = self::tbool($value);
}
} elseif (str::del_suffix($name, ":array")) {
$value = self::tarray($value);
if (str::del_suffix($name, ":int")) {
$value = array_map([self::class, "tint"], $value);
} elseif (str::del_suffix($name, ":bool")) {
$value = array_map([self::class, "tbool"], $value);
}
}
if (cl::has($query, $name)) {
A::ensure_array($query[$name]);
$query[$name][] = $value;
} else {
$query[$name] = $value;
}
}
if ($query !== null) array_unshift($margs, $query);
return $margs;
}
} }

@ -817,10 +817,10 @@ class cl {
static final function all_f(?array $array): bool { return self::all_if($array, [cv::class, "f"]);} static final function all_f(?array $array): bool { return self::all_if($array, [cv::class, "f"]);}
static final function all_pt(?array $array): bool { return self::all_if($array, [cv::class, "pt"]);} static final function all_pt(?array $array): bool { return self::all_if($array, [cv::class, "pt"]);}
static final function all_pf(?array $array): bool { return self::all_if($array, [cv::class, "pf"]);} static final function all_pf(?array $array): bool { return self::all_if($array, [cv::class, "pf"]);}
static final function all_equals(?array $array, $value): bool { return self::all_if($array, cv::equals($value)); } static final function all_equals(?array $array, $value): bool { return self::all_if($array, cv::Fequals($value)); }
static final function all_not_equals(?array $array, $value): bool { return self::all_if($array, cv::not_equals($value)); } static final function all_not_equals(?array $array, $value): bool { return self::all_if($array, cv::Fnot_equals($value)); }
static final function all_same(?array $array, $value): bool { return self::all_if($array, cv::same($value)); } static final function all_same(?array $array, $value): bool { return self::all_if($array, cv::Fsame($value)); }
static final function all_not_same(?array $array, $value): bool { return self::all_if($array, cv::not_same($value)); } static final function all_not_same(?array $array, $value): bool { return self::all_if($array, cv::Fnot_same($value)); }
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@ -842,10 +842,10 @@ class cl {
static final function any_f(?array $array): bool { return self::any_if($array, [cv::class, "f"]);} static final function any_f(?array $array): bool { return self::any_if($array, [cv::class, "f"]);}
static final function any_pt(?array $array): bool { return self::any_if($array, [cv::class, "pt"]);} static final function any_pt(?array $array): bool { return self::any_if($array, [cv::class, "pt"]);}
static final function any_pf(?array $array): bool { return self::any_if($array, [cv::class, "pf"]);} static final function any_pf(?array $array): bool { return self::any_if($array, [cv::class, "pf"]);}
static final function any_equals(?array $array, $value): bool { return self::any_if($array, cv::equals($value)); } static final function any_equals(?array $array, $value): bool { return self::any_if($array, cv::Fequals($value)); }
static final function any_not_equals(?array $array, $value): bool { return self::any_if($array, cv::not_equals($value)); } static final function any_not_equals(?array $array, $value): bool { return self::any_if($array, cv::Fnot_equals($value)); }
static final function any_same(?array $array, $value): bool { return self::any_if($array, cv::same($value)); } static final function any_same(?array $array, $value): bool { return self::any_if($array, cv::Fsame($value)); }
static final function any_not_same(?array $array, $value): bool { return self::any_if($array, cv::not_same($value)); } static final function any_not_same(?array $array, $value): bool { return self::any_if($array, cv::Fnot_same($value)); }
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@ -876,10 +876,10 @@ class cl {
static final function filter_f(?array $array): ?array { return self::filter_if($array, [cv::class, "f"]);} static final function filter_f(?array $array): ?array { return self::filter_if($array, [cv::class, "f"]);}
static final function filter_pt(?array $array): ?array { return self::filter_if($array, [cv::class, "pt"]);} static final function filter_pt(?array $array): ?array { return self::filter_if($array, [cv::class, "pt"]);}
static final function filter_pf(?array $array): ?array { return self::filter_if($array, [cv::class, "pf"]);} static final function filter_pf(?array $array): ?array { return self::filter_if($array, [cv::class, "pf"]);}
static final function filter_equals(?array $array, $value): ?array { return self::filter_if($array, cv::equals($value)); } static final function filter_equals(?array $array, $value): ?array { return self::filter_if($array, cv::Fequals($value)); }
static final function filter_not_equals(?array $array, $value): ?array { return self::filter_if($array, cv::not_equals($value)); } static final function filter_not_equals(?array $array, $value): ?array { return self::filter_if($array, cv::Fnot_equals($value)); }
static final function filter_same(?array $array, $value): ?array { return self::filter_if($array, cv::same($value)); } static final function filter_same(?array $array, $value): ?array { return self::filter_if($array, cv::Fsame($value)); }
static final function filter_not_same(?array $array, $value): ?array { return self::filter_if($array, cv::not_same($value)); } static final function filter_not_same(?array $array, $value): ?array { return self::filter_if($array, cv::Fnot_same($value)); }
############################################################################# #############################################################################

@ -49,7 +49,7 @@ class cv {
* contruire une fonction qui retourne vrai si on lui passe en argument une * contruire une fonction qui retourne vrai si on lui passe en argument une
* valeur égale à $value * valeur égale à $value
*/ */
static final function equals($value): callable { static final function Fequals($value): callable {
return function ($arg) use($value) { return $arg == $value; }; return function ($arg) use($value) { return $arg == $value; };
} }
@ -57,7 +57,7 @@ class cv {
* contruire une fonction qui retourne vrai si on lui passe en argument une * contruire une fonction qui retourne vrai si on lui passe en argument une
* valeur qui n'est pas égale à $value * valeur qui n'est pas égale à $value
*/ */
static final function not_equals($value): callable { static final function Fnot_equals($value): callable {
return function ($arg) use($value) { return $arg != $value; }; return function ($arg) use($value) { return $arg != $value; };
} }
@ -65,7 +65,7 @@ class cv {
* contruire une fonction qui retourne vrai si on lui passe en argument une * contruire une fonction qui retourne vrai si on lui passe en argument une
* valeur strictement égale à $value * valeur strictement égale à $value
*/ */
static final function same($value): callable { static final function Fsame($value): callable {
return function ($arg) use($value) { return $arg === $value; }; return function ($arg) use($value) { return $arg === $value; };
} }
@ -73,7 +73,7 @@ class cv {
* contruire une fonction qui retourne vrai si on lui passe en argument une * contruire une fonction qui retourne vrai si on lui passe en argument une
* valeur qui n'est pas strictement égale à $value * valeur qui n'est pas strictement égale à $value
*/ */
static final function not_same($value): callable { static final function Fnot_same($value): callable {
return function ($arg) use($value) { return $arg !== $value; }; return function ($arg) use($value) { return $arg !== $value; };
} }
@ -231,6 +231,24 @@ class cv {
############################################################################# #############################################################################
/**
* tester l'égalité non stricte de deux valeurs, dans le cas leurs types ne
* sont pas forcément cohérents: par exemple eq("1", 1) === true
*
* cette fonction est utile par exemple quand on veut comparer des données
* provenant d'une base de données: PHP a la facheuse tendance de retourner
* les nombres sous forme de chaines
*/
static final function equals($a, $b): bool {
# cette fonction existe parce que le type des valeurs retournées par la BDD
# n'est pas cohérent, "1" au lieu de 1 par exemple
if ($a === null) return $b === null;
elseif ($b === null) return $a === null;
elseif (is_scalar($a)) return strval($a) === strval($b);
# ne pas utiliser l'égalité stricte ici: ce sont des objets
else return $a == $b;
}
/** retourner -1, 0 ou 1 en fonction de l'ordre relatif entre $a et $b */ /** retourner -1, 0 ou 1 en fonction de l'ordre relatif entre $a et $b */
static final function compare($a, $b): int { static final function compare($a, $b): int {
if ($a === $b) return 0; if ($a === $b) return 0;

@ -1,6 +1,7 @@
<?php <?php
namespace nulib\db; namespace nulib\db;
use nulib\A;
use nulib\cl; use nulib\cl;
use nulib\php\func; use nulib\php\func;
use nulib\ValueException; use nulib\ValueException;
@ -29,6 +30,11 @@ class Capacitor implements ITransactor {
return $this->getStorage()->db(); return $this->getStorage()->db();
} }
function ensureLive(): self {
$this->getStorage()->ensureLive();
return $this;
}
/** @var CapacitorChannel */ /** @var CapacitorChannel */
protected $channel; protected $channel;
@ -138,9 +144,22 @@ class Capacitor implements ITransactor {
$this->storage->_reset($this->channel, $recreate); $this->storage->_reset($this->channel, $recreate);
} }
function charge($item, $func=null, ?array $args=null, ?array &$values=null): int { function charge($item, $func=null, ?array $args=null, ?array &$row=null): int {
if ($this->subChannels !== null) $this->beginTransaction(); if ($this->subChannels !== null) $this->beginTransaction();
return $this->storage->_charge($this->channel, $item, $func, $args, $values); return $this->storage->_charge($this->channel, $item, $func, $args, $row);
}
function chargeAll(?iterable $items, $func=null, ?array $args=null): int {
$count = 0;
if ($items !== null) {
if ($func !== null) {
$func = func::with($func, $args)->bind($this->channel);
}
foreach ($items as $item) {
$count += $this->charge($item, $func);
}
}
return $count;
} }
function discharge(bool $reset=true): Traversable { function discharge(bool $reset=true): Traversable {
@ -169,11 +188,27 @@ class Capacitor implements ITransactor {
return $this->storage->_delete($this->channel, $filter, $func, $args); return $this->storage->_delete($this->channel, $filter, $func, $args);
} }
function dbUpdate(array $update) { 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([ return $this->storage->db()->exec(cl::merge([
"update", "update",
"table" => $this->getTableName(), "table" => $this->getTableName(),
], $update)); ], $query), $params);
} }
function close(): void { function close(): void {

@ -25,8 +25,6 @@ class CapacitorChannel implements ITransactor {
const EACH_COMMIT_THRESHOLD = 100; const EACH_COMMIT_THRESHOLD = 100;
const USE_CACHE = false;
static function verifix_name(?string &$name, ?string &$tableName=null): void { static function verifix_name(?string &$name, ?string &$tableName=null): void {
if ($name !== null) { if ($name !== null) {
$name = strtolower($name); $name = strtolower($name);
@ -60,7 +58,6 @@ class CapacitorChannel implements ITransactor {
$this->tableName = $tableName; $this->tableName = $tableName;
$this->manageTransactions = $manageTransactions ?? static::MANAGE_TRANSACTIONS; $this->manageTransactions = $manageTransactions ?? static::MANAGE_TRANSACTIONS;
$this->eachCommitThreshold = self::verifix_eachCommitThreshold($eachCommitThreshold); $this->eachCommitThreshold = self::verifix_eachCommitThreshold($eachCommitThreshold);
$this->useCache = static::USE_CACHE;
$this->setup = false; $this->setup = false;
$this->created = false; $this->created = false;
$columnDefinitions = $this->COLUMN_DEFINITIONS(); $columnDefinitions = $this->COLUMN_DEFINITIONS();
@ -111,6 +108,8 @@ class CapacitorChannel implements ITransactor {
$def = strval($def); $def = strval($def);
if (preg_match('/\bprimary\s+key\b/i', $def)) { if (preg_match('/\bprimary\s+key\b/i', $def)) {
$primaryKeys[] = $col; $primaryKeys[] = $col;
} elseif ($def === "genserial") {
$primaryKeys[] = $col;
} }
} }
} }
@ -166,23 +165,6 @@ class CapacitorChannel implements ITransactor {
return $this; return $this;
} }
/**
* @var bool faut-il passer par le cache pour les requêtes de all(), each()
* et delete()?
* ça peut être nécessaire avec MySQL/MariaDB si on utilise les requêtes non
* bufférisées, et que la fonction manipule la base de données
*/
protected bool $useCache;
function isUseCache(): bool {
return $this->useCache;
}
function setUseCache(bool $useCache=true): self {
$this->useCache = $useCache;
return $this;
}
/** /**
* initialiser ce channel avant sa première utilisation. * initialiser ce channel avant sa première utilisation.
*/ */
@ -232,8 +214,18 @@ class CapacitorChannel implements ITransactor {
protected ?array $migration; protected ?array $migration;
function getMigration(): ?array { function getMigration(?string $prefix=null): ?array {
return $this->migration; if ($prefix === null || $this->migration === null) return $this->migration;
$migration = null;
str::add_suffix($prefix, ":");
foreach ($this->migration as $mkey => $mdef) {
if (str::starts_with($prefix, $mkey)) {
$migration[$mkey] = $mdef;
} elseif (strpos($mkey, ":") === false) {
$migration[$mkey] = $mdef;
}
}
return $migration;
} }
protected ?array $primaryKeys; protected ?array $primaryKeys;
@ -255,6 +247,10 @@ class CapacitorChannel implements ITransactor {
* Retourner la clé primaire par cette méthode est l'unique moyen de * Retourner la clé primaire par cette méthode est l'unique moyen de
* déclencher une mise à jour plutôt qu'une nouvelle création. * déclencher une mise à jour plutôt qu'une nouvelle création.
* *
* Bien que ce ne soit pas prévu à la base, si on veut modifier $item dans
* cette méthode pour des raisons pratiques, il suffit de retournerla valeur
* modifiée avec la clé "item"
*
* Retourner [false] pour annuler le chargement * Retourner [false] pour annuler le chargement
*/ */
function getItemValues($item): ?array { function getItemValues($item): ?array {
@ -277,8 +273,8 @@ class CapacitorChannel implements ITransactor {
* *
* cette méthode doit être utilisée dans {@link self::onUpdate()} * cette méthode doit être utilisée dans {@link self::onUpdate()}
*/ */
function wasRowModified(array $values, array $pvalues): bool { function wasRowModified(array $row, array $prow): bool {
return $values["item__sum_"] !== $pvalues["item__sum_"]; return $row["item__sum_"] !== $prow["item__sum_"];
} }
final function serialize($item): ?string { final function serialize($item): ?string {
@ -309,17 +305,17 @@ class CapacitorChannel implements ITransactor {
return array_combine($sumCols, [$serial, $sum]); return array_combine($sumCols, [$serial, $sum]);
} }
function wasSumModified(string $key, $value, array $pvalues): bool { function wasSumModified(string $key, $value, array $prow): bool {
$sumCol = $this->getSumCols($key)[1]; $sumCol = $this->getSumCols($key)[1];
$sum = $this->sum(null, $value); $sum = $this->sum(null, $value);
$psum = $pvalues[$sumCol] ?? $this->sum(null, $pvalues[$key] ?? null); $psum = $prow[$sumCol] ?? $this->sum(null, $prow[$key] ?? null);
return $sum !== $psum; return $sum !== $psum;
} }
function _wasSumModified(string $key, array $row, array $prow): bool { function _wasSumModified(string $key, array $raw, array $praw): bool {
$sumCol = $this->getSumCols($key)[1]; $sumCol = $this->getSumCols($key)[1];
$sum = $row[$sumCol] ?? null; $sum = $raw[$sumCol] ?? null;
$psum = $prow[$sumCol] ?? null; $psum = $praw[$sumCol] ?? null;
return $sum !== $psum; return $sum !== $psum;
} }
@ -328,21 +324,21 @@ class CapacitorChannel implements ITransactor {
* créer un nouvel élément * créer un nouvel élément
* *
* @param mixed $item l'élément à charger * @param mixed $item l'élément à charger
* @param array $values la ligne à créer, calculée à partir de $item et des * @param array $row la ligne à créer, calculée à partir de $item et des
* valeurs retournées par {@link getItemValues()} * valeurs retournées par {@link getItemValues()}
* @return ?array le cas échéant, un tableau non null à merger dans $values et * @return ?array le cas échéant, un tableau non null à merger dans $row et
* utilisé pour provisionner la ligne nouvellement créée. * utilisé pour provisionner la ligne nouvellement créée.
* Retourner [false] pour annuler le chargement (la ligne n'est pas créée) * Retourner [false] pour annuler le chargement (la ligne n'est pas créée)
* *
* Si $item est modifié dans cette méthode, il est possible de le retourner * Si $item est modifié dans cette méthode, il est possible de le retourner
* avec la clé "item" pour mettre à jour la ligne correspondante. * avec la clé "item" pour mettre à jour la colonne correspondante.
* *
* la création ou la mise à jour est uniquement décidée en fonction des * la création ou la mise à jour est uniquement décidée en fonction des
* valeurs calculées par {@link self::getItemValues()}. Bien que cette méthode * valeurs calculées par {@link self::getItemValues()}. Bien que cette méthode
* peut techniquement retourner de nouvelles valeurs pour la clé primaire, ça * peut techniquement retourner de nouvelles valeurs pour la clé primaire, ça
* risque de créer des doublons * risque de créer des doublons
*/ */
function onCreate($item, array $values, ?array $alwaysNull): ?array { function onCreate($item, array $row, ?array $alwaysNull): ?array {
return null; return null;
} }
@ -351,12 +347,12 @@ class CapacitorChannel implements ITransactor {
* mettre à jour un élément existant * mettre à jour un élément existant
* *
* @param mixed $item l'élément à charger * @param mixed $item l'élément à charger
* @param array $values la nouvelle ligne, calculée à partir de $item et * @param array $row la nouvelle ligne, calculée à partir de $item et
* des valeurs retournées par {@link getItemValues()} * des valeurs retournées par {@link getItemValues()}
* @param array $pvalues la précédente ligne, chargée depuis la base de * @param array $prow la précédente ligne, chargée depuis la base de
* données * données
* @return ?array null s'il ne faut pas mettre à jour la ligne. sinon, ce * @return ?array null s'il ne faut pas mettre à jour la ligne. sinon, ce
* tableau est mergé dans $values puis utilisé pour mettre à jour la ligne * tableau est mergé dans $row puis utilisé pour mettre à jour la ligne
* existante * existante
* Retourner [false] pour annuler le chargement (la ligne n'est pas mise à * Retourner [false] pour annuler le chargement (la ligne n'est pas mise à
* jour) * jour)
@ -365,24 +361,25 @@ class CapacitorChannel implements ITransactor {
* - La clé primaire (il s'agit généralement de "id_") ne peut pas être * - La clé primaire (il s'agit généralement de "id_") ne peut pas être
* modifiée. si elle est retournée, elle est ignorée * modifiée. si elle est retournée, elle est ignorée
*/ */
function onUpdate($item, array $values, array $pvalues): ?array { function onUpdate($item, array $row, array $prow): ?array {
return null; return null;
} }
/** /**
* méthode appelée lors du parcours des éléments avec * méthode appelée lors du parcours des éléments avec {@link Capacitor::each()}
* {@link Capacitor::each()}
* *
* @param mixed $item l'élément courant * @param ?array $row la ligne courante. l'élément courant est accessible via
* @param ?array $values la ligne courante * $row["item"]
* @return ?array le cas échéant, un tableau non null utilisé pour mettre à * @return ?array le cas échéant, un tableau non null utilisé pour mettre à
* jour la ligne courante * jour la ligne courante
* Retourner [false] pour arrêter le parcours des éléments (la ligne courante
* ainsi que les autres lignes ne sont plus mise à jour)
* *
* - Il est possible de mettre à jour $item en le retourant avec la clé "item" * - Il est possible de mettre à jour $item en le retourant avec la clé "item"
* - La clé primaire (il s'agit généralement de "id_") ne peut pas être * - La clé primaire (il s'agit généralement de "id_") ne peut pas être
* modifiée. si elle est retournée, elle est ignorée * modifiée. si elle est retournée, elle est ignorée
*/ */
function onEach($item, array $values): ?array { function onEach(array $row): ?array {
return null; return null;
} }
const onEach = "->".[self::class, "onEach"][1]; const onEach = "->".[self::class, "onEach"][1];
@ -391,11 +388,11 @@ class CapacitorChannel implements ITransactor {
* méthode appelée lors du parcours des éléments avec * méthode appelée lors du parcours des éléments avec
* {@link Capacitor::delete()} * {@link Capacitor::delete()}
* *
* @param mixed $item l'élément courant * @param ?array $row la ligne courante. l'élément courant est accessible via
* @param ?array $values la ligne courante * $row["item"]
* @return bool true s'il faut supprimer la ligne, false sinon * @return bool true s'il faut supprimer la ligne, false sinon
*/ */
function onDelete($item, array $values): bool { function onDelete(array $row): bool {
return true; return true;
} }
const onDelete = "->".[self::class, "onDelete"][1]; const onDelete = "->".[self::class, "onDelete"][1];
@ -418,6 +415,16 @@ class CapacitorChannel implements ITransactor {
return $this; return $this;
} }
function initStorage(CapacitorStorage $storage): self {
new Capacitor($storage, $this);
return $this;
}
function ensureLive(): self {
$this->capacitor->ensureLive();
return $this;
}
function willUpdate(...$transactors): ITransactor { function willUpdate(...$transactors): ITransactor {
return $this->capacitor->willUpdate(...$transactors); return $this->capacitor->willUpdate(...$transactors);
} }
@ -454,36 +461,75 @@ class CapacitorChannel implements ITransactor {
$this->capacitor->reset($recreate); $this->capacitor->reset($recreate);
} }
function charge($item, $func=null, ?array $args=null, ?array &$values=null): int { function charge($item, $func=null, ?array $args=null, ?array &$row=null): int {
return $this->capacitor->charge($item, $func, $args, $values); return $this->capacitor->charge($item, $func, $args, $row);
}
function chargeAll(?iterable $items, $func=null, ?array $args=null): int {
return $this->capacitor->chargeAll($items, $func, $args);
} }
function discharge(bool $reset=true): Traversable { function discharge(bool $reset=true): Traversable {
return $this->capacitor->discharge($reset); return $this->capacitor->discharge($reset);
} }
/**
* retourner le filtre de base: les filtres de toutes les fonctions ci-dessous
* sont fusionnées avec le filtre de base
*
* cela permet de limiter toutes les opérations à un sous-ensemble des données
* du canal
*/
function getBaseFilter(): ?array {
return null;
}
protected function verifixFilter(&$filter): void {
if ($filter !== null && !is_array($filter)) {
$primaryKeys = $this->primaryKeys ?? ["id_"];
$id = $filter;
$this->verifixId($id);
$filter = [$primaryKeys[0] => $id];
}
$filter = cl::merge($this->getBaseFilter(), $filter);
}
function count($filter=null): int { function count($filter=null): int {
$this->verifixFilter($filter);
return $this->capacitor->count($filter); return $this->capacitor->count($filter);
} }
function one($filter, ?array $mergeQuery=null): ?array { function one($filter, ?array $mergeQuery=null): ?array {
$this->verifixFilter($filter);
return $this->capacitor->one($filter, $mergeQuery); return $this->capacitor->one($filter, $mergeQuery);
} }
function all($filter, ?array $mergeQuery=null): Traversable { function all($filter, ?array $mergeQuery=null): Traversable {
$this->verifixFilter($filter);
return $this->capacitor->all($filter, $mergeQuery); return $this->capacitor->all($filter, $mergeQuery);
} }
function each($filter, $func=null, ?array $args=null, ?array $mergeQuery=null, ?int &$nbUpdated=null): int { 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($filter, $func, $args, $mergeQuery, $nbUpdated);
} }
function delete($filter, $func=null, ?array $args=null): int { function delete($filter, $func=null, ?array $args=null): int {
$this->verifixFilter($filter);
return $this->capacitor->delete($filter, $func, $args); return $this->capacitor->delete($filter, $func, $args);
} }
function dbUpdate(array $update) { function dbAll(array $query, ?array $params=null): iterable {
return $this->capacitor->dbUpdate($update); return $this->capacitor->dbAll($query, $params);
}
function dbOne(array $query, ?array $params=null): ?array {
return $this->capacitor->dbOne($query, $params);
}
/** @return int|false */
function dbUpdate(array $query, ?array $params=null) {
return $this->capacitor->dbUpdate($query, $params);
} }
function close(): void { function close(): void {

@ -1,7 +1,9 @@
<?php <?php
namespace nulib\db; namespace nulib\db;
use nulib\A;
use nulib\cl; use nulib\cl;
use nulib\cv;
use nulib\db\_private\_migration; use nulib\db\_private\_migration;
use nulib\php\func; use nulib\php\func;
use nulib\ValueException; use nulib\ValueException;
@ -14,7 +16,12 @@ use Traversable;
abstract class CapacitorStorage { abstract class CapacitorStorage {
abstract function db(): IDatabase; abstract function db(): IDatabase;
/** @var CapacitorChannel[] */ function ensureLive(): self {
$this->db()->ensure();
return $this;
}
/** @var CapacitorChannel[] */
protected $channels; protected $channels;
function addChannel(CapacitorChannel $channel): CapacitorChannel { function addChannel(CapacitorChannel $channel): CapacitorChannel {
@ -32,21 +39,43 @@ abstract class CapacitorStorage {
return $channel; return $channel;
} }
/** DOIT être défini dans les classes dérivées */ const PRIMARY_KEY_DEFINITION = [
const PRIMARY_KEY_DEFINITION = null; "id_" => "genserial",
];
# les définitions sont par défaut pour MariaDB/MySQL
const SERDATA_DEFINITION = "mediumtext"; const SERDATA_DEFINITION = "mediumtext";
const SERSUM_DEFINITION = "varchar(40)"; const SERSUM_DEFINITION = "varchar(40)";
const SERTS_DEFINITION = "datetime"; 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 sercol($def): string { protected static function gencol($def): string {
if (!is_string($def)) $def = strval($def); 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) { switch ($def) {
case "serdata": $def = static::SERDATA_DEFINITION; break; case "serdata": $def = static::SERDATA_DEFINITION; break;
case "sersum": $def = static::SERSUM_DEFINITION; break; case "sersum": $def = static::SERSUM_DEFINITION; break;
case "serts": $def = static::SERTS_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; return "$def$rest";
} }
const COLUMN_DEFINITIONS = [ const COLUMN_DEFINITIONS = [
@ -81,7 +110,7 @@ abstract class CapacitorStorage {
$mindex++; $mindex++;
} else { } else {
if ($mdef) { if ($mdef) {
$definitions[$mcol] = self::sercol($mdef); $definitions[$mcol] = self::gencol($mdef);
} else { } else {
unset($definitions[$mcol]); unset($definitions[$mcol]);
} }
@ -92,19 +121,45 @@ abstract class CapacitorStorage {
$constraints[] = $def; $constraints[] = $def;
} }
} else { } else {
$definitions[$col] = self::sercol($def); $definitions[$col] = self::gencol($def);
} }
} }
return cl::merge($definitions, $constraints); return cl::merge($definitions, $constraints);
} }
protected function getMigration(CapacitorChannel $channel): ?array { protected function getMigration(CapacitorChannel $channel): ?array {
return $channel->getMigration(); return $channel->getMigration($this->db()->getPrefix());
} }
/** sérialiser les valeurs qui doivent l'être dans $values */ /** sérialiser les valeurs qui doivent l'être dans $row */
protected function serialize(CapacitorChannel $channel, ?array $values): ?array { protected function serialize(CapacitorChannel $channel, ?array $row): ?array {
if ($values === null) return null; 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); $cols = $this->ColumnDefinitions($channel);
$index = 0; $index = 0;
$row = []; $row = [];
@ -112,44 +167,18 @@ abstract class CapacitorStorage {
$key = $col; $key = $col;
if ($key === $index) { if ($key === $index) {
$index++; $index++;
} elseif (!array_key_exists($col, $raw)) {
} elseif ($channel->isSerialCol($key)) { } elseif ($channel->isSerialCol($key)) {
[$serialCol, $sumCol] = $channel->getSumCols($key); $value = $raw[$col];
if (array_key_exists($key, $values)) { if ($value !== null) $value = $channel->unserialize($value);
$sum = $channel->getSum($key, $values[$key]); $row[$key] = $value;
$row[$serialCol] = $sum[$serialCol]; } else {
if (array_key_exists($sumCol, $cols)) { $row[$key] = $raw[$col];
$row[$sumCol] = $sum[$sumCol];
}
}
} elseif (array_key_exists($key, $values)) {
$row[$col] = $values[$key];
} }
} }
return $row; return $row;
} }
/** désérialiser les valeurs qui doivent l'être dans $values */
protected function unserialize(CapacitorChannel $channel, ?array $row): ?array {
if ($row === null) return null;
$cols = $this->ColumnDefinitions($channel);
$index = 0;
$values = [];
foreach (array_keys($cols) as $col) {
$key = $col;
if ($key === $index) {
$index++;
} elseif (!array_key_exists($col, $row)) {
} elseif ($channel->isSerialCol($key)) {
$value = $row[$col];
if ($value !== null) $value = $channel->unserialize($value);
$values[$key] = $value;
} else {
$values[$key] = $row[$col];
}
}
return $values;
}
function getPrimaryKeys(CapacitorChannel $channel): array { function getPrimaryKeys(CapacitorChannel $channel): array {
$primaryKeys = $channel->getPrimaryKeys(); $primaryKeys = $channel->getPrimaryKeys();
if ($primaryKeys === null) $primaryKeys = ["id_"]; if ($primaryKeys === null) $primaryKeys = ["id_"];
@ -215,6 +244,22 @@ abstract class CapacitorStorage {
"class_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 { protected function _createChannelsSql(): array {
return [ return [
"create table if not exists", "create table if not exists",
@ -316,7 +361,7 @@ abstract class CapacitorStorage {
* en fonction du type d'opération: création ou mise à jour * 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 * Dans les deux cas, si la fonction retourne un tableau, il est utilisé pour
* modifier les valeurs insérées/mises à jour. De plus, $values obtient la * modifier les valeurs insérées/mises à jour. De plus, $row obtient la
* valeur finale des données insérées/mises à jour * valeur finale des données insérées/mises à jour
* *
* Si $args est renseigné, il est ajouté aux arguments utilisés pour appeler * Si $args est renseigné, il est ajouté aux arguments utilisés pour appeler
@ -327,23 +372,27 @@ abstract class CapacitorStorage {
* @return int 1 si l'objet a été chargé ou mis à jour, 0 s'il existait * @return int 1 si l'objet a été chargé ou mis à jour, 0 s'il existait
* déjà à l'identique dans le canal * déjà à l'identique dans le canal
*/ */
function _charge(CapacitorChannel $channel, $item, $func, ?array $args, ?array &$values=null): int { function _charge(CapacitorChannel $channel, $item, $func, ?array $args, ?array &$row=null): int {
$this->_create($channel); $this->_create($channel);
$tableName = $channel->getTableName(); $tableName = $channel->getTableName();
$db = $this->db(); $db = $this->db();
$args ??= []; $args ??= [];
$values = func::call([$channel, "getItemValues"], $item, ...$args); $row = func::call([$channel, "getItemValues"], $item, ...$args);
if ($values === [false]) return 0; if ($row === [false]) return 0;
$row = cl::merge( if ($row !== null && array_key_exists("item", $row)) {
$item = A::pop($row, "item");
}
$raw = cl::merge(
$channel->getSum("item", $item), $channel->getSum("item", $item),
$this->serialize($channel, $values)); $this->serialize($channel, $row));
$prow = null; $praw = null;
$rowIds = $this->getRowIds($channel, $row, $primaryKeys); $rowIds = $this->getRowIds($channel, $raw, $primaryKeys);
if ($rowIds !== null) { if ($rowIds !== null) {
# modification # modification
$prow = $db->one([ $praw = $db->one([
"select", "select",
"from" => $tableName, "from" => $tableName,
"where" => $rowIds, "where" => $rowIds,
@ -352,47 +401,47 @@ abstract class CapacitorStorage {
$now = date("Y-m-d H:i:s"); $now = date("Y-m-d H:i:s");
$insert = null; $insert = null;
if ($prow === null) { if ($praw === null) {
# création # création
$row = cl::merge($row, [ $raw = cl::merge($raw, [
"created_" => $now, "created_" => $now,
"modified_" => $now, "modified_" => $now,
]); ]);
$insert = true; $insert = true;
$initFunc = func::with([$channel, "onCreate"], $args); $initFunc = func::with([$channel, "onCreate"], $args);
$values = $this->unserialize($channel, $row); $row = $this->unserialize($channel, $raw);
$pvalues = null; $prow = null;
} else { } else {
# modification # modification
# intégrer autant que possible les valeurs de prow dans row, de façon que # 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é # l'utilisateur puisse voir clairement ce qui a été modifié
if ($channel->_wasSumModified("item", $row, $prow)) { if ($channel->_wasSumModified("item", $raw, $praw)) {
$insert = false; $insert = false;
$row = cl::merge($prow, $row, [ $raw = cl::merge($praw, $raw, [
"modified_" => $now, "modified_" => $now,
]); ]);
} else { } else {
$row = cl::merge($prow, $row); $raw = cl::merge($praw, $raw);
} }
$initFunc = func::with([$channel, "onUpdate"], $args); $initFunc = func::with([$channel, "onUpdate"], $args);
$values = $this->unserialize($channel, $row); $row = $this->unserialize($channel, $raw);
$pvalues = $this->unserialize($channel, $prow); $prow = $this->unserialize($channel, $praw);
} }
$updates = $initFunc->prependArgs([$item, $values, $pvalues])->invoke(); $updates = $initFunc->prependArgs([$item, $row, $prow])->invoke();
if ($updates === [false]) return 0; if ($updates === [false]) return 0;
if (is_array($updates) && $updates) { if (is_array($updates) && $updates) {
if ($insert === null) $insert = false; if ($insert === null) $insert = false;
if (!array_key_exists("modified_", $updates)) { if (!array_key_exists("modified_", $updates)) {
$updates["modified_"] = $now; $updates["modified_"] = $now;
} }
$values = cl::merge($values, $updates); $row = cl::merge($row, $updates);
$row = cl::merge($row, $this->serialize($channel, $updates)); $raw = cl::merge($raw, $this->serialize($channel, $updates));
} }
if ($func !== null) { if ($func !== null) {
$updates = func::with($func) $updates = func::with($func, $args)
->prependArgs([$item, $values, $pvalues]) ->prependArgs([$item, $row, $prow])
->bind($channel) ->bind($channel)
->invoke(); ->invoke();
if ($updates === [false]) return 0; if ($updates === [false]) return 0;
@ -401,8 +450,8 @@ abstract class CapacitorStorage {
if (!array_key_exists("modified_", $updates)) { if (!array_key_exists("modified_", $updates)) {
$updates["modified_"] = $now; $updates["modified_"] = $now;
} }
$values = cl::merge($values, $updates); $row = cl::merge($row, $updates);
$row = cl::merge($row, $this->serialize($channel, $updates)); $raw = cl::merge($raw, $this->serialize($channel, $updates));
} }
} }
@ -421,23 +470,22 @@ abstract class CapacitorStorage {
$id = $db->exec([ $id = $db->exec([
"insert", "insert",
"into" => $tableName, "into" => $tableName,
"values" => $row, "values" => $raw,
]); ]);
if (count($primaryKeys) == 1 && $rowIds === null) { if (count($primaryKeys) == 1 && $rowIds === null) {
# mettre à jour avec l'id généré # mettre à jour avec l'id généré
$values[$primaryKeys[0]] = $id; $row[$primaryKeys[0]] = $id;
} }
$nbModified = 1; $nbModified = 1;
} else { } else {
# calculer ce qui a changé pour ne mettre à jour que le nécessaire # calculer ce qui a changé pour ne mettre à jour que le nécessaire
$updates = []; $updates = [];
foreach ($row as $col => $value) { foreach ($raw as $col => $value) {
if (array_key_exists($col, $rowIds)) { if (array_key_exists($col, $rowIds)) {
# ne jamais mettre à jour la clé primaire # ne jamais mettre à jour la clé primaire
continue; continue;
} }
$pvalue = $prow[$col] ?? null; if (!cv::equals($value, $praw[$col] ?? null)) {
if ($value !== ($pvalue)) {
$updates[$col] = $value; $updates[$col] = $value;
} }
} }
@ -467,19 +515,22 @@ abstract class CapacitorStorage {
} }
} }
function charge(?string $channel, $item, $func=null, ?array $args=null, ?array &$values=null): int { function charge(?string $channel, $item, $func=null, ?array $args=null, ?array &$row=null): int {
return $this->_charge($this->getChannel($channel), $item, $func, $args, $values); return $this->_charge($this->getChannel($channel), $item, $func, $args, $row);
} }
/** décharger les données du canal spécifié */ /**
* décharger les données du canal spécifié. seul la valeur de $item est
* fournie
*/
function _discharge(CapacitorChannel $channel, bool $reset=true): Traversable { function _discharge(CapacitorChannel $channel, bool $reset=true): Traversable {
$this->_create($channel); $this->_create($channel);
$rows = $this->db()->all([ $raws = $this->db()->all([
"select item__", "select item__",
"from" => $channel->getTableName(), "from" => $channel->getTableName(),
]); ]);
foreach ($rows as $row) { foreach ($raws as $raw) {
yield unserialize($row['item__']); yield unserialize($raw['item__']);
} }
if ($reset) $this->_reset($channel); if ($reset) $this->_reset($channel);
} }
@ -548,45 +599,34 @@ abstract class CapacitorStorage {
if ($filter === null) throw ValueException::null("filter"); if ($filter === null) throw ValueException::null("filter");
$this->_create($channel); $this->_create($channel);
$this->verifixFilter($channel, $filter); $this->verifixFilter($channel, $filter);
$row = $this->db()->one(cl::merge([ $raw = $this->db()->one(cl::merge([
"select", "select",
"from" => $channel->getTableName(), "from" => $channel->getTableName(),
"where" => $filter, "where" => $filter,
], $mergeQuery)); ], $mergeQuery));
return $this->unserialize($channel, $row); return $this->unserialize($channel, $raw);
} }
function one(?string $channel, $filter, ?array $mergeQuery=null): ?array { function one(?string $channel, $filter, ?array $mergeQuery=null): ?array {
return $this->_one($this->getChannel($channel), $filter, $mergeQuery); return $this->_one($this->getChannel($channel), $filter, $mergeQuery);
} }
private function _allCached(string $id, CapacitorChannel $channel, $filter, ?array $mergeQuery=null): Traversable {
$this->_create($channel);
$this->verifixFilter($channel, $filter);
$rows = $this->db()->all(cl::merge([
"select",
"from" => $channel->getTableName(),
"where" => $filter,
], $mergeQuery), null, $this->getPrimaryKeys($channel));
if ($channel->isUseCache()) {
$cacheIds = [$id, get_class($channel)];
cache::get()->resetCached($cacheIds);
$rows = cache::new(null, $cacheIds, function() use ($rows) {
yield from $rows;
});
}
foreach ($rows as $key => $row) {
yield $key => $this->unserialize($channel, $row);
}
}
/** /**
* obtenir les lignes correspondant au filtre sur le canal spécifié * obtenir les lignes correspondant au filtre sur le canal spécifié
* *
* si $filter n'est pas un tableau, il est transformé en ["id_" => $filter] * si $filter n'est pas un tableau, il est transformé en ["id_" => $filter]
*/ */
function _all(CapacitorChannel $channel, $filter, ?array $mergeQuery=null): Traversable { function _all(CapacitorChannel $channel, $filter, ?array $mergeQuery=null): Traversable {
return $this->_allCached("all", $channel, $filter, $mergeQuery); $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 { function all(?string $channel, $filter, $mergeQuery=null): Traversable {
@ -622,11 +662,13 @@ abstract class CapacitorStorage {
$tableName = $channel->getTableName(); $tableName = $channel->getTableName();
try { try {
$args ??= []; $args ??= [];
$all = $this->_allCached("each", $channel, $filter, $mergeQuery); $rows = $this->_all($channel, $filter, $mergeQuery);
foreach ($all as $values) { foreach ($rows as $row) {
$rowIds = $this->getRowIds($channel, $values); $rowIds = $this->getRowIds($channel, $row);
$updates = $onEach->invoke([$values["item"], $values, ...$args]); $updates = $onEach->invoke([$row, ...$args]);
if (is_array($updates) && $updates) { if ($updates === [false]) {
break;
} elseif ($updates !== null) {
if (!array_key_exists("modified_", $updates)) { if (!array_key_exists("modified_", $updates)) {
$updates["modified_"] = date("Y-m-d H:i:s"); $updates["modified_"] = date("Y-m-d H:i:s");
} }
@ -689,10 +731,10 @@ abstract class CapacitorStorage {
$tableName = $channel->getTableName(); $tableName = $channel->getTableName();
try { try {
$args ??= []; $args ??= [];
$all = $this->_allCached("delete", $channel, $filter); $rows = $this->_all($channel, $filter);
foreach ($all as $values) { foreach ($rows as $row) {
$rowIds = $this->getRowIds($channel, $values); $rowIds = $this->getRowIds($channel, $row);
$shouldDelete = boolval($onDelete->invoke([$values["item"], $values, ...$args])); $shouldDelete = boolval($onDelete->invoke([$row, ...$args]));
if ($shouldDelete) { if ($shouldDelete) {
$db->exec([ $db->exec([
"delete", "delete",

@ -2,9 +2,23 @@
namespace nulib\db; namespace nulib\db;
interface IDatabase extends ITransactor { interface IDatabase extends ITransactor {
/**
* retourner le type de la base de données (mysql, pgsql, sqlite, ...)
* ce préfixe peut servir à qualifier les migrations
*/
function getPrefix(): ?string;
/** obtenir la requête SQL correspondant à $query */ /** obtenir la requête SQL correspondant à $query */
function getSql($query, ?array $params=null): string; function getSql($query, ?array $params=null): string;
/**
* vérifier la connexion à la base de données, et refaire la connexion si
* nécessaire. NB: si la connexion a la base de données était perdue, les
* 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 ensure(): self;
/** /**
* - si c'est un insert, retourner l'identifiant autogénéré de la ligne * - si c'est un insert, retourner l'identifiant autogénéré de la ligne
* - sinon retourner le nombre de lignes modifiées en cas de succès, ou false * - sinon retourner le nombre de lignes modifiées en cas de succès, ou false

@ -9,6 +9,7 @@ class _select extends _common {
const SCHEMA = [ const SCHEMA = [
"prefix" => "?string", "prefix" => "?string",
"schema" => "?array", "schema" => "?array",
"distinct" => "bool",
"cols" => "?array", "cols" => "?array",
"col_prefix" => "?string", "col_prefix" => "?string",
"from" => "?string", "from" => "?string",
@ -45,8 +46,16 @@ class _select extends _common {
if (($prefix = $query["prefix"] ?? null) !== null) $sql[] = $prefix; if (($prefix = $query["prefix"] ?? null) !== null) $sql[] = $prefix;
## select ## select
self::consume('(select(?:\s*distinct)?)\s*', $tmpsql, $ms); self::consume('(select(?:\s*(distinct))?)\s*', $tmpsql, $ms);
$sql[] = $ms[1]; $sql[] = $ms[1];
if (($ms[2] ?? null) !== null) {
# ne pas le rajouter de nouveau ci-dessous
$distinct = false;
} else $distinct = null;
## distinct
$distinct ??= $query["distinct"] ?? false;
if ($distinct) $sql[] = "distinct";
## cols ## cols
$usercols = []; $usercols = [];

@ -6,7 +6,6 @@ class _update extends _common {
"prefix" => "?string", "prefix" => "?string",
"table" => "?string", "table" => "?string",
"schema" => "?array", "schema" => "?array",
"cols" => "?array",
"values" => "?array", "values" => "?array",
"where" => "?array", "where" => "?array",
"suffix" => "?string", "suffix" => "?string",

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

@ -19,10 +19,6 @@ class MysqlStorage extends CapacitorStorage {
return $this->db; return $this->db;
} }
const PRIMARY_KEY_DEFINITION = [
"id_" => "integer primary key auto_increment",
];
protected function tableExists(string $tableName): bool { protected function tableExists(string $tableName): bool {
$db = $this->db; $db = $this->db;
$found = $db->get([ $found = $db->get([
@ -43,7 +39,7 @@ class MysqlStorage extends CapacitorStorage {
function _getMigration(CapacitorChannel $channel): _mysqlMigration { function _getMigration(CapacitorChannel $channel): _mysqlMigration {
$migrations = cl::merge([ $migrations = cl::merge([
"0init" => [$this->_createSql($channel)], "0init" => [$this->_createSql($channel)],
], $channel->getMigration()); ], $channel->getMigration($this->db->getPrefix()));
return new _mysqlMigration($migrations, $channel->getName()); return new _mysqlMigration($migrations, $channel->getName());
} }

@ -12,6 +12,12 @@ use nulib\ValueException;
class Pdo implements IDatabase { class Pdo implements IDatabase {
use Tvalues; use Tvalues;
const PREFIX = null;
function getPrefix(): ?string {
return static::PREFIX;
}
static function with($pdo, ?array $params=null): self { static function with($pdo, ?array $params=null): self {
if ($pdo instanceof static) { if ($pdo instanceof static) {
return $pdo; return $pdo;
@ -34,11 +40,6 @@ class Pdo implements IDatabase {
} }
const CONFIG_errmodeException_lowerCase = [self::class, "config_errmodeException_lowerCase"]; const CONFIG_errmodeException_lowerCase = [self::class, "config_errmodeException_lowerCase"];
static function config_unbufferedQueries(self $pdo): void {
$pdo->db->setAttribute(\PDO::MYSQL_ATTR_USE_BUFFERED_QUERY, false);
}
const CONFIG_unbufferedQueries = [self::class, "config_unbufferedQueries"];
protected const OPTIONS = [ protected const OPTIONS = [
\PDO::ATTR_PERSISTENT => true, \PDO::ATTR_PERSISTENT => true,
]; ];
@ -119,8 +120,8 @@ class Pdo implements IDatabase {
return $query->getSql(); return $query->getSql();
} }
function open(): self { function open(bool $reopen=false): self {
if ($this->db === null) { if ($this->db === null || $reopen) {
$dbconn = $this->dbconn; $dbconn = $this->dbconn;
$options = $this->options; $options = $this->options;
if (is_callable($options)) { if (is_callable($options)) {
@ -147,6 +148,30 @@ class Pdo implements IDatabase {
return $this->db()->exec($query); return $this->db()->exec($query);
} }
/** @return array|null */
function _query(string $query) {
$db = $this->db();
/** @var \PDOStatement $stmt */
$stmt = $db->query($query);
if ($stmt === false) return null;
try {
return $stmt->fetchAll(\PDO::FETCH_ASSOC);
} finally {
$stmt->closeCursor();
}
}
const SQL_CHECK_LIVE = "select 1";
function ensure(): self {
try {
$this->_query(static::SQL_CHECK_LIVE);
} catch (\PDOException $e) {
$this->open(true);
}
return $this;
}
function exec($query, ?array $params=null) { function exec($query, ?array $params=null) {
$db = $this->db(); $db = $this->db();
$query = new _pdoQuery($query, $params); $query = new _pdoQuery($query, $params);

@ -12,6 +12,12 @@ use nulib\ValueException;
class Pgsql implements IDatabase { class Pgsql implements IDatabase {
use Tvalues; use Tvalues;
const PREFIX = "pgsql";
function getPrefix(): ?string {
return self::PREFIX;
}
static function with($pgsql, ?array $params=null): self { static function with($pgsql, ?array $params=null): self {
if ($pgsql instanceof static) { if ($pgsql instanceof static) {
return $pgsql; return $pgsql;
@ -132,8 +138,8 @@ class Pgsql implements IDatabase {
return $query->getSql(); return $query->getSql();
} }
function open(): self { function open(bool $reopen=false): self {
if ($this->db === null) { if ($this->db === null || $reopen) {
$dbconn = $this->dbconn; $dbconn = $this->dbconn;
$connection_string = [$dbconn[""] ?? null]; $connection_string = [$dbconn[""] ?? null];
unset($dbconn[""]); unset($dbconn[""]);
@ -187,6 +193,31 @@ class Pgsql implements IDatabase {
return true; return true;
} }
function _query(string $query): ?array {
$result = pg_query($this->db(), $query);
if ($result === false) return null;
try {
$rows = [];
while (($row = pg_fetch_assoc($result)) !== false) {
$rows[] = $row;
}
return $rows;
} finally {
pg_free_result($result);
}
}
const SQL_CHECK_LIVE = "select 1";
function ensure(): self {
try {
$this->_query(static::SQL_CHECK_LIVE);
} catch (\PDOException $e) {
$this->open(true);
}
return $this;
}
function getLastSerial() { function getLastSerial() {
$db = $this->db(); $db = $this->db();
$result = @pg_query($db, "select lastval()"); $result = @pg_query($db, "select lastval()");

@ -9,6 +9,10 @@ class PgsqlStorage extends CapacitorStorage {
const SERDATA_DEFINITION = "text"; const SERDATA_DEFINITION = "text";
const SERSUM_DEFINITION = "varchar(40)"; const SERSUM_DEFINITION = "varchar(40)";
const SERTS_DEFINITION = "timestamp"; const SERTS_DEFINITION = "timestamp";
const GENSERIAL_DEFINITION = "serial primary key";
const GENTEXT_DEFINITION = "text";
const GENBOOL_DEFINITION = "boolean default false";
const GENUUID_DEFINITION = "uuid";
function __construct($pgsql) { function __construct($pgsql) {
$this->db = Pgsql::with($pgsql); $this->db = Pgsql::with($pgsql);
@ -20,10 +24,6 @@ class PgsqlStorage extends CapacitorStorage {
return $this->db; return $this->db;
} }
const PRIMARY_KEY_DEFINITION = [
"id_" => "serial primary key",
];
protected function tableExists(string $tableName): bool { protected function tableExists(string $tableName): bool {
if (($index = strpos($tableName, ".")) !== false) { if (($index = strpos($tableName, ".")) !== false) {
$schemaName = substr($tableName, 0, $index); $schemaName = substr($tableName, 0, $index);
@ -44,7 +44,7 @@ class PgsqlStorage extends CapacitorStorage {
function _getMigration(CapacitorChannel $channel): _pgsqlMigration { function _getMigration(CapacitorChannel $channel): _pgsqlMigration {
$migrations = cl::merge([ $migrations = cl::merge([
"0init" => [$this->_createSql($channel)], "0init" => [$this->_createSql($channel)],
], $channel->getMigration()); ], $channel->getMigration($this->db->getPrefix()));
return new _pgsqlMigration($migrations, $channel->getName()); return new _pgsqlMigration($migrations, $channel->getName());
} }

@ -19,6 +19,12 @@ use SQLite3Stmt;
class Sqlite implements IDatabase { class Sqlite implements IDatabase {
use Tvalues; use Tvalues;
const PREFIX = "sqlite";
function getPrefix(): ?string {
return self::PREFIX;
}
static function with($sqlite, ?array $params=null): self { static function with($sqlite, ?array $params=null): self {
if ($sqlite instanceof static) { if ($sqlite instanceof static) {
return $sqlite; return $sqlite;
@ -151,8 +157,8 @@ class Sqlite implements IDatabase {
return $query->getSql(); return $query->getSql();
} }
function open(): self { function open(bool $reopen=false): self {
if ($this->db === null) { if ($this->db === null || $reopen) {
$this->db = new SQLite3($this->file, $this->flags, $this->encryptionKey); $this->db = new SQLite3($this->file, $this->flags, $this->encryptionKey);
_config::with($this->config)->configure($this); _config::with($this->config)->configure($this);
_sqliteMigration::with($this->migration)->migrate($this); _sqliteMigration::with($this->migration)->migrate($this);
@ -186,6 +192,31 @@ class Sqlite implements IDatabase {
return $this->db()->exec($query); return $this->db()->exec($query);
} }
function _query(string $query): ?array {
$result = $this->db()->query($query);
if ($result === false) return null;
try {
$rows = [];
while (($row = $result->fetchArray(SQLITE3_ASSOC)) !== false) {
$rows[] = $row;
}
return $rows;
} finally {
$result->finalize();
}
}
const SQL_CHECK_LIVE = "select 1";
function ensure(): self {
try {
$this->_query(static::SQL_CHECK_LIVE);
} catch (\PDOException $e) {
$this->open(true);
}
return $this;
}
function exec($query, ?array $params=null) { function exec($query, ?array $params=null) {
$db = $this->db(); $db = $this->db();
$query = new _sqliteQuery($query, $params); $query = new _sqliteQuery($query, $params);

@ -9,6 +9,8 @@ use nulib\db\CapacitorStorage;
* Class SqliteStorage * Class SqliteStorage
*/ */
class SqliteStorage extends CapacitorStorage { class SqliteStorage extends CapacitorStorage {
const GENSERIAL_DEFINITION = "integer primary key autoincrement";
function __construct($sqlite) { function __construct($sqlite) {
$this->db = Sqlite::with($sqlite); $this->db = Sqlite::with($sqlite);
} }
@ -19,10 +21,6 @@ class SqliteStorage extends CapacitorStorage {
return $this->db; return $this->db;
} }
const PRIMARY_KEY_DEFINITION = [
"id_" => "integer primary key autoincrement",
];
protected function tableExists(string $tableName): bool { protected function tableExists(string $tableName): bool {
$found = $this->db->get([ $found = $this->db->get([
# depuis la version 3.33.0 le nom officiel de la table est sqlite_schema, # depuis la version 3.33.0 le nom officiel de la table est sqlite_schema,
@ -36,26 +34,10 @@ class SqliteStorage extends CapacitorStorage {
function _getMigration(CapacitorChannel $channel): _sqliteMigration { function _getMigration(CapacitorChannel $channel): _sqliteMigration {
$migrations = cl::merge([ $migrations = cl::merge([
"0init" => [$this->_createSql($channel)], "0init" => [$this->_createSql($channel)],
], $channel->getMigration()); ], $channel->getMigration($this->db->getPrefix()));
return new _sqliteMigration($migrations, $channel->getName()); return new _sqliteMigration($migrations, $channel->getName());
} }
function channelExists(string $name, ?array &$row=null): bool {
$row = $this->db->one([
"select",
"from" => static::CHANNELS_TABLE,
"where" => ["name" => $name],
]);
return $row !== null;
}
function getChannels(): iterable {
return $this->db->all([
"select",
"from" => static::CHANNELS_TABLE,
]);
}
protected function _addToChannelsSql(CapacitorChannel $channel): array { protected function _addToChannelsSql(CapacitorChannel $channel): array {
$sql = parent::_addToChannelsSql($channel); $sql = parent::_addToChannelsSql($channel);
$sql[0] = "insert or ignore"; $sql[0] = "insert or ignore";

12
php/src/ext/utils.php Normal file

@ -0,0 +1,12 @@
<?php
namespace nulib\ext;
class utils {
/** générateur d'UUIDv4 "du pauvre" */
static final function uuidgen(): string {
$data = random_bytes(16);
$data[6] = chr(ord($data[6]) & 0x0f | 0x40); // set version to 0100
$data[8] = chr(ord($data[8]) & 0x3f | 0x80); // set bits 6-7 to 10
return vsprintf("%s%s-%s-%s-%s-%s%s%s", str_split(bin2hex($data), 4));
}
}

@ -117,4 +117,18 @@ class file {
} }
return $file; return $file;
} }
static function string_reader(string $content, ?callable $func=null): MemoryStream {
$file = new MemoryStream();
$file->fwrite($content);
$file->rewind();
if ($func !== null) {
try {
$func($file);
} finally {
$file ->close();
}
}
return $file;
}
} }

@ -26,7 +26,7 @@ class CsvBuilder extends AbstractBuilder {
} }
protected function _checkOk(): bool { protected function _checkOk(): bool {
$size = $this->ftell(); $this->size = $size = $this->ftell();
if ($size === 0) return false; if ($size === 0) return false;
$this->rewind(); $this->rewind();
return true; return true;

@ -118,18 +118,8 @@ abstract class AbstractBuilder extends TempStream implements IBuilder {
if ($unsetRows) $this->rows = null; if ($unsetRows) $this->rows = null;
} }
abstract protected function _sendContentType(): void; function getCount(): int {
return $this->index;
protected bool $sentHeaders = false;
function sendHeaders(): void {
if ($this->sentHeaders) return;
$this->_sendContentType();
$output = $this->output;
if ($output !== null) {
http::download_as(path::filename($output));
}
$this->sentHeaders = true;
} }
protected function _build(?iterable $rows=null): void { protected function _build(?iterable $rows=null): void {
@ -141,6 +131,8 @@ abstract class AbstractBuilder extends TempStream implements IBuilder {
protected bool $built = false, $closed = false; protected bool $built = false, $closed = false;
protected ?int $size = null;
function build(?iterable $rows=null, bool $close=true): bool { function build(?iterable $rows=null, bool $close=true): bool {
$ok = true; $ok = true;
if (!$this->built) { if (!$this->built) {
@ -154,6 +146,24 @@ abstract class AbstractBuilder extends TempStream implements IBuilder {
return $ok; return $ok;
} }
abstract protected function _sendContentType(): void;
protected bool $sentHeaders = false;
function sendHeaders(): void {
if ($this->sentHeaders) return;
$this->_sendContentType();
$output = $this->output;
if ($output !== null) {
http::download_as(path::filename($output));
}
$size = $this->size;
if ($size !== null) {
header("Content-Length: $size");
}
$this->sentHeaders = true;
}
function sendFile(?iterable $rows=null): int { function sendFile(?iterable $rows=null): int {
if (!$this->built) { if (!$this->built) {
$this->_build($rows); $this->_build($rows);

@ -52,6 +52,7 @@ abstract class AbstractReader implements IReader {
$this->headers = $params["headers"] ?? static::HEADERS; $this->headers = $params["headers"] ?? static::HEADERS;
$this->useHeaders = $params["use_headers"] ?? static::USE_HEADERS; $this->useHeaders = $params["use_headers"] ?? static::USE_HEADERS;
$this->input = $params["input"] ?? static::INPUT; $this->input = $params["input"] ?? static::INPUT;
$this->skip = $params["skip"] ?? 0;
$this->trim = boolval($params["trim"] ?? static::TRIM); $this->trim = boolval($params["trim"] ?? static::TRIM);
$this->emptyAsNull = boolval($params["empty_as_null"] ?? static::EMPTY_AS_NULL); $this->emptyAsNull = boolval($params["empty_as_null"] ?? static::EMPTY_AS_NULL);
$this->parseNone = boolval($params["parse_none"] ?? static::PARSE_NONE); $this->parseNone = boolval($params["parse_none"] ?? static::PARSE_NONE);
@ -67,6 +68,8 @@ abstract class AbstractReader implements IReader {
protected $input; protected $input;
protected int $skip;
protected bool $trim; protected bool $trim;
protected bool $emptyAsNull; protected bool $emptyAsNull;
@ -81,15 +84,16 @@ abstract class AbstractReader implements IReader {
protected function cookRow(array &$row): bool { protected function cookRow(array &$row): bool {
if (!$this->useHeaders) return true; if (!$this->useHeaders) return true;
if ($this->isrc == 0) { if ($this->skip > 0) {
$this->skip--;
return false;
}
if ($this->headers === null) {
# ligne d'en-tête # ligne d'en-tête
$headers = $this->headers; if ($this->schema === null) $headers = null;
if ($headers === null) { else $headers = array_keys($this->schema);
if ($this->schema === null) $headers = null; if ($headers === null) $headers = $row;
else $headers = array_keys($this->schema); $this->headers = $headers;
if ($headers === null) $headers = $row;
$this->headers = $headers;
}
return false; return false;
} }
A::ensure_size($row, count($this->headers)); A::ensure_size($row, count($this->headers));

@ -1,6 +1,8 @@
<?php <?php
namespace nulib\file\tab; namespace nulib\file\tab;
use nulib\file\IWriter;
interface IBuilder extends \nulib\file\IReader { interface IBuilder extends \nulib\file\IReader {
function writeHeaders(?array $headers=null): void; function writeHeaders(?array $headers=null): void;
@ -8,7 +10,23 @@ interface IBuilder extends \nulib\file\IReader {
function writeAll(?iterable $rows=null): void; function writeAll(?iterable $rows=null): void;
/**
* @return int le nombre de ligne qui ont été écrites (sans compte l'en-tête)
*/
function getCount(): int;
/** préparer le fichier (sans l'envoyer) */
function build(?iterable $rows=null, bool $close=true): bool;
/** enregistrer le fichier préparé avec {@link build()} */
function copyTo(IWriter $dest, bool $closeWriter=false, bool $closeReader=true): void;
/** envoyer les en-têtes */
function sendHeaders(): void; function sendHeaders(): void;
/**
* envoyer le fichier pour téléchargement (la méthode {@link sendHeaders()}
* est automatiquement appelée le cas échéant
*/
function sendFile(?iterable $rows=null): int; function sendFile(?iterable $rows=null): int;
} }

@ -1,4 +1,4 @@
# TOOD # nulib\output
* dans msg::action($m, function() {}), *bloquer* la marque pour empêcher d'aller * dans msg::action($m, function() {}), *bloquer* la marque pour empêcher d'aller
plus bas que prévu. comme ça s'il y a plusieurs success ou failure dans la plus bas que prévu. comme ça s'il y a plusieurs success ou failure dans la
@ -32,4 +32,10 @@ pour l'UI
peut-être rajouter `ui` (ou `web`?) en plus de say, log, debuglog? peut-être rajouter `ui` (ou `web`?) en plus de say, log, debuglog?
--> ou renommer `say` en `console`, et `ui` en `say` --> ou renommer `say` en `console`, et `ui` en `say`
* [ ] ajouter une option `Application::MSG_SIGNALS` qui fait que
* les méthodes `msg::eXXX()` appellent automatiquement `app::_dispatch_signals()`
* [ ] ajouter une option `Application::MSG_ACTIONS` qui fait que
* `msg::section()` et/ou `msg::title()` appellent automatiquement `app::action()`
* `msg::estep()` appelle automatiquement `app::step()`
-*- coding: utf-8 mode: markdown -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8:noeol:binary -*- coding: utf-8 mode: markdown -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8:noeol:binary

@ -7,10 +7,10 @@ use nulib\php\func;
* Class Printer: wrapper autour d'une instance de {@link IContent} qui affiche * Class Printer: wrapper autour d'une instance de {@link IContent} qui affiche
* le contenu au lieu de le retourner * le contenu au lieu de le retourner
* *
* cette classe est conçue pour wrapper des objets complexes dont le contenu est * cette classe est conçue pour wrapper des objets complexes dont le contenu
* contruit au fur et à mesure: ainsi, les objets peuvent être utilisés tels * peut être multiple et est contruit au fur et à mesure: ainsi, les objets
* quels dans un contenu, ou alors être wrappés si on veut simplement les * peuvent être utilisés tels quels dans un contenu, ou alors être wrappés si
* afficher * on veut simplement les afficher
*/ */
class Printer implements IPrintable { class Printer implements IPrintable {
function __construct(IContent $content) { function __construct(IContent $content) {
@ -24,6 +24,9 @@ class Printer implements IPrintable {
c::write($content); c::write($content);
} }
/**
* afficher le contenu retourné par une méthode spécifique de $content
*/
function __call($name, $args) { function __call($name, $args) {
$content = func::call([$this->content, $name], ...$args); $content = func::call([$this->content, $name], ...$args);
c::write($content); c::write($content);

@ -15,6 +15,6 @@ class Date extends DateTime {
} }
function format($format=self::DEFAULT_FORMAT): string { function format($format=self::DEFAULT_FORMAT): string {
return \DateTime::format($format); return parent::format($format);
} }
} }

@ -4,6 +4,7 @@ namespace nulib\php\time;
use DateTimeInterface; use DateTimeInterface;
use DateTimeZone; use DateTimeZone;
use InvalidArgumentException; use InvalidArgumentException;
use nulib\str;
/** /**
* Class DateTime: une date et une heure * Class DateTime: une date et une heure
@ -29,10 +30,20 @@ class DateTime extends \DateTime {
else return new static($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 DMY_PATTERN = '/^(\d+)\/(\d+)(?:\/(\d+))?$/';
const YMD_PATTERN = '/^((?:\d{2})?\d{2})(\d{2})(\d{2})$/'; const YMD_PATTERN = '/^((?:\d{2})?\d{2})(\d{2})(\d{2})$/';
const DMYHIS_PATTERN = '/^(\d+)\/(\d+)(?:\/(\d+))? +(\d+)[h:.](\d+)(?:[:.](\d+))?$/'; 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])?$/'; 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) { protected static function get_value(array $datetime, ?string $key, ?string $k, ?int $index) {
@ -128,6 +139,12 @@ class DateTime extends \DateTime {
return false; 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 { static function _YmdHMSZ_format(\DateTime $datetime): string {
$YmdHMS = $datetime->format("Ymd\\THis"); $YmdHMS = $datetime->format("Ymd\\THis");
$Z = $datetime->format("P"); $Z = $datetime->format("P");
@ -145,6 +162,7 @@ class DateTime extends \DateTime {
"second" => "s", "second" => "s",
"wday" => "N", "wday" => "N",
"wnum" => "W", "wnum" => "W",
"nbsecs" => [self::class, "_nbsecs_format"],
]; ];
const STRING_FORMATS = [ const STRING_FORMATS = [
"timezone" => "P", "timezone" => "P",
@ -155,14 +173,6 @@ class DateTime extends \DateTime {
"YmdHMSZ" => [self::class, "_YmdHMSZ_format"], "YmdHMSZ" => [self::class, "_YmdHMSZ_format"],
]; ];
static function clone(DateTimeInterface $dateTime): self {
if ($dateTime instanceof static) return clone $dateTime;
$clone = new static();
$clone->setTimestamp($dateTime->getTimestamp());
$clone->setTimezone($dateTime->getTimezone());
return $clone;
}
/** /**
* corriger une année à deux chiffres qui est située dans le passé et * corriger une année à deux chiffres qui est située dans le passé et
* retourner l'année à 4 chiffres. * retourner l'année à 4 chiffres.
@ -206,16 +216,34 @@ class DateTime extends \DateTime {
return $year; return $year;
} }
function __construct($datetime="now", DateTimeZone $timezone=null) { 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"; $datetime ??= "now";
if ($datetime instanceof \DateTimeInterface) { if ($datetime instanceof \DateTimeInterface) {
if ($timezone === null) $timezone = $datetime->getTimezone(); $timezone ??= $datetime->getTimezone();
parent::__construct(); parent::__construct();
$this->setTimestamp($datetime->getTimestamp()); $this->setTimestamp($datetime->getTimestamp());
$this->setTimezone($timezone); $this->setTimezone($timezone);
} elseif (is_int($datetime)) { } elseif (is_int($datetime)) {
parent::__construct("now", $timezone); parent::__construct("now", $timezone);
$this->setTimestamp($datetime); $this->setTimestamp($datetime);
} elseif (is_string($datetime)) { } elseif (is_string($datetime)) {
$Y = $H = $Z = null; $Y = $H = $Z = null;
if (preg_match(self::DMY_PATTERN, $datetime, $ms)) { if (preg_match(self::DMY_PATTERN, $datetime, $ms)) {
@ -253,22 +281,34 @@ class DateTime extends \DateTime {
if ($Y !== null) { if ($Y !== null) {
if ($H === null) $datetime = sprintf("%04d-%02d-%02d", $Y, $m, $d); 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); else $datetime = sprintf("%04d-%02d-%02d %02d:%02d:%02d", $Y, $m, $d, $H, $M, $S);
if ($Z !== null) $timezone = new DateTimeZone("UTC"); if ($Z !== null) $timezone ??= new DateTimeZone(self::fix_z($Z));
} }
parent::__construct($datetime, $timezone); parent::__construct($datetime, $timezone);
} elseif (is_array($datetime) && ($datetime = self::parse_array($datetime)) !== null) { } elseif (is_array($datetime) && ($datetime = self::parse_array($datetime)) !== null) {
[$Y, $m, $d, $H, $M, $S, $tz] = $datetime; $setTimezone = $timezone;
$timezone = null;
[$Y, $m, $d, $H, $M, $S, $Z] = $datetime;
if ($H === null && $M === null && $S === null) { if ($H === null && $M === null && $S === null) {
$datetime = sprintf("%04d-%02d-%02d", $Y, $m, $d); $datetime = sprintf("%04d-%02d-%02d", $Y, $m, $d);
} else { } else {
$datetime = sprintf("%04d-%02d-%02d %02d:%02d:%02d", $Y, $m, $d, $H ?? 0, $M ?? 0, $S ?? 0); $datetime = sprintf("%04d-%02d-%02d %02d:%02d:%02d", $Y, $m, $d, $H ?? 0, $M ?? 0, $S ?? 0);
} }
if ($tz === "Z" || $tz === "z") $tz = "UTC"; if ($Z !== null) $timezone ??= new DateTimeZone(self::fix_z($Z));
$timezone = $tz !== null? new DateTimeZone($tz): null;
parent::__construct($datetime, $timezone); parent::__construct($datetime, $timezone);
} else { } else {
throw new InvalidArgumentException("datetime must be a string or an instance of DateTimeInterface"); 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);
}
}
function clone(): self {
return clone $this;
} }
function diff($target, $absolute=false): DateInterval { function diff($target, $absolute=false): DateInterval {
@ -276,7 +316,13 @@ class DateTime extends \DateTime {
} }
function format($format=self::DEFAULT_FORMAT): string { function format($format=self::DEFAULT_FORMAT): string {
return \DateTime::format($format); 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);
} }
/** /**
@ -331,7 +377,7 @@ class DateTime extends \DateTime {
function __get($name) { function __get($name) {
if (array_key_exists($name, self::INT_FORMATS)) { if (array_key_exists($name, self::INT_FORMATS)) {
$format = self::INT_FORMATS[$name]; $format = self::INT_FORMATS[$name];
if (is_callable($format)) return $format($this); if (is_callable($format)) return intval($format($this));
else return intval($this->format($format)); else return intval($this->format($format));
} elseif (array_key_exists($name, self::STRING_FORMATS)) { } elseif (array_key_exists($name, self::STRING_FORMATS)) {
$format = self::STRING_FORMATS[$name]; $format = self::STRING_FORMATS[$name];

@ -40,8 +40,8 @@ class Delay {
"s" => [1, 0], "s" => [1, 0],
]; ];
static function compute_dest(int $x, string $u, ?int $y, DateTime $from): array { static function compute_dest(int $x, string $u, ?int $y, ?DateTimeInterface $from): array {
$dest = DateTime::clone($from); $dest = DateTime::with($from)->clone();
$yu = null; $yu = null;
switch ($u) { switch ($u) {
case "w": case "w":
@ -92,7 +92,7 @@ class Delay {
function __construct($delay, ?DateTimeInterface $from=null) { function __construct($delay, ?DateTimeInterface $from=null) {
if ($from === null) $from = new DateTime(); if ($from === null) $from = new DateTime();
if ($delay === "INF") { if ($delay === "INF") {
$dest = DateTime::clone($from); $dest = DateTime::with($from)->clone();
$dest->add(new DateInterval("P9999Y")); $dest->add(new DateInterval("P9999Y"));
$repr = "INF"; $repr = "INF";
} elseif (is_int($delay)) { } elseif (is_int($delay)) {

10
php/src/php/time/TODO.md Normal file

@ -0,0 +1,10 @@
# nulib\php\time
* Date, DateTime sont immutables par défaut. par exemple, add() retourne un nouvel objet.
ajouter une version des méthodes qui modifie les données en place en les
préfixant de `_` e.g `_add()`
en terme d'implémentation, dériver \DateTime pour supporter les modification
en place, bien que ce ne soit pas le fonctionnement par défaut
-*- coding: utf-8 mode: markdown -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8:noeol:binary

@ -17,6 +17,11 @@ class str {
return $s; return $s;
} }
/** s'assure que $s est une chaine s'il n'est pas null */
static final function ensure(&$s): void {
if ($s !== null) $s = self::with($s);
}
/** /**
* Retourner $s converti en chaine non nulle, ou "" si $s est fausse selon les * Retourner $s converti en chaine non nulle, ou "" si $s est fausse selon les
* règles de PHP * règles de PHP

@ -1,124 +0,0 @@
<?php
namespace nulib\tools;
use nulib\app;
use nulib\app\cli\Application;
use nulib\app\RunFile;
use nulib\ExitError;
use nulib\ext\yaml;
use nulib\os\path;
use nulib\os\proc\Cmd;
use nulib\os\sh;
use nulib\output\msg;
class BgLauncherApp extends Application {
const ACTION_INFOS = 0, ACTION_START = 1, ACTION_STOP = 2;
const ARGS = [
"purpose" => "lancer un script en tâche de fond",
"usage" => "ApplicationClass args...",
"sections" => [
parent::VERBOSITY_SECTION,
],
["-i", "--infos", "name" => "action", "value" => self::ACTION_INFOS,
"help" => "Afficher des informations sur la tâche",
],
["-s", "--start", "name" => "action", "value" => self::ACTION_START,
"help" => "Démarrer la tâche",
],
["-k", "--stop", "name" => "action", "value" => self::ACTION_STOP,
"help" => "Arrêter la tâche",
],
];
protected int $action = self::ACTION_START;
protected ?array $args = null;
static function show_infos(RunFile $runfile, ?int $level=null): void {
msg::print($runfile->getDesc(), $level);
msg::print(yaml::with(["data" => $runfile->read()]), ($level ?? 0) - 1);
}
function main() {
$args = $this->args;
$appClass = $args[0] ?? null;
if ($appClass === null) {
self::die("Vous devez spécifier la classe de l'application");
}
$appClass = $args[0] = str_replace("/", "\\", $appClass);
if (!class_exists($appClass)) {
self::die("$appClass: classe non trouvée");
}
$useRunfile = constant("$appClass::USE_RUNFILE");
if (!$useRunfile) {
self::die("Cette application ne supporte le lancement en tâche de fond");
}
$runfile = app::with($appClass)->getRunfile();
switch ($this->action) {
case self::ACTION_START:
$argc = count($args);
$appClass::_manage_runfile($argc, $args, $runfile);
if ($runfile->warnIfLocked()) self::exit(app::EC_LOCKED);
array_splice($args, 0, 0, [
PHP_BINARY,
path::abspath(NULIB_APP_app_launcher),
]);
app::params_putenv();
self::_start($args, $runfile);
break;
case self::ACTION_STOP:
self::_stop($runfile);
self::show_infos($runfile, -1);
break;
case self::ACTION_INFOS:
self::show_infos($runfile);
break;
}
}
public static function _start(array $args, Runfile $runfile): void {
$pid = pcntl_fork();
if ($pid == -1) {
# parent, impossible de forker
throw new ExitError(app::EC_FORK_PARENT, "Unable to fork");
} elseif (!$pid) {
# child, fork ok
$runfile->wfPrepare($pid);
$outfile = $runfile->getOutfile() ?? "/tmp/NULIB_APP_app_console.out";
$exitcode = app::EC_FORK_CHILD;
try {
# rediriger STDIN, STDOUT et STDERR
fclose(fopen($outfile, "wb")); // vider le fichier
fclose(STDIN); $in = fopen("/dev/null", "rb");
fclose(STDOUT); $out = fopen($outfile, "ab");
fclose(STDERR); $err = fopen($outfile, "ab");
# puis lancer la commande
$cmd = new Cmd($args);
$cmd->addSource("/g/init.env");
$cmd->addRedir("both", $outfile, true);
$cmd->fork_exec($exitcode, false);
sh::_waitpid(-$pid, $exitcode);
} finally {
$runfile->wfReaped($exitcode);
}
}
}
public static function _stop(Runfile $runfile): bool {
$data = $runfile->read();
$pid = $runfile->_getCid($data);
msg::action("stop $pid");
if ($runfile->wfKill($reason)) {
msg::asuccess();
return true;
} else {
msg::afailure($reason);
return false;
}
}
}

@ -1,53 +0,0 @@
<?php
namespace nulib\tools;
use nulib\app;
use nulib\app\cli\Application;
use nulib\output\msg;
use nulib\php\time\DateTime;
use nulib\text\words;
class SteamTrainApp extends Application {
const PROJDIR = __DIR__.'/../..';
const TITLE = "Train à vapeur";
const USE_LOGFILE = true;
const USE_RUNFILE = true;
const USE_RUNLOCK = true;
const ARGS = [
"purpose" => self::TITLE,
"description" => <<<EOT
Cette application peut être utilisée pour tester le lancement des tâches de fond
EOT,
["-c", "--count", "args" => 1,
"help" => "spécifier le nombre d'étapes",
],
["-f", "--force-enabled", "value" => true,
"help" => "lancer la commande même si les tâches planifiées sont désactivées",
],
["-n", "--no-install-signal-handler", "value" => false,
"help" => "ne pas installer le gestionnaire de signaux",
],
];
protected $count = 100;
protected bool $forceEnabled = false;
protected bool $installSignalHandler = true;
function main() {
app::check_bgapplication_enabled($this->forceEnabled);
if ($this->installSignalHandler) app::install_signal_handler();
$count = intval($this->count);
msg::info("Starting train for ".words::q($count, "step#s"));
app::action("Running train...", $count);
for ($i = 1; $i <= $count; $i++) {
msg::print("Tchou-tchou! x $i");
app::step();
sleep(1);
}
msg::info("Stopping train at ".new DateTime());
}
}

@ -276,7 +276,7 @@ class txt {
* supprimer les diacritiques de la chaine $text * supprimer les diacritiques de la chaine $text
* *
* la translitération se fait avec les règles de la locale spécifiée. * la translitération se fait avec les règles de la locale spécifiée.
* NB: la translitération ne fonctionne pas si LC_CTYPE == C ou POISX * NB: la translitération ne fonctionne pas si LC_CTYPE == C ou POSIX
*/ */
static final function remove_diacritics(?string $text, string $locale="fr_FR.UTF-8"): ?string { static final function remove_diacritics(?string $text, string $locale="fr_FR.UTF-8"): ?string {
if ($text === null) return null; if ($text === null) return null;

@ -1,39 +0,0 @@
#!/bin/bash
# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8
source "$(dirname -- "$0")/../../load.sh" || exit 1
declare -A DESTDIRS=(
[template-_bg_launcher.php]=sbin
[template-.launcher.php]=_cli
[template-_wrapper.sh]=_cli
)
declare -A MODES=(
[template-_bg_launcher.php]=+x
[template-.launcher.php]=
[template-_wrapper.sh]=+x
)
projdir=
args=(
"copier les templates dans le projet en cours"
#"usage"
-d:,--projdir: .
)
parse_args "$@"; set -- "${args[@]}"
if [ -n "$projdir" ]; then
cd "$projdir" || die
fi
[ -f composer.json ] || die "$(basename "$(dirname "$(pwd)")"): n'est pas un projet composer"
setx -a templates=ls_files "$MYDIR" "template-*"
for template in "${templates[@]}"; do
destdir="${DESTDIRS[$template]}"
[ -n "$destdir" ] || die "$template: la destination n'est pas configurée"
mode="${MODES[$template]}"
destname="${template#template-}"
tail -n+4 "$MYDIR/$template" >"$destdir/$destname"
[ -n "$mode" ] && chmod "$mode" "$destdir/$destname"
done

2
php/tests/.gitignore vendored Normal file

@ -0,0 +1,2 @@
*.db*
*.cache

@ -23,4 +23,72 @@ class argsTest extends TestCase {
self::assertSame(["x", "1", "2", "3", "y"], args::from_array(["x", [1, 2, 3], "y"])); self::assertSame(["x", "1", "2", "3", "y"], args::from_array(["x", [1, 2, 3], "y"]));
} }
function testBuild_query() {
self::assertSame(null, args::build_query(null));
self::assertSame(null, args::build_query([]));
self::assertSame(["a" => true], args::build_query(["a"]));
self::assertSame(["a" => true], args::build_query(["+a"]));
self::assertSame(["a" => false], args::build_query(["-a"]));
self::assertSame(["a" => false], args::build_query(["~a"]));
self::assertSame(["x" => "a"], args::build_query(["x=a"]));
self::assertSame(["x" => 0], args::build_query(["x:int=0"]));
self::assertSame(["x" => 42], args::build_query(["x:int=42"]));
self::assertSame(["x" => false], args::build_query(["x:bool=0"]));
self::assertSame(["x" => true], args::build_query(["x:bool=42"]));
self::assertSame(["x" => ["a", "b"]], args::build_query(["x:array=a,b"]));
self::assertSame(["x" => [0, 42]], args::build_query(["x:array:int=0,42"]));
self::assertSame(["x" => [0, 42]], args::build_query(["x:int:array=0,42"]));
self::assertSame(["x" => "a", "y" => "b"], args::build_query(["x=a", "y=b"]));
self::assertSame(["x" => ["a", "b"]], args::build_query(["x=a", "x=b"]));
}
function testBuild_method_args() {
self::assertSame([], args::build_method_args(null));
self::assertSame([], args::build_method_args([]));
self::assertSame(["a"], args::build_method_args(["a"]));
self::assertSame(["a", "b"], args::build_method_args(["a", "b"]));
self::assertSame([0], args::build_method_args(["int:0"]));
self::assertSame([42], args::build_method_args(["int:42"]));
# pour le moment, pas de tint
self::assertSame([0], args::build_method_args(["int:"]));
self::assertSame([0], args::build_method_args(["int:truc"]));
self::assertSame([false], args::build_method_args(["bool:0"]));
self::assertSame([true], args::build_method_args(["bool:42"]));
self::assertSame([false], args::build_method_args(["bool:"]));
self::assertSame([true], args::build_method_args(["bool:truc"]));
# pour le moment, pas de tbool
self::assertSame([true], args::build_method_args(["bool:false"]));
self::assertSame([true], args::build_method_args(["bool:true"]));
self::assertSame([["a", "b"]], args::build_method_args(["array:a,b"]));
self::assertSame([["x" => "a", "y" => "b"]], args::build_method_args(["array:x:a,y:b"]));
# pour le moment, pas de tint
self::assertSame([[0, 42, 0, 0]], args::build_method_args(["array:int:0,42,,truc"]));
self::assertSame([["x" => 0, "y" => 42]], args::build_method_args(["array:int:x:0,y:42"]));
# pour le moment, pas de tbool
self::assertSame([[false, true, false, true, true, true]], args::build_method_args(["array:bool:0,42,,truc,false,true"]));
self::assertSame([["x" => false, "y" => true]], args::build_method_args(["array:bool:x:0,y:42"]));
self::assertSame([["a" => true]], args::build_method_args(["+a"]));
self::assertSame([["a" => false]], args::build_method_args(["-a"]));
self::assertSame([["a" => false]], args::build_method_args(["~a"]));
self::assertSame([["x" => "a"]], args::build_method_args(["x=a"]));
self::assertSame([["x" => 0]], args::build_method_args(["x:int=0"]));
self::assertSame([["x" => 42]], args::build_method_args(["x:int=42"]));
self::assertSame([["x" => false]], args::build_method_args(["x:bool=0"]));
self::assertSame([["x" => true]], args::build_method_args(["x:bool=42"]));
self::assertSame([["x" => ["a", "b"]]], args::build_method_args(["x:array=a,b"]));
self::assertSame([["x" => [0, 42]]], args::build_method_args(["x:array:int=0,42"]));
self::assertSame([["x" => [0, 42]]], args::build_method_args(["x:int:array=0,42"]));
self::assertSame([["x" => "a", "y" => "b"], "a", "b"], args::build_method_args(["x=a", "a", "y=b", "b"]));
self::assertSame([["x" => ["a", "b"]]], args::build_method_args(["x=a", "x=b"]));
}
} }

@ -1 +0,0 @@
/capacitor.db*

@ -5,6 +5,7 @@ use nulib\db\Capacitor;
use nulib\db\sqlite\impl\MyChannel; use nulib\db\sqlite\impl\MyChannel;
use nulib\db\sqlite\impl\MyChannelV2; use nulib\db\sqlite\impl\MyChannelV2;
use nulib\db\sqlite\impl\MyChannelV3; use nulib\db\sqlite\impl\MyChannelV3;
use nulib\db\sqlite\impl\MyIndexChannel;
use nulib\output\msg; use nulib\output\msg;
use nulib\output\std\StdMessenger; use nulib\output\std\StdMessenger;
use nulib\php\time\DateTime; use nulib\php\time\DateTime;
@ -69,6 +70,42 @@ alter table my add column date_mod datetime;
-- infos -- infos
alter table my add column age integer; alter table my add column age integer;
EOT;
self::assertSame($expected, $sql);
}
function testMigrationIndex() {
$storage = new SqliteStorage(__DIR__.'/capacitor.db');
$data = [
["un", "premier", "first"],
["deux", "deuxieme", "second"],
];
new Capacitor($storage, $channel = new MyIndexChannel());
$channel->reset(true);
$channel->chargeAll($data);
$sql = $channel->getCapacitor()->getCreateSql();
$class = MyIndexChannel::class;
$expected = <<<EOT
-- -*- coding: utf-8 mode: sql -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8
-- autogénéré à partir de $class
-- 0init
create table if not exists my_index (
name varchar not null primary key
, first varchar
, second varchar
, item__ mediumtext
, item__sum_ varchar(40)
, created_ datetime
, modified_ datetime
);
-- index
create index my_index_first on my_index(first);
create index my_index_second on my_index(second);
EOT; EOT;
self::assertSame($expected, $sql); self::assertSame($expected, $sql);
} }

@ -7,6 +7,12 @@ use nulib\db\Capacitor;
use nulib\db\CapacitorChannel; use nulib\db\CapacitorChannel;
class SqliteStorageTest extends TestCase { class SqliteStorageTest extends TestCase {
static function Txx(...$values): void {
foreach ($values as $value) {
var_export($value);
}
}
function _testChargeStrings(SqliteStorage $storage, ?string $channel) { function _testChargeStrings(SqliteStorage $storage, ?string $channel) {
$storage->reset($channel); $storage->reset($channel);
$storage->charge($channel, "first"); $storage->charge($channel, "first");
@ -67,7 +73,8 @@ class SqliteStorageTest extends TestCase {
$capacitor->charge(["name" => "third", "age" => 15]); $capacitor->charge(["name" => "third", "age" => 15]);
$capacitor->charge(["name" => "fourth", "age" => 20]); $capacitor->charge(["name" => "fourth", "age" => 20]);
$setDone = function ($item, $row, $suffix=null) { $setDone = function ($row, $suffix=null) {
$item = $row["item"];
$updates = ["done" => 1]; $updates = ["done" => 1];
if ($suffix !== null) { if ($suffix !== null) {
$item["name"] .= $suffix; $item["name"] .= $suffix;
@ -76,9 +83,9 @@ class SqliteStorageTest extends TestCase {
return $updates; return $updates;
}; };
$capacitor->each(["age" => [">", 10]], $setDone, ["++"]); $capacitor->each(["age" => [">", 10]], $setDone, ["++"]);
$capacitor->each(["done" => 0], $setDone, null); $capacitor->each(["done" => 0], $setDone);
Txx(cl::all($capacitor->discharge(false))); self::Txx(cl::all($capacitor->discharge(false)));
$capacitor->close(); $capacitor->close();
self::assertTrue(true); self::assertTrue(true);
} }
@ -133,16 +140,16 @@ class SqliteStorageTest extends TestCase {
$capacitor->charge(["a" => null, "b" => null]); $capacitor->charge(["a" => null, "b" => null]);
$capacitor->charge(["a" => "first", "b" => "second"]); $capacitor->charge(["a" => "first", "b" => "second"]);
Txx("=== all"); self::Txx("=== all");
/** @var Sqlite $sqlite */ /** @var Sqlite $sqlite */
$sqlite = $capacitor->getStorage()->db(); $sqlite = $capacitor->getStorage()->db();
Txx(cl::all($sqlite->all([ self::Txx(cl::all($sqlite->all([
"select", "select",
"from" => $capacitor->getChannel()->getTableName(), "from" => $capacitor->getChannel()->getTableName(),
]))); ])));
Txx("=== each"); self::Txx("=== each");
$capacitor->each(null, function ($item, $values) { $capacitor->each(null, function ($row) {
Txx($values); self::Txx($row);
}); });
$capacitor->close(); $capacitor->close();
@ -170,99 +177,100 @@ class SqliteStorageTest extends TestCase {
}); });
$capacitor->reset(); $capacitor->reset();
$capacitor->charge(["name" => "first", "age" => 5], function($item, ?array $values, ?array $pvalues) { $capacitor->charge(["name" => "first", "age" => 5], function($item, ?array $row, ?array $prow) {
self::assertSame("first", $item["name"]); self::assertSame("first", $item["name"]);
self::assertSame(5, $item["age"]); self::assertSame(5, $item["age"]);
self::assertnotnull($values); self::assertnotnull($row);
self::assertSame(["name", "age", "item", "item__sum_", "created_", "modified_"], array_keys($values)); self::assertSame(["name", "age", "item", "item__sum_", "created_", "modified_"], array_keys($row));
self::assertSame([ self::assertSame([
"name" => "first", "name" => "first",
"age" => 5, "age" => 5,
"item" => $item, "item" => $item,
], cl::select($values, ["name", "age", "item"])); ], cl::select($row, ["name", "age", "item"]));
self::assertNull($pvalues); self::assertNull($prow);
}); });
$capacitor->charge(["name" => "first", "age" => 10], function($item, ?array $values, ?array $pvalues) { $capacitor->charge(["name" => "first", "age" => 10], function($item, ?array $row, ?array $prow) {
self::assertSame("first", $item["name"]); self::assertSame("first", $item["name"]);
self::assertSame(10, $item["age"]); self::assertSame(10, $item["age"]);
self::assertnotnull($values); self::assertnotnull($row);
self::assertSame(["name", "age", "done", "notes", "item", "item__sum_", "created_", "modified_"], array_keys($values)); self::assertSame(["name", "age", "done", "notes", "item", "item__sum_", "created_", "modified_"], array_keys($row));
self::assertSame([ self::assertSame([
"name" => "first", "name" => "first",
"age" => 10, "age" => 10,
"done" => 0, "done" => 0,
"notes" => null, "notes" => null,
"item" => $item, "item" => $item,
], cl::select($values, ["name", "age", "done", "notes", "item"])); ], cl::select($row, ["name", "age", "done", "notes", "item"]));
self::assertNotNull($pvalues); self::assertNotNull($prow);
self::assertSame([ self::assertSame([
"name" => "first", "name" => "first",
"age" => 5, "age" => 5,
"done" => 0, "done" => 0,
"notes" => null, "notes" => null,
"item" => ["name" => "first", "age" => 5], "item" => ["name" => "first", "age" => 5],
], cl::select($pvalues, ["name", "age", "done", "notes", "item"])); ], cl::select($prow, ["name", "age", "done", "notes", "item"]));
}); });
$capacitor->each(null, function($item, ?array $values) { $capacitor->each(null, function(array $row) {
$item = $row["item"];
self::assertSame("first", $item["name"]); self::assertSame("first", $item["name"]);
self::assertSame(10, $item["age"]); self::assertSame(10, $item["age"]);
self::assertnotnull($values); self::assertnotnull($row);
self::assertSame(["name", "age", "done", "notes", "item", "item__sum_", "created_", "modified_"], array_keys($values)); self::assertSame(["name", "age", "done", "notes", "item", "item__sum_", "created_", "modified_"], array_keys($row));
self::assertSame([ self::assertSame([
"name" => "first", "name" => "first",
"age" => 10, "age" => 10,
"done" => 0, "done" => 0,
"notes" => null, "notes" => null,
"item" => $item, "item" => $item,
], cl::select($values, ["name", "age", "done", "notes", "item"])); ], cl::select($row, ["name", "age", "done", "notes", "item"]));
return [ return [
"done" => 1, "done" => 1,
"notes" => "modified", "notes" => "modified",
]; ];
}); });
$capacitor->charge(["name" => "first", "age" => 10], function($item, ?array $values, ?array $pvalues) { $capacitor->charge(["name" => "first", "age" => 10], function($item, ?array $row, ?array $prow) {
self::assertSame("first", $item["name"]); self::assertSame("first", $item["name"]);
self::assertSame(10, $item["age"]); self::assertSame(10, $item["age"]);
self::assertnotnull($values); self::assertnotnull($row);
self::assertSame(["name", "age", "done", "notes", "item", "item__sum_", "created_", "modified_"], array_keys($values)); self::assertSame(["name", "age", "done", "notes", "item", "item__sum_", "created_", "modified_"], array_keys($row));
self::assertSame([ self::assertSame([
"name" => "first", "name" => "first",
"age" => 10, "age" => 10,
"done" => 1, "done" => 1,
"notes" => "modified", "notes" => "modified",
"item" => $item, "item" => $item,
], cl::select($values, ["name", "age", "done", "notes", "item"])); ], cl::select($row, ["name", "age", "done", "notes", "item"]));
self::assertNotNull($pvalues); self::assertNotNull($prow);
self::assertSame([ self::assertSame([
"name" => "first", "name" => "first",
"age" => 10, "age" => 10,
"done" => 1, "done" => 1,
"notes" => "modified", "notes" => "modified",
"item" => $item, "item" => $item,
], cl::select($pvalues, ["name", "age", "done", "notes", "item"])); ], cl::select($prow, ["name", "age", "done", "notes", "item"]));
}); });
$capacitor->charge(["name" => "first", "age" => 20], function($item, ?array $values, ?array $pvalues) { $capacitor->charge(["name" => "first", "age" => 20], function($item, ?array $row, ?array $prow) {
self::assertSame("first", $item["name"]); self::assertSame("first", $item["name"]);
self::assertSame(20, $item["age"]); self::assertSame(20, $item["age"]);
self::assertnotnull($values); self::assertnotnull($row);
self::assertSame(["name", "age", "done", "notes", "item", "item__sum_", "created_", "modified_"], array_keys($values)); self::assertSame(["name", "age", "done", "notes", "item", "item__sum_", "created_", "modified_"], array_keys($row));
self::assertSame([ self::assertSame([
"name" => "first", "name" => "first",
"age" => 20, "age" => 20,
"done" => 1, "done" => 1,
"notes" => "modified", "notes" => "modified",
"item" => $item, "item" => $item,
], cl::select($values, ["name", "age", "done", "notes", "item"])); ], cl::select($row, ["name", "age", "done", "notes", "item"]));
self::assertNotNull($pvalues); self::assertNotNull($prow);
self::assertSame([ self::assertSame([
"name" => "first", "name" => "first",
"age" => 10, "age" => 10,
"done" => 1, "done" => 1,
"notes" => "modified", "notes" => "modified",
"item" => ["name" => "first", "age" => 10], "item" => ["name" => "first", "age" => 10],
], cl::select($pvalues, ["name", "age", "done", "notes", "item"])); ], cl::select($prow, ["name", "age", "done", "notes", "item"]));
}); });
} }
@ -287,55 +295,55 @@ class SqliteStorageTest extends TestCase {
}); });
$capacitor->reset(); $capacitor->reset();
$nbModified = $capacitor->charge(["name" => "first", "age" => 5], function ($item, ?array $values, ?array $pvalues) { $nbModified = $capacitor->charge(["name" => "first", "age" => 5], function ($item, ?array $row, ?array $prow) {
self::assertSame([ self::assertSame([
"name" => "first", "age" => 5, "name" => "first", "age" => 5,
"item" => $item, "item" => $item,
], cl::select($values, ["name", "age", "item"])); ], cl::select($row, ["name", "age", "item"]));
return ["item" => null]; return ["item" => null];
}); });
self::assertSame(1, $nbModified); self::assertSame(1, $nbModified);
sleep(1); sleep(1);
# nb: on met des sleep() pour que la date de modification soit systématiquement différente # nb: on met des sleep() pour que la date de modification soit systématiquement différente
$nbModified = $capacitor->charge(["name" => "first", "age" => 10], function ($item, ?array $values, ?array $pvalues) { $nbModified = $capacitor->charge(["name" => "first", "age" => 10], function ($item, ?array $row, ?array $prow) {
self::assertSame([ self::assertSame([
"name" => "first", "age" => 10, "name" => "first", "age" => 10,
"item" => $item, "item__sum_" => "9181336dfca20c86313d6065d89aa2ad5070b0fc", "item" => $item, "item__sum_" => "9181336dfca20c86313d6065d89aa2ad5070b0fc",
], cl::select($values, ["name", "age", "item", "item__sum_"])); ], cl::select($row, ["name", "age", "item", "item__sum_"]));
self::assertSame([ self::assertSame([
"name" => "first", "age" => 5, "name" => "first", "age" => 5,
"item" => null, "item__sum_" => null, "item" => null, "item__sum_" => null,
], cl::select($pvalues, ["name", "age", "item", "item__sum_"])); ], cl::select($prow, ["name", "age", "item", "item__sum_"]));
return ["item" => null]; return ["item" => null];
}); });
self::assertSame(1, $nbModified); self::assertSame(1, $nbModified);
sleep(1); sleep(1);
# pas de modification ici # pas de modification ici
$nbModified = $capacitor->charge(["name" => "first", "age" => 10], function ($item, ?array $values, ?array $pvalues) { $nbModified = $capacitor->charge(["name" => "first", "age" => 10], function ($item, ?array $row, ?array $prow) {
self::assertSame([ self::assertSame([
"name" => "first", "age" => 10, "name" => "first", "age" => 10,
"item" => $item, "item__sum_" => "9181336dfca20c86313d6065d89aa2ad5070b0fc", "item" => $item, "item__sum_" => "9181336dfca20c86313d6065d89aa2ad5070b0fc",
], cl::select($values, ["name", "age", "item", "item__sum_"])); ], cl::select($row, ["name", "age", "item", "item__sum_"]));
self::assertSame([ self::assertSame([
"name" => "first", "age" => 10, "name" => "first", "age" => 10,
"item" => null, "item__sum_" => null, "item" => null, "item__sum_" => null,
], cl::select($pvalues, ["name", "age", "item", "item__sum_"])); ], cl::select($prow, ["name", "age", "item", "item__sum_"]));
return ["item" => null]; return ["item" => null];
}); });
self::assertSame(0, $nbModified); self::assertSame(0, $nbModified);
sleep(1); sleep(1);
$nbModified = $capacitor->charge(["name" => "first", "age" => 20], function ($item, ?array $values, ?array $pvalues) { $nbModified = $capacitor->charge(["name" => "first", "age" => 20], function ($item, ?array $row, ?array $prow) {
self::assertSame([ self::assertSame([
"name" => "first", "age" => 20, "name" => "first", "age" => 20,
"item" => $item, "item__sum_" => "001b91982b4e0883b75428c0eb28573a5dc5f7a5", "item" => $item, "item__sum_" => "001b91982b4e0883b75428c0eb28573a5dc5f7a5",
], cl::select($values, ["name", "age", "item", "item__sum_"])); ], cl::select($row, ["name", "age", "item", "item__sum_"]));
self::assertSame([ self::assertSame([
"name" => "first", "age" => 10, "name" => "first", "age" => 10,
"item" => null, "item__sum_" => null, "item" => null, "item__sum_" => null,
], cl::select($pvalues, ["name", "age", "item", "item__sum_"])); ], cl::select($prow, ["name", "age", "item", "item__sum_"]));
return ["item" => null]; return ["item" => null];
}); });
self::assertSame(1, $nbModified); self::assertSame(1, $nbModified);

@ -3,6 +3,7 @@ namespace nulib\db\sqlite;
use Exception; use Exception;
use nulib\tests\TestCase; use nulib\tests\TestCase;
use nulib\ValueException;
class SqliteTest extends TestCase { class SqliteTest extends TestCase {
const CREATE_PERSON = "create table person(nom varchar, prenom varchar, age integer)"; const CREATE_PERSON = "create table person(nom varchar, prenom varchar, age integer)";
@ -12,8 +13,8 @@ class SqliteTest extends TestCase {
function testMigration() { function testMigration() {
$sqlite = new Sqlite(":memory:", [ $sqlite = new Sqlite(":memory:", [
"migration" => [ "migration" => [
self::CREATE_PERSON, "create" => self::CREATE_PERSON,
self::INSERT_JEPHTE, "insert" => self::INSERT_JEPHTE,
], ],
]); ]);
self::assertSame("clain", $sqlite->get("select nom, age from person")); self::assertSame("clain", $sqlite->get("select nom, age from person"));
@ -30,15 +31,15 @@ class SqliteTest extends TestCase {
], $sqlite->get("select nom, age from person where nom = 'payet'", null, true)); ], $sqlite->get("select nom, age from person where nom = 'payet'", null, true));
self::assertSame([ self::assertSame([
["key" => "0", "value" => self::CREATE_PERSON, "done" => 1], ["channel" => "", "name" => "create", "done" => 1],
["key" => "1", "value" => self::INSERT_JEPHTE, "done" => 1], ["channel" => "", "name" => "insert", "done" => 1],
], iterator_to_array($sqlite->all("select key, value, done from _migration"))); ], iterator_to_array($sqlite->all("select channel, name, done from _migration")));
} }
function testException() { function testException() {
$sqlite = new Sqlite(":memory:"); $sqlite = new Sqlite(":memory:");
self::assertException(Exception::class, [$sqlite, "exec"], "prout"); self::assertException(Exception::class, [$sqlite, "exec"], "prout");
self::assertException(SqliteException::class, [$sqlite, "exec"], ["prout"]); self::assertException(ValueException::class, [$sqlite, "exec"], ["prout"]);
} }
protected function assertInserted(Sqlite $sqlite, array $row, array $query): void { protected function assertInserted(Sqlite $sqlite, array $row, array $query): void {
@ -141,6 +142,10 @@ class SqliteTest extends TestCase {
self::assertSame([ self::assertSame([
["count" => 2], ["count" => 2],
], iterator_to_array($sqlite->all(["select count(name) as count from user", "group by" => ["amount"], "having" => ["count(name) = 2"]]))); ], iterator_to_array($sqlite->all([
"select count(name) as count from user",
"group by" => ["amount"],
"having" => ["count(name) = 2"],
])));
} }
} }

@ -0,0 +1,29 @@
<?php
namespace nulib\db\sqlite\impl;
use nulib\cl;
use nulib\db\CapacitorChannel;
class MyIndexChannel extends CapacitorChannel {
const NAME = "my_index";
const TABLE_NAME = "my_index";
const COLUMN_DEFINITIONS = [
"name" => "varchar not null primary key",
"first" => "varchar",
"second" => "varchar",
];
const MIGRATION = [
"index" => [
"create index my_index_first on my_index(first)",
"create index my_index_second on my_index(second)",
],
];
function getItemValues($item): ?array {
return cl::select($item, [
"name" => 0,
"first" => 1,
"second" => 2,
]);
}
}

@ -29,7 +29,7 @@ class DateTest extends TestCase {
function testClone() { function testClone() {
$date = self::dt("now"); $date = self::dt("now");
$clone = Date::clone($date); $clone = $date->clone();
self::assertInstanceOf(DateTime::class, $clone); self::assertInstanceOf(DateTime::class, $clone);
} }

@ -32,13 +32,17 @@ class DateTimeTest extends TestCase {
function testDateTimeZ() { function testDateTimeZ() {
$date = new DateTime("20240405T091523Z"); $date = new DateTime("20240405T091523Z");
self::assertSame("20240405T131523", $date->YmdHMS);
self::assertSame("20240405T131523+04:00", $date->YmdHMSZ);
# comme on spécifie la timezone, la valeur Z est ignorée
$date = new DateTime("20240405T091523Z", new DateTimeZone("Indian/Reunion"));
self::assertSame("20240405T091523", $date->YmdHMS); self::assertSame("20240405T091523", $date->YmdHMS);
self::assertSame("20240405T091523Z", $date->YmdHMSZ); self::assertSame("20240405T091523+04:00", $date->YmdHMSZ);
} }
function testClone() { function testClone() {
$date = self::dt("now"); $date = self::dt("now");
$clone = DateTime::clone($date); $clone = $date->clone();
self::assertInstanceOf(DateTime::class, $clone); self::assertInstanceOf(DateTime::class, $clone);
} }
@ -51,7 +55,7 @@ class DateTimeTest extends TestCase {
self::assertSame("05/04/2024 00:00:00", strval(new DateTime("20240405"))); self::assertSame("05/04/2024 00:00:00", strval(new DateTime("20240405")));
self::assertSame("05/04/2024 00:00:00", strval(new DateTime("240405"))); self::assertSame("05/04/2024 00:00:00", strval(new DateTime("240405")));
self::assertSame("05/04/2024 09:15:23", strval(new DateTime("20240405T091523"))); self::assertSame("05/04/2024 09:15:23", strval(new DateTime("20240405T091523")));
self::assertSame("05/04/2024 09:15:23", strval(new DateTime("20240405T091523Z"))); self::assertSame("05/04/2024 13:15:23", strval(new DateTime("20240405T091523Z")));
self::assertSame("05/04/2024 09:15:23", strval(new DateTime("5/4/2024 9:15:23"))); self::assertSame("05/04/2024 09:15:23", strval(new DateTime("5/4/2024 9:15:23")));
self::assertSame("05/04/2024 09:15:23", strval(new DateTime("5/4/2024 9.15.23"))); self::assertSame("05/04/2024 09:15:23", strval(new DateTime("5/4/2024 9.15.23")));
self::assertSame("05/04/2024 09:15:00", strval(new DateTime("5/4/2024 9:15"))); self::assertSame("05/04/2024 09:15:00", strval(new DateTime("5/4/2024 9:15")));

@ -74,7 +74,7 @@ function build_check_env() {
if template_copy_missing "$PROJDIR/$file"; then if template_copy_missing "$PROJDIR/$file"; then
updated=1 updated=1
setx name=basename -- "$file" setx name=basename -- "$file"
name="${name#.}"; name="${name%.}" name="${name#.}"; name="${name%.*}"
setx file=dirname -- "$file" setx file=dirname -- "$file"
file="$file/$name" file="$file/$name"
updatedfiles+=("$file") updatedfiles+=("$file")
@ -86,7 +86,7 @@ function build_check_env() {
if template_copy_missing "$file"; then if template_copy_missing "$file"; then
updated=1 updated=1
setx name=basename -- "$file" setx name=basename -- "$file"
name="${name#.}"; name="${name%.}" name="${name#.}"; name="${name%.*}"
setx file=dirname -- "$file" setx file=dirname -- "$file"
file="$file/$name" file="$file/$name"
updatedfiles+=("${file#$PROJDIR/}") updatedfiles+=("${file#$PROJDIR/}")

@ -1,12 +1,12 @@
# TODO Faire une copie de ce script dans un répertoire de l'application web # TODO Faire une copie de ce script dans un répertoire de l'application web
# (dans le répertoire _cli/ par défaut) et modifier les paramètres si nécessaire # (dans le répertoire cli_config/ par défaut) et modifier les paramètres si nécessaire
#------------------------------------------------------------------------------- #-------------------------------------------------------------------------------
<?php <?php
require __DIR__ . '/../vendor/autoload.php'; require __DIR__.'/../vendor/autoload.php';
# Lancer une application en ligne de commande # Lancer une application en ligne de commande
const NULIB_APP_app_params = [ const NULIB_APP_app_params = [
"projdir" => __DIR__ . '/..', "projdir" => __DIR__.'/..',
"appcode" => \app\config\bootstrap::APPCODE, "appcode" => \app\config\bootstrap::APPCODE,
]; ];
require __DIR__.'/../vendor/nulib/base/php/src/app/cli/include-launcher.php'; require __DIR__.'/../vendor/nulib/base/php/src/app/cli/include-launcher.php';

@ -6,10 +6,10 @@ require __DIR__.'/../vendor/autoload.php';
# Lancer une application en tâche de fond # Lancer une application en tâche de fond
use nulib\app; use nulib\app;
use nulib\tools\BgLauncherApp; use nulib\cli\BgLauncherApp;
# chemin vers le lanceur PHP # chemin vers le lanceur PHP
const NULIB_APP_app_launcher = __DIR__.'/../_cli/.launcher.php'; const NULIB_APP_app_launcher = __DIR__.'/../@@CLI@@/.launcher.php';
app::init([ app::init([
"projdir" => __DIR__.'/..', "projdir" => __DIR__.'/..',

@ -1,5 +1,5 @@
# TODO Faire une copie de ce script dans un répertoire de l'application web # TODO Faire une copie de ce script dans un répertoire de l'application web
# (dans le répertoire _cli/ par défaut) et modifier les paramétres si nécessaire # (dans le répertoire cli_config/ par défaut) et modifier les paramétres si nécessaire
#------------------------------------------------------------------------------- #-------------------------------------------------------------------------------
#!/bin/bash #!/bin/bash
# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8 # -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8
@ -57,7 +57,7 @@ Vérification des liens..."
for i in *.php*; do for i in *.php*; do
[ -f "$i" ] || continue [ -f "$i" ] || continue
name="bin/${i%.*}.php" name="bin/${i%.*}.php"
dest="../_cli/_wrapper.sh" dest="../@@CLI@@/_wrapper.sh"
link="../bin/${i%.*}.php" link="../bin/${i%.*}.php"
if [ -L "$link" ]; then if [ -L "$link" ]; then
echo "* $name OK" echo "* $name OK"

@ -467,9 +467,11 @@ OPTIONS
function host_docker_run() { function host_docker_run() {
# lancer une commande avec docker # lancer une commande avec docker
if [ "$1" == composer ]; then case "$1" in
: # pas d'analyse d'argument pour composer ci|cu|composer)
else : # pas d'analyse d'argument pour les commandes composer
;;
*)
SOPTS=+w: SOPTS=+w:
LOPTS=help,chdir:,no-use-rslave LOPTS=help,chdir:,no-use-rslave
args="$(getopt -n "$MYNAME" -o "$SOPTS" -l "$LOPTS" -- "$@")" || exit 1; eval "set -- $args" args="$(getopt -n "$MYNAME" -o "$SOPTS" -l "$LOPTS" -- "$@")" || exit 1; eval "set -- $args"
@ -513,7 +515,7 @@ OPTIONS
esac esac
shift shift
done done
fi esac
args=( args=(
run -it --rm run -it --rm
@ -535,26 +537,34 @@ OPTIONS
# monter le répertoire qui contient $PROJDIR # monter le répertoire qui contient $PROJDIR
mount_composer= mount_composer=
mount_runphp=1 mount_standalone=1
mount_mount=1
if [ -z "$PROJDIR" -o "${PROJDIR#$HOME/}" != "$PROJDIR" -o "$PROJDIR" == "$HOME" ]; then if [ -z "$PROJDIR" -o "${PROJDIR#$HOME/}" != "$PROJDIR" -o "$PROJDIR" == "$HOME" ]; then
# bind mount $HOME # bind mount $HOME
args+=(-v "$HOME:$HOME${UseRslave:+:rslave}") args+=(-v "$HOME:$HOME${UseRslave:+:rslave}")
[ -n "$RUNPHP_STANDALONE" ] && if [ -n "$RUNPHP_STANDALONE" -a "${RUNPHP_STANDALONE#$HOME/}" != "$RUNPHP_STANDALONE" ]; then
[ "${RUNPHP_STANDALONE#$HOME/}" != "$RUNPHP_STANDALONE" ] && mount_standalone=
mount_runphp= fi
if [ -n "$RUNPHP_MOUNT" -a "${RUNPHP_MOUNT#$HOME/}" != "$RUNPHP_MOUNT" ]; then
mount_mount=
fi
elif [ -n "$PROJDIR" ]; then elif [ -n "$PROJDIR" ]; then
# bind mount uniquement le répertoire du projet # bind mount uniquement le répertoire du projet
args+=(-v "$PROJDIR:$PROJDIR${UseRslave:+:rslave}") args+=(-v "$PROJDIR:$PROJDIR${UseRslave:+:rslave}")
mount_composer=1 mount_composer=1
[ "$RUNPHP_STANDALONE" == "$PROJDIR" ] && mount_runphp= [ "$RUNPHP_STANDALONE" == "$PROJDIR" ] && mount_standalone=
[ "$RUNPHP_MOUNT" == "$PROJDIR" ] && mount_mount=
fi fi
if [ -n "$mount_composer" -a -d "$HOME/.composer" ]; then if [ -n "$mount_composer" -a -d "$HOME/.composer" ]; then
# monter la configuration de composer # monter la configuration de composer
args+=(-v "$HOME/.composer:$HOME/.composer") args+=(-v "$HOME/.composer:$HOME/.composer")
fi fi
if [ -n "$RUNPHP_STANDALONE" -a -n "$mount_runphp" ]; then if [ -n "$RUNPHP_STANDALONE" -a -n "$mount_standalone" ]; then
args+=(-v "$RUNPHP_STANDALONE:$RUNPHP_STANDALONE") args+=(-v "$RUNPHP_STANDALONE:$RUNPHP_STANDALONE")
fi fi
if [ -n "$RUNPHP_MOUNT" -a -n "$mount_mount" ]; then
args+=(-v "$RUNPHP_MOUNT:$RUNPHP_MOUNT")
fi
args+=(-w "$(pwd)") args+=(-w "$(pwd)")
# lancer avec l'utilisateur courant # lancer avec l'utilisateur courant
@ -589,9 +599,22 @@ function container_exec() {
fi fi
fi fi
if [ "$1" == composer ]; then # pour les commandes suivantes, pas d'analyse d'argument
: # pas d'analyse d'argument pour composer case "$1" in
else ci)
eecho "== installing composer dependencies"
shift
composer i "$@"
;;
cu)
eecho "== upgrading composer dependencies"
shift
composer u "$@"
;;
composer)
"$@"
;;
*)
SOPTS=+w: SOPTS=+w:
LOPTS=chdir: LOPTS=chdir:
args="$(getopt -n "$MYNAME" -o "$SOPTS" -l "$LOPTS" -- "$@")" || exit 1; eval "set -- $args" args="$(getopt -n "$MYNAME" -o "$SOPTS" -l "$LOPTS" -- "$@")" || exit 1; eval "set -- $args"
@ -606,26 +629,14 @@ function container_exec() {
esac esac
shift shift
done done
fi
if [ $# -eq 0 ]; then [ $# -gt 0 ] || die "no command specified"
die "no command specified"
elif [ "$1" == ci ]; then
eecho "== installing composer dependencies"
shift
composer i "$@"
elif [ "$1" == cu ]; then
eecho "== upgrading composer dependencies"
shift
composer u "$@"
elif [ "$1" == composer ]; then
"$@"
else
if [ -n "$chdir" ]; then if [ -n "$chdir" ]; then
cd "$chdir" || exit 1 cd "$chdir" || exit 1
fi fi
exec "$@" exec "$@"
fi ;;
esac
} }
################################################################################ ################################################################################

@ -40,11 +40,31 @@ p == 1 {
ac_clean "$conf0" ac_clean "$conf0"
} }
declare -A PHPWRAPPER_DESTDIRS=(
[_bg_launcher.php]=@@SBIN@@
[.launcher.php]=@@CLI@@
[_wrapper.sh]=@@CLI@@
)
declare -A PHPWRAPPER_MODES=(
[_bg_launcher.php]=+x
[.launcher.php]=
[_wrapper.sh]=+x
)
projdir= projdir=
install_phpwrappers=auto
args=( args=(
"Mettre à jour le script runphp" "Mettre à jour le script runphp"
"[path/to/runphp]" "[path/to/runphp]"
-d:,--projdir:PROJDIR . "Copier les fichiers pour un projet de l'université de la Réunion" -d:,--projdir:PROJDIR . "\
Copier les fichiers pour un projet de l'université de la Réunion:
- BUILDENV0 et BUILDENV sont fixés à ..env.dist et .env
- les fichiers ..env.dist et .runphp.conf sont créés le cas échéant
- le script build est mis à jour
- les wrappers PHP pour la gestion des tâches de fond sont mis à jour le cas
échéant"
-p,--phpwrappers-install install_phpwrappers=1 "forcer l'installation des wrappers PHP"
--np,--no-phpwrappers-install install_phpwrappers= "ne pas installer les wrappers PHP"
) )
parse_args "$@"; set -- "${args[@]}" parse_args "$@"; set -- "${args[@]}"
@ -93,6 +113,7 @@ else
fi fi
# (Re)construire le fichier destination # (Re)construire le fichier destination
estep "$(relpath "$runphp")"
( (
cat "$preamble" cat "$preamble"
echo echo
@ -102,24 +123,81 @@ fi
) >"$runphp" ) >"$runphp"
[ -x "$runphp" ] || chmod +x "$runphp" [ -x "$runphp" ] || chmod +x "$runphp"
eval "$(
vars=(PROJDIR COMPOSERDIR COMPOSERPHAR VENDORDIR BUILDENV0 BUILDENV BUILD_FLAVOUR DIST IMAGENAME)
arrays=(BUILD_IMAGES DISTFILES TEMPLATEFILES VARFILES)
for var in "${vars[@]}"; do eval "$var="; done
for array in "${arrays[@]}"; do eval "$array=()"; done
source "$runphp"
for var in "${vars[@]}"; do echo_setv2 "$var"; done
for array in "${arrays[@]}"; do echo_seta2 "$array"; done
)"
estep "$(relpath "$rundir/Dockerfile.runphp")"
rsync -lpt "$MYDIR/Dockerfile.runphp" "$rundir/" rsync -lpt "$MYDIR/Dockerfile.runphp" "$rundir/"
if [ -n "$projdir" ]; then if [ -n "$projdir" ]; then
if testdiff "$rundir/build" "$MYDIR/build"; then if testdiff "$rundir/build" "$MYDIR/build"; then
estep "$(relpath "$rundir/build")"
cp "$MYDIR/build" "$rundir/build" cp "$MYDIR/build" "$rundir/build"
chmod +x "$rundir/build" chmod +x "$rundir/build"
fi fi
if [ ! -f "$projdir/..env.dist" ]; then if [ ! -f "$projdir/..env.dist" ]; then
estep "$(relpath "$projdir/..env.dist")"
sed <"$MYDIR/dot-build.env.dist" >"$projdir/..env.dist" ' sed <"$MYDIR/dot-build.env.dist" >"$projdir/..env.dist" '
/^IMAGENAME=/s/=.*\//='"$(basename -- "$projdir")"'\// /^IMAGENAME=/s/=.*\//='"$(basename -- "$projdir")"'\//
' '
initial_config=1 initial_config=1
fi fi
if [ ! -f "$projdir/.runphp.conf" ]; then if [ ! -f "$projdir/.runphp.conf" ]; then
estep "$(relpath "$projdir/.runphp.conf")"
sed <"$MYDIR/dot-runphp.conf" >"$projdir/.runphp.conf" ' sed <"$MYDIR/dot-runphp.conf" >"$projdir/.runphp.conf" '
/^RUNPHP=/s/=.*/=sbin\/runphp/ /^RUNPHP=/s/=.*/=sbin\/runphp/
' '
fi fi
sbin_path=sbin
cli_path=cli_config
if [ "$install_phpwrappers" == auto ]; then
if [ ! -f "$PROJDIR/$COMPOSERDIR/composer.json" ]; then
# ce doit être un projet PHP
install_phpwrappers=
elif [ -d "$projdir/cli_config" ]; then
install_phpwrappers=1
sbin_path=sbin
cli_path=cli_config
elif [ -d "$projdir/_cli" ]; then
install_phpwrappers=1
sbin_path=sbin
cli_path=_cli
else
install_phpwrappers=
fi
fi
if [ -n "$install_phpwrappers" ]; then
setx -a phpwrappers=ls_files "$MYDIR" "phpwrapper-*"
for phpwrapper in "${phpwrappers[@]}"; do
destname="${phpwrapper#phpwrapper-}"
destdir="${PHPWRAPPER_DESTDIRS[$destname]}"
[ -n "$destdir" ] || die "$phpwrapper: la destination n'est pas configurée"
mode="${PHPWRAPPER_MODES[$destname]}"
case "$destdir" in
@@SBIN@@) destdir="$PROJDIR/$sbin_path";;
@@CLI@@) destdir="$PROJDIR/$cli_path";;
*) destdir="$PROJDIR/$destdir";;
esac
estep "$(relpath "$destdir/$destname")"
mkdir -p "$destdir"
tail -n+4 "$MYDIR/$phpwrapper" | sed "
s|/@@SBIN@@/|/$sbin_path/|
s|/@@CLI@@/|/$cli_path/|
" >"$destdir/$destname"
[ -n "$mode" ] && chmod "$mode" "$destdir/$destname"
done
fi
fi fi
[ -n "$initial_config" ] [ -n "$initial_config" ]

@ -6,9 +6,22 @@ outil pour gérer les projets PHP
projets dépendants du projet courant projets dépendants du projet courant
* pver: gestion des versions. * pver: gestion des versions.
calculer la prochaine version en respectant semver calculer la prochaine version en respectant semver
* pmer: gérer les branches de features et hotfixes.
* prel: faire une release. ## scripts de gestion de projet
ces outils peuvent agir sur les projets dépendants: faire une release sur un
projet downstream, ou synchroniser la version depuis un projet upstream définir précisément le rôle des scripts
* pdist: créer la branche DIST, basculer dessus, merger MAIN dans DIST
* pmain: initialiser la branche MAIN (si nouveau dépôt), basculer dessus, merger DEVELOP dans MAIN
(s'occupe aussi de la configuration pman.conf)
* pdev: créer la branche DEVELOP, basculer dessus
* pwip: créer une branche WIP, basculer dessus si unique (ou laisser le choix), merger WIP dans DEVELOP
* PEUT-ETRE: pfix: créer une branche HOTFIX, basculer dessus si unique (ou laisser le choix), merger HOTFIX dans MAIN
* prel: faire une release de DEVELOP dans MAIN. à terme, support des branches de hotfix
* il s'agit d'une spécialisation de pmain et/ou pfix pour la gestion des releases
* à terme, gestion en cascade des projets dépendants: release sur un projet downstream, ou synchroniser la version depuis un projet upstream
il faudra supprimer
* pman: fonctionnalités réparties dans les autres scripts spécialisés
* pmer: fonctionnalités réperties dans les autres scripts spécialisés
-*- coding: utf-8 mode: markdown -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8:noeol:binary -*- coding: utf-8 mode: markdown -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8:noeol:binary