Compare commits

...

60 Commits

Author SHA1 Message Date
01c93ec71e maj todo 2025-09-02 08:22:39 +04:00
afaf12497b modifs.mineures sans commentaires 2025-08-30 17:52:22 +04:00
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
75 changed files with 1573 additions and 659 deletions

1
.idea/nulib-base.iml generated

@ -4,6 +4,7 @@
<content url="file://$MODULE_DIR$">
<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$/tests" isTestSource="true" />
<excludeFolder url="file://$MODULE_DIR$/php/vendor" />
</content>
<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"?>
<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">
<option name="transferred" value="true" />
</component>
@ -10,6 +15,11 @@
<option name="highlightLevel" value="WARNING" />
<option name="transferred" value="true" />
</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">
<include_path>
<path value="$PROJECT_DIR$/php/vendor/symfony/polyfill-ctype" />
@ -55,7 +65,7 @@
</component>
<component name="PhpUnit">
<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>
</component>
<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=()
preconfig_scripts=()
configure_variables=(dest)
configure_dest_for=(lib/profile.d/nulib-base)
configure_dest_for=(lib/profile.d/nulib)
config_scripts=(lib/uinst/conf)
install_profiles=true
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
* `51ce95e` maj doc

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

16
TODO.md

@ -1,5 +1,6 @@
# nulib
# nulib/bash
* [nulib/bash](bash/TODO.md)
* runners
* [ ] rnlphp -- lancer un programme php avec la bonne version (+docker le cas échéant)
* [ ] utilisable en shebang
@ -12,4 +13,17 @@
* [ ] rnlsh -- lancer un shell avec les librairies bash / lancer un script
* 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

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

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

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

@ -21,6 +21,13 @@ if [ -z "$NULIB_NO_INIT_ENV" ]; then
fi
[ -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
[ -z "$TMPDIR" -a -d "$HOME/tmp" ] && TMPDIR="$HOME/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
# -*- 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 ..
done
if [ -z "$PROJDIR" ]; then
# s'il n'y a pas de projet, --bs est l'action par défaut
[ $# -gt 0 ] || set -- --bs --ue
elif [ "$MYNAME" == composer ]; then
export RUNPHP_MOUNT=
if [ "$MYNAME" == composer ]; then
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
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
fi

@ -24,6 +24,8 @@
},
"require-dev": {
"nulib/tests": "^7.4",
"ext-mbstring": "*",
"ext-iconv": "*",
"ext-posix": "*",
"ext-pcntl": "*",
"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",
"This file is @generated automatically"
],
"content-hash": "2d630ab5ff0ffe6139447cf93c362ed7",
"content-hash": "71744d15224f445d1aeefe16ec7d1099",
"packages": [
{
"name": "symfony/deprecation-contracts",
@ -301,16 +301,16 @@
},
{
"name": "myclabs/deep-copy",
"version": "1.13.1",
"version": "1.13.3",
"source": {
"type": "git",
"url": "https://github.com/myclabs/DeepCopy.git",
"reference": "1720ddd719e16cf0db4eb1c6eca108031636d46c"
"reference": "faed855a7b5f4d4637717c2b3863e277116beb36"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/1720ddd719e16cf0db4eb1c6eca108031636d46c",
"reference": "1720ddd719e16cf0db4eb1c6eca108031636d46c",
"url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/faed855a7b5f4d4637717c2b3863e277116beb36",
"reference": "faed855a7b5f4d4637717c2b3863e277116beb36",
"shasum": ""
},
"require": {
@ -349,7 +349,7 @@
],
"support": {
"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": [
{
@ -357,20 +357,20 @@
"type": "tidelift"
}
],
"time": "2025-04-29T12:36:36+00:00"
"time": "2025-07-05T12:25:42+00:00"
},
{
"name": "nikic/php-parser",
"version": "v5.4.0",
"version": "v5.5.0",
"source": {
"type": "git",
"url": "https://github.com/nikic/PHP-Parser.git",
"reference": "447a020a1f875a434d62f2a401f53b82a396e494"
"reference": "ae59794362fe85e051a58ad36b289443f57be7a9"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/447a020a1f875a434d62f2a401f53b82a396e494",
"reference": "447a020a1f875a434d62f2a401f53b82a396e494",
"url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/ae59794362fe85e051a58ad36b289443f57be7a9",
"reference": "ae59794362fe85e051a58ad36b289443f57be7a9",
"shasum": ""
},
"require": {
@ -413,9 +413,9 @@
],
"support": {
"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",
@ -2027,6 +2027,8 @@
"php": "^7.4"
},
"platform-dev": {
"ext-mbstring": "*",
"ext-iconv": "*",
"ext-posix": "*",
"ext-pcntl": "*",
"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_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_equals($dest, $value): void { self::filter_if($dest, cv::equals($value)); }
static final function filter_not_equals($dest, $value): void { self::filter_if($dest, cv::not_equals($value)); }
static final function filter_same($dest, $value): void { self::filter_if($dest, cv::same($value)); }
static final function filter_not_same($dest, $value): void { self::filter_if($dest, cv::not_same($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::Fnot_equals($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::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;
use nulib\A;
use nulib\cl;
use nulib\cv;
use nulib\file;
use nulib\str;
class args {
@ -10,7 +13,10 @@ class args {
* - ["myArg" => $value] devient ["--my-arg", "$value"]
* - ["myOpt" => true] devient ["--my-opt"]
* - ["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 {
$args = [];
@ -36,4 +42,205 @@ class 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_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_equals(?array $array, $value): bool { return self::all_if($array, cv::equals($value)); }
static final function all_not_equals(?array $array, $value): bool { return self::all_if($array, cv::not_equals($value)); }
static final function all_same(?array $array, $value): bool { return self::all_if($array, cv::same($value)); }
static final function all_not_same(?array $array, $value): bool { return self::all_if($array, cv::not_same($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::Fnot_equals($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::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_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_equals(?array $array, $value): bool { return self::any_if($array, cv::equals($value)); }
static final function any_not_equals(?array $array, $value): bool { return self::any_if($array, cv::not_equals($value)); }
static final function any_same(?array $array, $value): bool { return self::any_if($array, cv::same($value)); }
static final function any_not_same(?array $array, $value): bool { return self::any_if($array, cv::not_same($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::Fnot_equals($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::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_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_equals(?array $array, $value): ?array { return self::filter_if($array, cv::equals($value)); }
static final function filter_not_equals(?array $array, $value): ?array { return self::filter_if($array, cv::not_equals($value)); }
static final function filter_same(?array $array, $value): ?array { return self::filter_if($array, cv::same($value)); }
static final function filter_not_same(?array $array, $value): ?array { return self::filter_if($array, cv::not_same($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::Fnot_equals($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::Fnot_same($value)); }
#############################################################################

@ -49,7 +49,7 @@ class cv {
* contruire une fonction qui retourne vrai si on lui passe en argument une
* valeur égale à $value
*/
static final function equals($value): callable {
static final function Fequals($value): callable {
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
* 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; };
}
@ -65,7 +65,7 @@ class cv {
* contruire une fonction qui retourne vrai si on lui passe en argument une
* valeur strictement égale à $value
*/
static final function same($value): callable {
static final function Fsame($value): callable {
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
* 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; };
}
@ -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 */
static final function compare($a, $b): int {
if ($a === $b) return 0;

@ -1,6 +1,7 @@
<?php
namespace nulib\db;
use nulib\A;
use nulib\cl;
use nulib\php\func;
use nulib\ValueException;
@ -29,6 +30,11 @@ class Capacitor implements ITransactor {
return $this->getStorage()->db();
}
function ensureLive(): self {
$this->getStorage()->ensureLive();
return $this;
}
/** @var CapacitorChannel */
protected $channel;
@ -138,9 +144,22 @@ class Capacitor implements ITransactor {
$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();
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 {
@ -169,11 +188,27 @@ class Capacitor implements ITransactor {
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([
"update",
"table" => $this->getTableName(),
], $update));
], $query), $params);
}
function close(): void {

@ -25,8 +25,6 @@ class CapacitorChannel implements ITransactor {
const EACH_COMMIT_THRESHOLD = 100;
const USE_CACHE = false;
static function verifix_name(?string &$name, ?string &$tableName=null): void {
if ($name !== null) {
$name = strtolower($name);
@ -60,7 +58,6 @@ class CapacitorChannel implements ITransactor {
$this->tableName = $tableName;
$this->manageTransactions = $manageTransactions ?? static::MANAGE_TRANSACTIONS;
$this->eachCommitThreshold = self::verifix_eachCommitThreshold($eachCommitThreshold);
$this->useCache = static::USE_CACHE;
$this->setup = false;
$this->created = false;
$columnDefinitions = $this->COLUMN_DEFINITIONS();
@ -111,6 +108,8 @@ class CapacitorChannel implements ITransactor {
$def = strval($def);
if (preg_match('/\bprimary\s+key\b/i', $def)) {
$primaryKeys[] = $col;
} elseif ($def === "genserial") {
$primaryKeys[] = $col;
}
}
}
@ -166,23 +165,6 @@ class CapacitorChannel implements ITransactor {
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.
*/
@ -232,8 +214,18 @@ class CapacitorChannel implements ITransactor {
protected ?array $migration;
function getMigration(): ?array {
return $this->migration;
function getMigration(?string $prefix=null): ?array {
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;
@ -255,6 +247,10 @@ class CapacitorChannel implements ITransactor {
* 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.
*
* 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
*/
function getItemValues($item): ?array {
@ -277,8 +273,8 @@ class CapacitorChannel implements ITransactor {
*
* cette méthode doit être utilisée dans {@link self::onUpdate()}
*/
function wasRowModified(array $values, array $pvalues): bool {
return $values["item__sum_"] !== $pvalues["item__sum_"];
function wasRowModified(array $row, array $prow): bool {
return $row["item__sum_"] !== $prow["item__sum_"];
}
final function serialize($item): ?string {
@ -309,17 +305,17 @@ class CapacitorChannel implements ITransactor {
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];
$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;
}
function _wasSumModified(string $key, array $row, array $prow): bool {
function _wasSumModified(string $key, array $raw, array $praw): bool {
$sumCol = $this->getSumCols($key)[1];
$sum = $row[$sumCol] ?? null;
$psum = $prow[$sumCol] ?? null;
$sum = $raw[$sumCol] ?? null;
$psum = $praw[$sumCol] ?? null;
return $sum !== $psum;
}
@ -328,21 +324,21 @@ class CapacitorChannel implements ITransactor {
* créer un nouvel élément
*
* @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()}
* @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.
* 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
* 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
* valeurs calculées par {@link self::getItemValues()}. Bien que cette méthode
* peut techniquement retourner de nouvelles valeurs pour la clé primaire, ça
* risque de créer des doublons
*/
function onCreate($item, array $values, ?array $alwaysNull): ?array {
function onCreate($item, array $row, ?array $alwaysNull): ?array {
return null;
}
@ -351,12 +347,12 @@ class CapacitorChannel implements ITransactor {
* mettre à jour un élément existant
*
* @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()}
* @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
* @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
* Retourner [false] pour annuler le chargement (la ligne n'est pas mise à
* jour)
@ -365,24 +361,25 @@ class CapacitorChannel implements ITransactor {
* - 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
*/
function onUpdate($item, array $values, array $pvalues): ?array {
function onUpdate($item, array $row, array $prow): ?array {
return null;
}
/**
* méthode appelée lors du parcours des éléments avec
* {@link Capacitor::each()}
* méthode appelée lors du parcours des éléments avec {@link Capacitor::each()}
*
* @param mixed $item l'élément courant
* @param ?array $values la ligne courante
* @param ?array $row la ligne courante. l'élément courant est accessible via
* $row["item"]
* @return ?array le cas échéant, un tableau non null utilisé pour mettre à
* 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"
* - 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
*/
function onEach($item, array $values): ?array {
function onEach(array $row): ?array {
return null;
}
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
* {@link Capacitor::delete()}
*
* @param mixed $item l'élément courant
* @param ?array $values la ligne courante
* @param ?array $row la ligne courante. l'élément courant est accessible via
* $row["item"]
* @return bool true s'il faut supprimer la ligne, false sinon
*/
function onDelete($item, array $values): bool {
function onDelete(array $row): bool {
return true;
}
const onDelete = "->".[self::class, "onDelete"][1];
@ -418,6 +415,16 @@ class CapacitorChannel implements ITransactor {
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 {
return $this->capacitor->willUpdate(...$transactors);
}
@ -454,36 +461,75 @@ class CapacitorChannel implements ITransactor {
$this->capacitor->reset($recreate);
}
function charge($item, $func=null, ?array $args=null, ?array &$values=null): int {
return $this->capacitor->charge($item, $func, $args, $values);
function charge($item, $func=null, ?array $args=null, ?array &$row=null): int {
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 {
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 {
$this->verifixFilter($filter);
return $this->capacitor->count($filter);
}
function one($filter, ?array $mergeQuery=null): ?array {
$this->verifixFilter($filter);
return $this->capacitor->one($filter, $mergeQuery);
}
function all($filter, ?array $mergeQuery=null): Traversable {
$this->verifixFilter($filter);
return $this->capacitor->all($filter, $mergeQuery);
}
function each($filter, $func=null, ?array $args=null, ?array $mergeQuery=null, ?int &$nbUpdated=null): int {
$this->verifixFilter($filter);
return $this->capacitor->each($filter, $func, $args, $mergeQuery, $nbUpdated);
}
function delete($filter, $func=null, ?array $args=null): int {
$this->verifixFilter($filter);
return $this->capacitor->delete($filter, $func, $args);
}
function dbUpdate(array $update) {
return $this->capacitor->dbUpdate($update);
function dbAll(array $query, ?array $params=null): iterable {
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 {

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

@ -2,9 +2,23 @@
namespace nulib\db;
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 */
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
* - sinon retourner le nombre de lignes modifiées en cas de succès, ou false

@ -1,7 +1,29 @@
# db/Capacitor
* charge() permet de spécifier la clé associée avec la valeur chargée, et
charge() permet de spécifier la clé associée avec la valeur chargée, et
discharge() retourne les valeurs avec la clé primaire
* chargeAll() (ou peut-être chargeFrom()) permet de charger depuis un iterable
---
chargeAll() (ou peut-être chargeFrom()) permet de charger depuis un iterable
---
rendre obsolète la classe Capacitor: ne garder que CapacitorChannel et
CapacitorStorage
---
constante de classe AUTO_MIGRATE valant par défaut null
false: ne jamais faire de migration: assumer que la table existe avec les bonnes
colonnes
true: toujours chercher à faire la migration
null: calculer la valeur en fonction du profil courant: true pour devel, false
sinon
-*- coding: utf-8 mode: markdown -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8:noeol:binary

@ -9,6 +9,7 @@ class _select extends _common {
const SCHEMA = [
"prefix" => "?string",
"schema" => "?array",
"distinct" => "bool",
"cols" => "?array",
"col_prefix" => "?string",
"from" => "?string",
@ -45,8 +46,16 @@ class _select extends _common {
if (($prefix = $query["prefix"] ?? null) !== null) $sql[] = $prefix;
## select
self::consume('(select(?:\s*distinct)?)\s*', $tmpsql, $ms);
self::consume('(select(?:\s*(distinct))?)\s*', $tmpsql, $ms);
$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
$usercols = [];

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

@ -4,6 +4,13 @@ namespace nulib\db\mysql;
use nulib\db\pdo\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 {
$url = $this->dbconn["name"] ?? null;
if ($url !== null && preg_match('/^mysql(?::|.*;)dbname=([^;]+)/i', $url, $ms)) {

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

@ -12,6 +12,12 @@ use nulib\ValueException;
class Pdo implements IDatabase {
use Tvalues;
const PREFIX = null;
function getPrefix(): ?string {
return static::PREFIX;
}
static function with($pdo, ?array $params=null): self {
if ($pdo instanceof static) {
return $pdo;
@ -34,11 +40,6 @@ class Pdo implements IDatabase {
}
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 = [
\PDO::ATTR_PERSISTENT => true,
];
@ -119,8 +120,8 @@ class Pdo implements IDatabase {
return $query->getSql();
}
function open(): self {
if ($this->db === null) {
function open(bool $reopen=false): self {
if ($this->db === null || $reopen) {
$dbconn = $this->dbconn;
$options = $this->options;
if (is_callable($options)) {
@ -147,6 +148,30 @@ class Pdo implements IDatabase {
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) {
$db = $this->db();
$query = new _pdoQuery($query, $params);

@ -12,6 +12,12 @@ use nulib\ValueException;
class Pgsql implements IDatabase {
use Tvalues;
const PREFIX = "pgsql";
function getPrefix(): ?string {
return self::PREFIX;
}
static function with($pgsql, ?array $params=null): self {
if ($pgsql instanceof static) {
return $pgsql;
@ -132,8 +138,8 @@ class Pgsql implements IDatabase {
return $query->getSql();
}
function open(): self {
if ($this->db === null) {
function open(bool $reopen=false): self {
if ($this->db === null || $reopen) {
$dbconn = $this->dbconn;
$connection_string = [$dbconn[""] ?? null];
unset($dbconn[""]);
@ -187,6 +193,31 @@ class Pgsql implements IDatabase {
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() {
$db = $this->db();
$result = @pg_query($db, "select lastval()");

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

@ -19,6 +19,12 @@ use SQLite3Stmt;
class Sqlite implements IDatabase {
use Tvalues;
const PREFIX = "sqlite";
function getPrefix(): ?string {
return self::PREFIX;
}
static function with($sqlite, ?array $params=null): self {
if ($sqlite instanceof static) {
return $sqlite;
@ -151,8 +157,8 @@ class Sqlite implements IDatabase {
return $query->getSql();
}
function open(): self {
if ($this->db === null) {
function open(bool $reopen=false): self {
if ($this->db === null || $reopen) {
$this->db = new SQLite3($this->file, $this->flags, $this->encryptionKey);
_config::with($this->config)->configure($this);
_sqliteMigration::with($this->migration)->migrate($this);
@ -186,6 +192,31 @@ class Sqlite implements IDatabase {
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) {
$db = $this->db();
$query = new _sqliteQuery($query, $params);

@ -9,6 +9,8 @@ use nulib\db\CapacitorStorage;
* Class SqliteStorage
*/
class SqliteStorage extends CapacitorStorage {
const GENSERIAL_DEFINITION = "integer primary key autoincrement";
function __construct($sqlite) {
$this->db = Sqlite::with($sqlite);
}
@ -19,10 +21,6 @@ class SqliteStorage extends CapacitorStorage {
return $this->db;
}
const PRIMARY_KEY_DEFINITION = [
"id_" => "integer primary key autoincrement",
];
protected function tableExists(string $tableName): bool {
$found = $this->db->get([
# 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 {
$migrations = cl::merge([
"0init" => [$this->_createSql($channel)],
], $channel->getMigration());
], $channel->getMigration($this->db->getPrefix()));
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 {
$sql = parent::_addToChannelsSql($channel);
$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;
}
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 {
$size = $this->ftell();
$this->size = $size = $this->ftell();
if ($size === 0) return false;
$this->rewind();
return true;

@ -118,18 +118,8 @@ abstract class AbstractBuilder extends TempStream implements IBuilder {
if ($unsetRows) $this->rows = null;
}
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));
}
$this->sentHeaders = true;
function getCount(): int {
return $this->index;
}
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 ?int $size = null;
function build(?iterable $rows=null, bool $close=true): bool {
$ok = true;
if (!$this->built) {
@ -154,6 +146,24 @@ abstract class AbstractBuilder extends TempStream implements IBuilder {
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 {
if (!$this->built) {
$this->_build($rows);

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

@ -1,6 +1,8 @@
<?php
namespace nulib\file\tab;
use nulib\file\IWriter;
interface IBuilder extends \nulib\file\IReader {
function writeHeaders(?array $headers=null): void;
@ -8,7 +10,23 @@ interface IBuilder extends \nulib\file\IReader {
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;
/**
* 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;
}

@ -1,4 +1,4 @@
# TOOD
# nulib\output
* 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
@ -32,4 +32,19 @@ pour l'UI
peut-être rajouter `ui` (ou `web`?) en plus de say, log, debuglog?
--> 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()`
* support des logs structurés: log:: ajoute des objets json, un par ligne, dans
le fichier destination. msg::, say:: mettent en forme l'information structurée.
* clés standards: timestamp, user, tech, exception
* string ou list deviennent ["user" => string|list]
ne pas faire cette transformation si le tableau est associatif
* un trait Tlogger permet de spécifier le cas échéant comment mettre en forme
une donnée structurée --> il permet de calculer les valeurs de user_message
et tech_message
-*- 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
* le contenu au lieu de le retourner
*
* cette classe est conçue pour wrapper des objets complexes dont le contenu est
* contruit au fur et à mesure: ainsi, les objets peuvent être utilisés tels
* quels dans un contenu, ou alors être wrappés si on veut simplement les
* afficher
* cette classe est conçue pour wrapper des objets complexes dont le contenu
* peut être multiple et est contruit au fur et à mesure: ainsi, les objets
* peuvent être utilisés tels quels dans un contenu, ou alors être wrappés si
* on veut simplement les afficher
*/
class Printer implements IPrintable {
function __construct(IContent $content) {
@ -24,6 +24,9 @@ class Printer implements IPrintable {
c::write($content);
}
/**
* afficher le contenu retourné par une méthode spécifique de $content
*/
function __call($name, $args) {
$content = func::call([$this->content, $name], ...$args);
c::write($content);

@ -15,6 +15,6 @@ class Date extends DateTime {
}
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 DateTimeZone;
use InvalidArgumentException;
use nulib\str;
/**
* Class DateTime: une date et une heure
@ -29,10 +30,20 @@ class DateTime extends \DateTime {
else return new static($datetime);
}
static function withn($datetime): ?self {
if ($datetime === null) return null;
elseif ($datetime instanceof static) return $datetime;
else return new static($datetime);
}
static function ensure(&$datetime): void {
$datetime = static::withn($datetime);
}
const DMY_PATTERN = '/^(\d+)\/(\d+)(?:\/(\d+))?$/';
const YMD_PATTERN = '/^((?:\d{2})?\d{2})(\d{2})(\d{2})$/';
const DMYHIS_PATTERN = '/^(\d+)\/(\d+)(?:\/(\d+))? +(\d+)[h:.](\d+)(?:[:.](\d+))?$/';
const YMDHISZ_PATTERN = '/^((?:\d{2})?\d{2})(\d{2})(\d{2})[tT](\d{2})(\d{2})(\d{2})?([zZ])?$/';
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) {
@ -128,6 +139,12 @@ class DateTime extends \DateTime {
return false;
}
/** retourner le nombre de secondes depuis minuit */
static function _nbsecs_format(\DateTime $datetime): string {
[$h, $m, $s] = explode(",", $datetime->format("H,i,s"));
return $h * 3600 + $m * 60 + $s;
}
static function _YmdHMSZ_format(\DateTime $datetime): string {
$YmdHMS = $datetime->format("Ymd\\THis");
$Z = $datetime->format("P");
@ -145,6 +162,7 @@ class DateTime extends \DateTime {
"second" => "s",
"wday" => "N",
"wnum" => "W",
"nbsecs" => [self::class, "_nbsecs_format"],
];
const STRING_FORMATS = [
"timezone" => "P",
@ -155,14 +173,6 @@ class DateTime extends \DateTime {
"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
* retourner l'année à 4 chiffres.
@ -206,16 +216,34 @@ class DateTime extends \DateTime {
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";
if ($datetime instanceof \DateTimeInterface) {
if ($timezone === null) $timezone = $datetime->getTimezone();
$timezone ??= $datetime->getTimezone();
parent::__construct();
$this->setTimestamp($datetime->getTimestamp());
$this->setTimezone($timezone);
} elseif (is_int($datetime)) {
parent::__construct("now", $timezone);
$this->setTimestamp($datetime);
} elseif (is_string($datetime)) {
$Y = $H = $Z = null;
if (preg_match(self::DMY_PATTERN, $datetime, $ms)) {
@ -253,22 +281,34 @@ class DateTime extends \DateTime {
if ($Y !== null) {
if ($H === null) $datetime = sprintf("%04d-%02d-%02d", $Y, $m, $d);
else $datetime = sprintf("%04d-%02d-%02d %02d:%02d:%02d", $Y, $m, $d, $H, $M, $S);
if ($Z !== null) $timezone = new DateTimeZone("UTC");
if ($Z !== null) $timezone ??= new DateTimeZone(self::fix_z($Z));
}
parent::__construct($datetime, $timezone);
} 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) {
$datetime = sprintf("%04d-%02d-%02d", $Y, $m, $d);
} else {
$datetime = sprintf("%04d-%02d-%02d %02d:%02d:%02d", $Y, $m, $d, $H ?? 0, $M ?? 0, $S ?? 0);
}
if ($tz === "Z" || $tz === "z") $tz = "UTC";
$timezone = $tz !== null? new DateTimeZone($tz): null;
if ($Z !== null) $timezone ??= new DateTimeZone(self::fix_z($Z));
parent::__construct($datetime, $timezone);
} else {
throw new InvalidArgumentException("datetime must be a string or an instance of DateTimeInterface");
}
if ($forceLocalTimezone) {
$setTimezone ??= new DateTimeZone(date_default_timezone_get());
$this->setTimezone($setTimezone);
}
}
function clone(): self {
return clone $this;
}
function diff($target, $absolute=false): DateInterval {
@ -276,7 +316,13 @@ class DateTime extends \DateTime {
}
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) {
if (array_key_exists($name, self::INT_FORMATS)) {
$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));
} elseif (array_key_exists($name, self::STRING_FORMATS)) {
$format = self::STRING_FORMATS[$name];

@ -40,8 +40,8 @@ class Delay {
"s" => [1, 0],
];
static function compute_dest(int $x, string $u, ?int $y, DateTime $from): array {
$dest = DateTime::clone($from);
static function compute_dest(int $x, string $u, ?int $y, ?DateTimeInterface $from): array {
$dest = DateTime::with($from)->clone();
$yu = null;
switch ($u) {
case "w":
@ -92,7 +92,7 @@ class Delay {
function __construct($delay, ?DateTimeInterface $from=null) {
if ($from === null) $from = new DateTime();
if ($delay === "INF") {
$dest = DateTime::clone($from);
$dest = DateTime::with($from)->clone();
$dest->add(new DateInterval("P9999Y"));
$repr = "INF";
} 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;
}
/** 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
* 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
*
* 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 {
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"]));
}
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\MyChannelV2;
use nulib\db\sqlite\impl\MyChannelV3;
use nulib\db\sqlite\impl\MyIndexChannel;
use nulib\output\msg;
use nulib\output\std\StdMessenger;
use nulib\php\time\DateTime;
@ -69,6 +70,42 @@ alter table my add column date_mod datetime;
-- infos
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;
self::assertSame($expected, $sql);
}

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

@ -3,6 +3,7 @@ namespace nulib\db\sqlite;
use Exception;
use nulib\tests\TestCase;
use nulib\ValueException;
class SqliteTest extends TestCase {
const CREATE_PERSON = "create table person(nom varchar, prenom varchar, age integer)";
@ -12,8 +13,8 @@ class SqliteTest extends TestCase {
function testMigration() {
$sqlite = new Sqlite(":memory:", [
"migration" => [
self::CREATE_PERSON,
self::INSERT_JEPHTE,
"create" => self::CREATE_PERSON,
"insert" => self::INSERT_JEPHTE,
],
]);
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));
self::assertSame([
["key" => "0", "value" => self::CREATE_PERSON, "done" => 1],
["key" => "1", "value" => self::INSERT_JEPHTE, "done" => 1],
], iterator_to_array($sqlite->all("select key, value, done from _migration")));
["channel" => "", "name" => "create", "done" => 1],
["channel" => "", "name" => "insert", "done" => 1],
], iterator_to_array($sqlite->all("select channel, name, done from _migration")));
}
function testException() {
$sqlite = new Sqlite(":memory:");
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 {
@ -141,6 +142,10 @@ class SqliteTest extends TestCase {
self::assertSame([
["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() {
$date = self::dt("now");
$clone = Date::clone($date);
$clone = $date->clone();
self::assertInstanceOf(DateTime::class, $clone);
}

@ -32,13 +32,17 @@ class DateTimeTest extends TestCase {
function testDateTimeZ() {
$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("20240405T091523Z", $date->YmdHMSZ);
self::assertSame("20240405T091523+04:00", $date->YmdHMSZ);
}
function testClone() {
$date = self::dt("now");
$clone = DateTime::clone($date);
$clone = $date->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("240405")));
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:00", strval(new DateTime("5/4/2024 9:15")));

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

@ -1,5 +1,5 @@
# 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
require __DIR__.'/../vendor/autoload.php';

@ -6,10 +6,10 @@ require __DIR__.'/../vendor/autoload.php';
# Lancer une application en tâche de fond
use nulib\app;
use nulib\tools\BgLauncherApp;
use nulib\cli\BgLauncherApp;
# 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([
"projdir" => __DIR__.'/..',

@ -1,5 +1,5 @@
# 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
# -*- 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
[ -f "$i" ] || continue
name="bin/${i%.*}.php"
dest="../_cli/_wrapper.sh"
dest="../@@CLI@@/_wrapper.sh"
link="../bin/${i%.*}.php"
if [ -L "$link" ]; then
echo "* $name OK"

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

@ -40,11 +40,31 @@ p == 1 {
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=
install_phpwrappers=auto
args=(
"Mettre à jour le script 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[@]}"
@ -93,6 +113,7 @@ else
fi
# (Re)construire le fichier destination
estep "$(relpath "$runphp")"
(
cat "$preamble"
echo
@ -102,24 +123,81 @@ fi
) >"$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/"
if [ -n "$projdir" ]; then
if testdiff "$rundir/build" "$MYDIR/build"; then
estep "$(relpath "$rundir/build")"
cp "$MYDIR/build" "$rundir/build"
chmod +x "$rundir/build"
fi
if [ ! -f "$projdir/..env.dist" ]; then
estep "$(relpath "$projdir/..env.dist")"
sed <"$MYDIR/dot-build.env.dist" >"$projdir/..env.dist" '
/^IMAGENAME=/s/=.*\//='"$(basename -- "$projdir")"'\//
'
initial_config=1
fi
if [ ! -f "$projdir/.runphp.conf" ]; then
estep "$(relpath "$projdir/.runphp.conf")"
sed <"$MYDIR/dot-runphp.conf" >"$projdir/.runphp.conf" '
/^RUNPHP=/s/=.*/=sbin\/runphp/
'
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
[ -n "$initial_config" ]

@ -6,9 +6,22 @@ outil pour gérer les projets PHP
projets dépendants du projet courant
* pver: gestion des versions.
calculer la prochaine version en respectant semver
* pmer: gérer les branches de features et hotfixes.
* prel: faire une release.
ces outils peuvent agir sur les projets dépendants: faire une release sur un
projet downstream, ou synchroniser la version depuis un projet upstream
## scripts de gestion de projet
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