Compare commits

...

79 Commits

Author SHA1 Message Date
d63a0cb704 modifs.mineures sans commentaires 2025-04-18 21:33:30 +04:00
91beea9c29 modifs.mineures sans commentaires 2025-04-16 19:22:47 +04:00
0e9be5f221 bug 2025-04-16 17:13:16 +04:00
5e141b575e pman: ajout des clés match_require et match_require-dev 2025-04-16 14:44:04 +04:00
8ee13a85c0 modifs.mineures sans commentaires 2025-04-16 12:26:30 +04:00
2af417a193 modifs.mineures sans commentaires 2025-04-16 12:19:21 +04:00
d4cc8bfa42 config pman composer 2025-04-16 12:19:05 +04:00
ca129dfda4 modifs.mineures sans commentaires 2025-04-15 12:28:02 +04:00
2a50167241 modifs.mineures sans commentaires 2025-04-15 12:20:21 +04:00
146461a184 modifs.mineures sans commentaires 2025-04-15 12:12:03 +04:00
2a46c12e08 modifs.mineures sans commentaires 2025-04-14 18:23:15 +04:00
d241ce6561 ajout PgsqlStorage 2025-04-14 10:42:30 +04:00
86136e75a5 modifs.mineures sans commentaires 2025-04-13 18:09:24 +04:00
64c872cf3f modifs.mineures sans commentaires 2025-04-11 18:20:27 +04:00
ebbd9e06c0 modifs.mineures sans commentaires 2025-04-10 15:27:19 +04:00
5c6d55ed46 maj ordre func 2025-04-10 15:04:37 +04:00
bab9ba81fe début pgsql 2025-04-10 14:33:24 +04:00
ecd01777c1 migration de nur_func à func 2025-04-10 09:42:09 +04:00
bd1f901b70 réorganiser le code de génération sql 2025-04-09 23:18:06 +04:00
1536e091fb améliorations func 2025-04-03 06:22:29 +04:00
a8d55d329a modifs.mineures sans commentaires 2025-03-28 16:17:25 +04:00
60ab13ff84 modifs.mineures sans commentaires 2025-03-28 15:39:05 +04:00
f2614385fe <pman>Init changelog & version 0.4.1p82 2025-03-25 08:48:09 +04:00
5f3ea483da <pman>Intégration de la branche rel74-0.4.1 2025-03-25 08:47:13 +04:00
39bc8fed3b <pman>Init changelog & version 0.4.1p74 2025-03-25 08:47:13 +04:00
5beb5e6621 corriger la prise en compte du proxy 2025-03-23 12:09:35 +04:00
0e9c4e971d renommer fichier de config pman 2025-03-23 06:58:56 +04:00
c3357de203 modifs.mineures sans commentaires 2025-03-20 08:06:31 +04:00
df9bc0d971 modifs.mineures sans commentaires 2025-03-19 06:38:48 +04:00
2e61d7bc21 modifs.mineures sans commentaires 2025-03-18 10:43:29 +04:00
2fd5b9933d support fichiers template 2025-03-17 20:47:30 +04:00
8cbe9c7654 template ignore les fichiers binaires (implémentation gros doigt) 2025-03-17 20:24:34 +04:00
2cdc059810 modifs.mineures sans commentaires 2025-03-17 11:43:34 +04:00
927915093a <pman>Init changelog & version 0.4.0p82 2025-03-14 15:26:15 +04:00
3df9ed2bba <pman>Intégration de la branche rel74-0.4.0 2025-03-14 15:23:41 +04:00
cd16509af1 <pman>Init changelog & version 0.4.0p74 2025-03-14 15:23:41 +04:00
cf9fab5273 maj src/php 2025-03-14 14:55:39 +04:00
5c5d8784a4 pdev: option --force-merge 2025-03-12 17:21:41 +04:00
3def99b378 modifs.mineures sans commentaires 2025-03-07 19:59:52 +04:00
953f614caa modifs.mineures sans commentaires 2025-03-06 12:31:26 +04:00
9723c146d5 ajout cl::same_keys() 2025-03-06 12:31:00 +04:00
5ae3e8b100 pdev: ajout --after-merge 2025-03-04 12:46:44 +04:00
cfc386dff8 Revert "prel/pdev: tracer les hooks"
This reverts commit d44537a9bb.
2025-03-04 12:38:06 +04:00
d44537a9bb prel/pdev: tracer les hooks 2025-03-04 12:25:04 +04:00
e8abaca6ae supprimer tests qui sont encore dans nur/ture 2025-03-04 08:30:46 +04:00
a3116e5dac ajout du schéma 2025-03-04 08:08:16 +04:00
7b12600848 bug 2025-03-03 13:09:04 +04:00
a85cd2faf4 bug 2025-03-03 13:05:39 +04:00
0d2ca2013d prel/pdev: option --fake 2025-03-03 13:01:45 +04:00
bd0da9cffe prel/pdev: ajouter les hook BEFORE_* 2025-03-03 12:54:10 +04:00
7e05caf4d7 runphp: passer les arguments inchangés à composer 2025-03-03 09:25:22 +04:00
9def939cf1 p: support des projets dépendants composer 2025-03-03 08:50:07 +04:00
01c65a6e6a tests verbosity et interaction 2025-03-03 08:11:24 +04:00
8e3569ac4b ne plus utiliser tooenc par défaut. renommer tooenc en uecho 2025-03-03 08:10:24 +04:00
ead5b5adcf support automatique options verbosity et interaction 2025-03-03 08:09:58 +04:00
882549375c pman: tenir compte des branches distantes 2025-03-03 06:51:49 +04:00
e129e0aa7f ajout pwip 2025-03-03 06:42:24 +04:00
9a2378ba74 pman: améliorer l'ergonomie 2025-03-03 06:07:52 +04:00
0cfad79ec0 modifs.mineures sans commentaires 2025-03-02 19:24:05 +04:00
8cfd803ead gestion des profils composer 2025-03-02 19:03:10 +04:00
d9989c6be7 supprimer composer.phar puisque c'est fourni par runphp 2025-03-02 15:48:38 +04:00
7eb5efb421 ajout config .pman.yml 2025-03-02 15:32:07 +04:00
92363cd543 pman: option --force-create 2025-03-02 15:25:40 +04:00
3b379eb799 maj doc 2025-03-01 19:45:00 +04:00
c64b0801e2 bug 2025-03-01 14:03:48 +04:00
745e5420df bug 2025-03-01 14:02:04 +04:00
3d55e81899 cosmetic 2025-03-01 14:00:55 +04:00
0f4636fa77 bug 2025-03-01 13:59:57 +04:00
bbb55599f7 pman: ajout init pman 2025-03-01 13:57:57 +04:00
939f7726ab pman: support des config standard nommées 2025-03-01 13:02:56 +04:00
192a7f52c3 <pman>Init changelog & version 0.3.4p82 2025-03-01 06:25:04 +04:00
f191506035 <pman>Intégration de la branche rel74-0.3.4 2025-03-01 06:23:16 +04:00
a964a23a9a <pman>Init changelog & version 0.3.4p74 2025-03-01 06:23:16 +04:00
62b9230bff pdev: ne pas inscrire delete si cette opération est interdite 2025-03-01 06:22:25 +04:00
608ac724ee <pman>Intégration de la branche rel74-0.3.3 2025-03-01 06:14:58 +04:00
0d15cb9192 <pman>Init changelog & version 0.3.3p74 2025-03-01 06:14:58 +04:00
2163ea992e script post release/merge 2025-03-01 06:14:00 +04:00
d3c2299c13 maj deps 2025-02-28 21:16:00 +04:00
1b188f29a1 <pman>Init changelog & version 0.3.2 2025-02-28 20:46:43 +04:00
125 changed files with 3123 additions and 3436 deletions

8
.composer.pman.yml Normal file
View File

@ -0,0 +1,8 @@
# -*- coding: utf-8 mode: yaml -*- vim:sw=2:sts=2:et:ai:si:sta:fenc=utf-8
composer:
profiles: [ dev, dist ]
dev:
link: true
dist:
link: false

2
.gitignore vendored
View File

@ -1,3 +1,5 @@
/test_*
/.composer.lock.runphp
.~lock*#

View File

@ -1,3 +1,51 @@
## Release 0.4.1p82 du 25/03/2025-08:47
## Release 0.4.1p74 du 25/03/2025-08:47
* `5beb5e6` corriger la prise en compte du proxy
* `0e9c4e9` renommer fichier de config pman
* `2fd5b99` support fichiers template
* `8cbe9c7` template ignore les fichiers binaires (implémentation gros doigt)
## Release 0.4.0p82 du 14/03/2025-15:24
## Release 0.4.0p74 du 14/03/2025-15:23
* `cf9fab5` maj src/php
* `5c5d878` pdev: option --force-merge
* `9723c14` ajout cl::same_keys()
* `5ae3e8b` pdev: ajout --after-merge
* `cfc386d` Revert "prel/pdev: tracer les hooks"
* `d44537a` prel/pdev: tracer les hooks
* `e8abaca` supprimer tests qui sont encore dans nur/ture
* `a3116e5` ajout du schéma
* `0d2ca20` prel/pdev: option --fake
* `bd0da9c` prel/pdev: ajouter les hook BEFORE_*
* `7e05caf` runphp: passer les arguments inchangés à composer
* `9def939` p: support des projets dépendants composer
* `01c65a6` tests verbosity et interaction
* `8e3569a` ne plus utiliser tooenc par défaut. renommer tooenc en uecho
* `ead5b5a` support automatique options verbosity et interaction
* `8825493` pman: tenir compte des branches distantes
* `e129e0a` ajout pwip
* `9a2378b` pman: améliorer l'ergonomie
* `8cfd803` gestion des profils composer
* `d9989c6` supprimer composer.phar puisque c'est fourni par runphp
* `7eb5efb` ajout config .pman.yml
* `92363cd` pman: option --force-create
* `3b379eb` maj doc
* `bbb5559` pman: ajout init pman
* `939f772` pman: support des config standard nommées
## Release 0.3.4p82 du 01/03/2025-06:23
## Release 0.3.4p74 du 01/03/2025-06:22
* `62b9230` pdev: ne pas inscrire delete si cette opération est interdite
* `2163ea9` pdev/prel: scripts post merge/release
## Release 0.3.2p82 du 28/02/2025-20:30
## Release 0.3.2p74 du 28/02/2025-20:28
(tech) Migration vers pman

View File

@ -1 +1 @@
0.3.2
0.4.1

View File

@ -6,15 +6,15 @@ function __esection() {
local length="${COLUMNS:-80}"
setx lsep=__complete "$prefix" "$length" -
tooenc "$COULEUR_BLEUE$lsep$COULEUR_NORMALE"
recho "$COULEUR_BLEUE$lsep$COULEUR_NORMALE"
[ -n "$*" ] || return 0
length=$((length - 1))
setx -a lines=echo "$1"
for line in "${lines[@]}"; do
setx line=__complete "$prefix- $line" "$length"
tooenc "$COULEUR_BLEUE$line-$COULEUR_NORMALE"
recho "$COULEUR_BLEUE$line-$COULEUR_NORMALE"
done
tooenc "$COULEUR_BLEUE$lsep$COULEUR_NORMALE"
recho "$COULEUR_BLEUE$lsep$COULEUR_NORMALE"
}
function __etitle() {
local -a lines; local maxlen=0
@ -23,10 +23,10 @@ function __etitle() {
setx -a lines=echo "$1"
for line in "${lines[@]}"; do
[ ${#line} -gt $maxlen ] && maxlen=${#line}
tooenc "${prefix}${COULEUR_BLEUE}T $line$COULEUR_NORMALE"
recho "${prefix}${COULEUR_BLEUE}T $line$COULEUR_NORMALE"
done
maxlen=$((maxlen + 2))
tooenc "${prefix}${COULEUR_BLEUE}T$(__complete "" $maxlen -)${COULEUR_NORMALE}"
recho "${prefix}${COULEUR_BLEUE}T$(__complete "" $maxlen -)${COULEUR_NORMALE}"
}
function __edesc() {
local -a lines
@ -34,7 +34,7 @@ function __edesc() {
setx -a lines=echo "$1"
for line in "${lines[@]}"; do
tooenc "${prefix}${COULEUR_BLEUE}>${COULEUR_NORMALE} $line"
recho "${prefix}${COULEUR_BLEUE}>${COULEUR_NORMALE} $line"
done
}
function __ebanner() {
@ -43,35 +43,35 @@ function __ebanner() {
local length="${COLUMNS:-80}"
setx lsep=__complete "$prefix" "$length" =
tooenc "$COULEUR_ROUGE$lsep"
recho "$COULEUR_ROUGE$lsep"
length=$((length - 1))
setx -a lines=echo "$1"
for line in "" "${lines[@]}" ""; do
setx line=__complete "$prefix= $line" "$length"
tooenc "$line="
recho "$line="
done
tooenc "$lsep$COULEUR_NORMALE"
recho "$lsep$COULEUR_NORMALE"
}
function __eimportant() { tooenc "$(__edate)$(__eindent0)${COULEUR_ROUGE}!${COULEUR_NORMALE} $(__eindent "$1" " ")"; }
function __eattention() { tooenc "$(__edate)$(__eindent0)${COULEUR_JAUNE}*${COULEUR_NORMALE} $(__eindent "$1" " ")"; }
function __eerror() { tooenc "$(__edate)$(__eindent0)${COULEUR_ROUGE}E${COULEUR_NORMALE} $(__eindent "$1" " ")"; }
function __ewarn() { tooenc "$(__edate)$(__eindent0)${COULEUR_JAUNE}W${COULEUR_NORMALE} $(__eindent "$1" " ")"; }
function __enote() { tooenc "$(__edate)$(__eindent0)${COULEUR_VERTE}N${COULEUR_NORMALE} $(__eindent "$1" " ")"; }
function __einfo() { tooenc "$(__edate)$(__eindent0)${COULEUR_BLEUE}I${COULEUR_NORMALE} $(__eindent "$1" " ")"; }
function __edebug() { tooenc "$(__edate)$(__eindent0)${COULEUR_BLANCHE}D${COULEUR_NORMALE} $(__eindent "$1" " ")"; }
function __eimportant() { recho "$(__edate)$(__eindent0)${COULEUR_ROUGE}!${COULEUR_NORMALE} $(__eindent "$1" " ")"; }
function __eattention() { recho "$(__edate)$(__eindent0)${COULEUR_JAUNE}*${COULEUR_NORMALE} $(__eindent "$1" " ")"; }
function __eerror() { recho "$(__edate)$(__eindent0)${COULEUR_ROUGE}E${COULEUR_NORMALE} $(__eindent "$1" " ")"; }
function __ewarn() { recho "$(__edate)$(__eindent0)${COULEUR_JAUNE}W${COULEUR_NORMALE} $(__eindent "$1" " ")"; }
function __enote() { recho "$(__edate)$(__eindent0)${COULEUR_VERTE}N${COULEUR_NORMALE} $(__eindent "$1" " ")"; }
function __einfo() { recho "$(__edate)$(__eindent0)${COULEUR_BLEUE}I${COULEUR_NORMALE} $(__eindent "$1" " ")"; }
function __edebug() { recho "$(__edate)$(__eindent0)${COULEUR_BLANCHE}D${COULEUR_NORMALE} $(__eindent "$1" " ")"; }
function __estep() { tooenc "$(__edate)$(__eindent0)${COULEUR_BLANCHE}.${COULEUR_NORMALE} $(__eindent "$1" " ")"; }
function __estepe() { tooenc "$(__edate)$(__eindent0)${COULEUR_ROUGE}.${COULEUR_NORMALE} $(__eindent "$1" " ")"; }
function __estepw() { tooenc "$(__edate)$(__eindent0)${COULEUR_JAUNE}.${COULEUR_NORMALE} $(__eindent "$1" " ")"; }
function __estepn() { tooenc "$(__edate)$(__eindent0)${COULEUR_VERTE}.${COULEUR_NORMALE} $(__eindent "$1" " ")"; }
function __estepi() { tooenc "$(__edate)$(__eindent0)${COULEUR_BLEUE}.${COULEUR_NORMALE} $(__eindent "$1" " ")"; }
function __estep_() { tooenc_ "$(__edate)$(__eindent0)${COULEUR_BLANCHE}.${COULEUR_NORMALE} $(__eindent "$1" " ")"; }
function __estepe_() { tooenc_ "$(__edate)$(__eindent0)${COULEUR_ROUGE}.${COULEUR_NORMALE} $(__eindent "$1" " ")"; }
function __estepw_() { tooenc_ "$(__edate)$(__eindent0)${COULEUR_JAUNE}.${COULEUR_NORMALE} $(__eindent "$1" " ")"; }
function __estepn_() { tooenc_ "$(__edate)$(__eindent0)${COULEUR_VERTE}.${COULEUR_NORMALE} $(__eindent "$1" " ")"; }
function __estepi_() { tooenc_ "$(__edate)$(__eindent0)${COULEUR_BLEUE}.${COULEUR_NORMALE} $(__eindent "$1" " ")"; }
function __estep() { recho "$(__edate)$(__eindent0)${COULEUR_BLANCHE}.${COULEUR_NORMALE} $(__eindent "$1" " ")"; }
function __estepe() { recho "$(__edate)$(__eindent0)${COULEUR_ROUGE}.${COULEUR_NORMALE} $(__eindent "$1" " ")"; }
function __estepw() { recho "$(__edate)$(__eindent0)${COULEUR_JAUNE}.${COULEUR_NORMALE} $(__eindent "$1" " ")"; }
function __estepn() { recho "$(__edate)$(__eindent0)${COULEUR_VERTE}.${COULEUR_NORMALE} $(__eindent "$1" " ")"; }
function __estepi() { recho "$(__edate)$(__eindent0)${COULEUR_BLEUE}.${COULEUR_NORMALE} $(__eindent "$1" " ")"; }
function __estep_() { recho_ "$(__edate)$(__eindent0)${COULEUR_BLANCHE}.${COULEUR_NORMALE} $(__eindent "$1" " ")"; }
function __estepe_() { recho_ "$(__edate)$(__eindent0)${COULEUR_ROUGE}.${COULEUR_NORMALE} $(__eindent "$1" " ")"; }
function __estepw_() { recho_ "$(__edate)$(__eindent0)${COULEUR_JAUNE}.${COULEUR_NORMALE} $(__eindent "$1" " ")"; }
function __estepn_() { recho_ "$(__edate)$(__eindent0)${COULEUR_VERTE}.${COULEUR_NORMALE} $(__eindent "$1" " ")"; }
function __estepi_() { recho_ "$(__edate)$(__eindent0)${COULEUR_BLEUE}.${COULEUR_NORMALE} $(__eindent "$1" " ")"; }
function __action() { tooenc "$(__edate)$(__eindent0)${COULEUR_BLANCHE}.${COULEUR_NORMALE} $(__eindent "$1" " ")"; }
function __asuccess() { tooenc "$(__edate)$(__eindent0)${COULEUR_VERTE}${COULEUR_NORMALE} $(__eindent "$1" " ")"; }
function __afailure() { tooenc "$(__edate)$(__eindent0)${COULEUR_ROUGE}${COULEUR_NORMALE} $(__eindent "$1" " ")"; }
function __adone() { tooenc "$(__edate)$(__eindent0)$(__eindent "$1")"; }
function __action() { recho "$(__edate)$(__eindent0)${COULEUR_BLANCHE}.${COULEUR_NORMALE} $(__eindent "$1" " ")"; }
function __asuccess() { recho "$(__edate)$(__eindent0)${COULEUR_VERTE}${COULEUR_NORMALE} $(__eindent "$1" " ")"; }
function __afailure() { recho "$(__edate)$(__eindent0)${COULEUR_ROUGE}${COULEUR_NORMALE} $(__eindent "$1" " ")"; }
function __adone() { recho "$(__edate)$(__eindent0)$(__eindent "$1")"; }

View File

@ -6,23 +6,23 @@ function __esection() {
local length="${COLUMNS:-80}"
setx lsep=__complete "$prefix" "$length" -
tooenc "$lsep"
recho "$lsep"
[ -n "$*" ] || return 0
length=$((length - 1))
setx -a lines=echo "$1"
for line in "${lines[@]}"; do
setx line=__complete "$prefix- $line" "$length"
tooenc "$line-"
recho "$line-"
done
tooenc "$lsep"
recho "$lsep"
}
function __etitle() {
local p="TITLE: " i=" "
tooenc "$(__edate)$(__eindent0)${p}$(__eindent "$1" "$i")"
recho "$(__edate)$(__eindent0)${p}$(__eindent "$1" "$i")"
}
function __edesc() {
local p="DESC: " i=" "
tooenc "$(__edate)$(__eindent0)${p}$(__eindent "$1" "$i")"
recho "$(__edate)$(__eindent0)${p}$(__eindent "$1" "$i")"
}
function __ebanner() {
local -a lines
@ -30,37 +30,37 @@ function __ebanner() {
local length="${COLUMNS:-80}"
setx lsep=__complete "$prefix" "$length" =
tooenc "$lsep"
recho "$lsep"
length=$((length - 1))
setx -a lines=echo "$1"
for line in "" "${lines[@]}" ""; do
setx line=__complete "$prefix= $line" "$length"
tooenc "$line="
recho "$line="
done
tooenc "$lsep"
recho "$lsep"
}
function __eimportant() { tooenc "$(__edate)$(__eindent0)IMPORTANT! $(__eindent "$1" " ")"; }
function __eattention() { tooenc "$(__edate)$(__eindent0)ATTENTION! $(__eindent "$1" " ")"; }
function __eerror() { tooenc "$(__edate)$(__eindent0)ERROR: $(__eindent "$1" " ")"; }
function __ewarn() { tooenc "$(__edate)$(__eindent0)WARNING: $(__eindent "$1" " ")"; }
function __enote() { tooenc "$(__edate)$(__eindent0)NOTE: $(__eindent "$1" " ")"; }
function __einfo() { tooenc "$(__edate)$(__eindent0)INFO: $(__eindent "$1" " ")"; }
function __edebug() { tooenc "$(__edate)$(__eindent0)DEBUG: $(__eindent "$1" " ")"; }
function __eecho() { tooenc "$(__edate)$(__eindent0)$(__eindent "$1")"; }
function __eecho_() { tooenc_ "$(__edate)$(__eindent0)$(__eindent "$1")"; }
function __eimportant() { recho "$(__edate)$(__eindent0)IMPORTANT! $(__eindent "$1" " ")"; }
function __eattention() { recho "$(__edate)$(__eindent0)ATTENTION! $(__eindent "$1" " ")"; }
function __eerror() { recho "$(__edate)$(__eindent0)ERROR: $(__eindent "$1" " ")"; }
function __ewarn() { recho "$(__edate)$(__eindent0)WARNING: $(__eindent "$1" " ")"; }
function __enote() { recho "$(__edate)$(__eindent0)NOTE: $(__eindent "$1" " ")"; }
function __einfo() { recho "$(__edate)$(__eindent0)INFO: $(__eindent "$1" " ")"; }
function __edebug() { recho "$(__edate)$(__eindent0)DEBUG: $(__eindent "$1" " ")"; }
function __eecho() { recho "$(__edate)$(__eindent0)$(__eindent "$1")"; }
function __eecho_() { recho_ "$(__edate)$(__eindent0)$(__eindent "$1")"; }
function __estep() { tooenc "$(__edate)$(__eindent0). $(__eindent "$1" " ")"; }
function __estepe() { tooenc "$(__edate)$(__eindent0).E $(__eindent "$1" " ")"; }
function __estepw() { tooenc "$(__edate)$(__eindent0).W $(__eindent "$1" " ")"; }
function __estepn() { tooenc "$(__edate)$(__eindent0).N $(__eindent "$1" " ")"; }
function __estepi() { tooenc "$(__edate)$(__eindent0).I $(__eindent "$1" " ")"; }
function __estep_() { tooenc_ "$(__edate)$(__eindent0). $(__eindent "$1" " ")"; }
function __estepe_() { tooenc_ "$(__edate)$(__eindent0).E $(__eindent "$1" " ")"; }
function __estepw_() { tooenc_ "$(__edate)$(__eindent0).W $(__eindent "$1" " ")"; }
function __estepn_() { tooenc_ "$(__edate)$(__eindent0).N $(__eindent "$1" " ")"; }
function __estepi_() { tooenc_ "$(__edate)$(__eindent0).I $(__eindent "$1" " ")"; }
function __estep() { recho "$(__edate)$(__eindent0). $(__eindent "$1" " ")"; }
function __estepe() { recho "$(__edate)$(__eindent0).E $(__eindent "$1" " ")"; }
function __estepw() { recho "$(__edate)$(__eindent0).W $(__eindent "$1" " ")"; }
function __estepn() { recho "$(__edate)$(__eindent0).N $(__eindent "$1" " ")"; }
function __estepi() { recho "$(__edate)$(__eindent0).I $(__eindent "$1" " ")"; }
function __estep_() { recho_ "$(__edate)$(__eindent0). $(__eindent "$1" " ")"; }
function __estepe_() { recho_ "$(__edate)$(__eindent0).E $(__eindent "$1" " ")"; }
function __estepw_() { recho_ "$(__edate)$(__eindent0).W $(__eindent "$1" " ")"; }
function __estepn_() { recho_ "$(__edate)$(__eindent0).N $(__eindent "$1" " ")"; }
function __estepi_() { recho_ "$(__edate)$(__eindent0).I $(__eindent "$1" " ")"; }
function __action() { tooenc "$(__edate)$(__eindent0)ACTION: $(__eindent "$1" " ")"; }
function __asuccess() { tooenc "$(__edate)$(__eindent0)(OK) $(__eindent "$1" " ")"; }
function __afailure() { tooenc "$(__edate)$(__eindent0)(KO) $(__eindent "$1" " ")"; }
function __adone() { tooenc "$(__edate)$(__eindent0)$(__eindent "$1")"; }
function __action() { recho "$(__edate)$(__eindent0)ACTION: $(__eindent "$1" " ")"; }
function __asuccess() { recho "$(__edate)$(__eindent0)(OK) $(__eindent "$1" " ")"; }
function __afailure() { recho "$(__edate)$(__eindent0)(KO) $(__eindent "$1" " ")"; }
function __adone() { recho "$(__edate)$(__eindent0)$(__eindent "$1")"; }

View File

@ -124,8 +124,8 @@ optdesc
commence par ++, c'est une option avancée qui n'est pas affichée par défaut."
function parse_args() {
eval "$NULIB__DISABLE_SET_X"
[ -n "$NULIB_ARGS_ONERROR_RETURN" ] && set_die_return
local __r=
local __DIE='[ -n "$NULIB_ARGS_ONERROR_RETURN" ] && return 1 || die'
if ! is_array args; then
eerror "Invalid args definition: args must be defined"
@ -145,17 +145,33 @@ function parse_args() {
__r=1
else
__DEFS=("$@")
__parse_args || __r=1
__nulib_args_parse_args || __r=1
fi
eval "$NULIB__ENABLE_SET_X"
if [ -n "$__r" ]; then
eval "$__DIE"
die || return
fi
}
function __parse_args() {
function __nulib_args_add_sopt() {
local noauto="$1"; shift
local def
for def in "$@"; do
array_contains "$noauto" "$def" || __sopts="$__sopts$def"
done
}
function __nulib_args_add_lopt() {
local noauto="$1"; shift
local def
for def in "$@"; do
array_contains "$noauto" "$def" || __lopts="$__lopts${__lopts:+,}$def"
done
}
function __nulib_args_parse_args() {
## tout d'abord, construire la liste des options
local __AUTOH=1 __AUTOHELP=1 # faut-il gérer automatiquement l'affichage de l'aide?
local __AUTOD=1 __AUTODEBUG=1 # faut-il rajouter les options -D et --debug
local -a __NOAUTOL __NOAUTOLOGTO # faut-il rajouter les options pour gérer la journalisation?
local -a __NOAUTOV __NOAUTOVERBOSITY # options de verbosité qui ont été définies par l'utilisateur
local -a __NOAUTOI __NOAUTOINTERACTION # options d'interaction qui ont été définies par l'utilisateur
local __ADVHELP # y a-t-il des options avancées?
local __popt __sopts __lopts
local -a __defs
@ -165,10 +181,10 @@ function __parse_args() {
+) __popt="$1"; shift; continue;;
-) __popt="$1"; shift; continue;;
-*) IFS=, read -a __defs <<<"$1"; shift;;
*) eerror "Invalid arg definition: expected option, got '$1'"; eval "$__DIE";;
*) die "Invalid arg definition: expected option, got '$1'" || return;;
esac
# est-ce que l'option prend un argument?
local __def __witharg __valdesc
local __def __longdef __witharg __valdesc
__witharg=
for __def in "${__defs[@]}"; do
if [ "${__def%::*}" != "$__def" ]; then
@ -180,23 +196,36 @@ function __parse_args() {
# définitions __sopts et __lopts
for __def in "${__defs[@]}"; do
__def="${__def%%:*}"
__longdef=
if [[ "$__def" == --* ]]; then
# --longopt
__def="${__def#--}"
__lopts="$__lopts${__lopts:+,}$__def$__witharg"
__longdef=1
elif [[ "$__def" == -* ]] && [ ${#__def} -eq 2 ]; then
# -o
__def="${__def#-}"
__sopts="$__sopts$__def$__witharg"
[ "$__def" == h ] && __AUTOH=
[ "$__def" == D ] && __AUTOD=
case "$__def" in
h) __AUTOH=;;
L) __NOAUTOL+=("-$__def");;
Q|q|v|D) __NOAUTOV+=("-$__def");;
b|y|i) __NOAUTOI+=("-$__def");;
esac
else
# -longopt ou longopt
__def="${__def#-}"
__lopts="$__lopts${__lopts:+,}$__def$__witharg"
__longdef=1
fi
if [ -n "$__longdef" ]; then
case "$__def" in
help|help++) __AUTOHELP=;;
log-to) __NOAUTOLOGTO+=("--$__def");;
very-quiet|quiet|verbose|debug) __NOAUTOVERBOSITY+=("--$__def");;
batch|automatic|interactive) __NOAUTOINTERACTION+=("--$__def");;
esac
fi
[ "$__def" == help -o "$__def" == help++ ] && __AUTOHELP=
[ "$__def" == debug ] && __AUTODEBUG=
done
# sauter l'action
shift
@ -209,8 +238,12 @@ function __parse_args() {
[ -n "$__AUTOH" ] && __sopts="${__sopts}h"
[ -n "$__AUTOHELP" ] && __lopts="$__lopts${__lopts:+,}help,help++"
[ -n "$__AUTOD" ] && __sopts="${__sopts}D"
[ -n "$__AUTODEBUG" ] && __lopts="$__lopts${__lopts:+,}debug"
__nulib_args_add_sopt __NOAUTOL L
__nulib_args_add_lopt __NOAUTOLOGTO log-to
__nulib_args_add_sopt __NOAUTOV Q q v D
__nulib_args_add_lopt __NOAUTOVERBOSITY very-quiet quiet verbose debug
__nulib_args_add_sopt __NOAUTOI b y i
__nulib_args_add_lopt __NOAUTOINTERACTION batch automatic interactive
__sopts="$__popt$__sopts"
local -a __getopt_args
@ -222,7 +255,7 @@ function __parse_args() {
else
# relancer pour avoir le message d'erreur
LANG=C getopt "${__getopt_args[@]}" 2>&1 1>/dev/null
eval "$__DIE"
die || return
fi
## puis traiter les options
@ -373,7 +406,7 @@ $prefix$usage"
fi
[[ "$1" == -* ]] || break
option_="$1"; shift
__parse_opt "$option_"
__nulib_args_parse_opt "$option_"
if [ -n "$__witharg" ]; then
# l'option prend un argument
value_="$1"; shift
@ -387,7 +420,7 @@ $prefix$usage"
unset -f inc@ res@ add@ set@ showhelp@
args=("$@")
}
function __parse_opt() {
function __nulib_args_parse_opt() {
# $1 est l'option spécifiée
local option_="$1"
set -- "${__DEFS[@]}"
@ -460,27 +493,62 @@ function __parse_opt() {
[ -n "$__found" ] && return 0
done
if [ -n "$__AUTOH" -a "$option_" == -h ]; then
__action="showhelp@"
case "$option_" in
-h)
if [ -n "$__AUTOH" ]; then
__action='showhelp@'
return 0
fi
;;
--help)
if [ -n "$__AUTOHELP" ]; then
if [ "$option_" == --help ]; then
__action="showhelp@"
return 0
elif [ "$option_" == --help++ ]; then
__action="showhelp@ ++"
__action='showhelp@'
return 0
fi
fi
if [ -n "$__AUTOD" -a "$option_" == -D ]; then
__action=set_debug
;;
--help++)
if [ -n "$__AUTOHELP" ]; then
__action='showhelp@ ++'
return 0
fi
if [ -n "$__AUTODEBUG" -a "$option_" == --debug ]; then
__action=set_debug
;;
-L)
if ! array_contains __NOAUTOL "$option_"; then
__action='elogto $value_'
return 0
fi
;;
--log-to)
if ! array_contains __NOAUTOL "$option_"; then
__action='elogto $value_'
return 0
fi
;;
-Q|-q|-v|-D)
if ! array_contains __NOAUTOV "$option_"; then
__action='set_verbosity $option_'
return 0
fi
;;
--very-quiet|--quiet|--verbose|--debug)
if ! array_contains __NOAUTOVERBOSITY "$option_"; then
__action='set_verbosity $option_'
return 0
fi
;;
-b|-y|-i)
if ! array_contains __NOAUTOI "$option_"; then
__action='set_interaction $option_'
return 0
fi
;;
--batch|--automatic|--interactive)
if ! array_contains __NOAUTOINTERACTION "$option_"; then
__action='set_interaction $option_'
return 0
fi
;;
esac
# ici, l'option n'a pas été trouvée, on ne devrait pas arriver ici
eerror "Unexpected option '$option_'"; eval "$__DIE"
die "Unexpected option '$option_'" || return
}

View File

@ -62,7 +62,7 @@ function ask_yesno() {
else
__eecho_ "Voulez-vous continuer?" 1>&2
fi
tooenc_ " $prompt " 1>&2
echo_ " $prompt " 1>&2
uread r
is_yes "${r:-$default}"
else
@ -198,17 +198,17 @@ function __rv_read() {
__eecho_ "Entrez la valeur" 1>&2
fi
if [ -n "$__rv_readline" ]; then
tooenc_ ": " 1>&2
echo_ ": " 1>&2
uread -e ${__rv_d:+-i"$__rv_d"} "${__rv_opts[@]}" __rv_r
else
if [ -n "$__rv_d" ]; then
if [ -n "$__rv_showdef" ]; then
tooenc_ " [$__rv_d]" 1>&2
echo_ " [$__rv_d]" 1>&2
else
tooenc_ " [****]" 1>&2
echo_ " [****]" 1>&2
fi
fi
tooenc_ ": " 1>&2
echo_ ": " 1>&2
uread "${__rv_opts[@]}" __rv_r
[ -n "$__rv_nl" ] && echo
fi
@ -217,6 +217,7 @@ function __rv_read() {
_setv "$__rv_v" "$__rv_r"
return 0
fi
echo
done
}
@ -268,7 +269,7 @@ function simple_menu() {
else
__eecho_ "Entrez le numéro de l'option choisie" 1>&2
fi
tooenc_ ": " 1>&2
echo_ ": " 1>&2
uread __sm_choice
# Valeur par défaut
@ -291,7 +292,7 @@ function simple_menu() {
let __sm_c=$__sm_c+1
if [ "$__sm_c" -eq 5 ]; then
# sauter une ligne toutes les 4 tentatives
tooenc "" 1>&2
echo 1>&2
__sm_c=0
fi
done
@ -438,7 +439,7 @@ function __void_actions_menu() {
if [ $c -eq 0 ]; then
[ -n "$title" ] && __etitle "$title" 1>&2
__eecho_ "=== Actions disponibles: " 1>&2
tooenc "$action_title" 1>&2
recho "$action_title" 1>&2
fi
if [ -n "$actyc" ]; then
__eecho_ "$actyc" 1>&2
@ -447,7 +448,7 @@ function __void_actions_menu() {
else
__eecho_ "Entrez l'action à effectuer" 1>&2
fi
tooenc_ ": " 1>&2
echo_ ": " 1>&2
uread choice
if [ -z "$choice" -a -n "$default_action" ]; then
select_action="$default_action"
@ -468,7 +469,7 @@ function __void_actions_menu() {
let c=$c+1
if [ $c -eq 5 ]; then
# sauter une ligne toutes les 4 tentatives
tooenc "" 1>&2
echo 1>&2
c=0
fi
done
@ -484,21 +485,21 @@ function __options_actions_menu() {
i=1
for option in "${options[@]}"; do
if [ "$option" == "$select_option" ]; then
tooenc "$i*- $option" 1>&2
echo "$i*- $option" 1>&2
else
tooenc "$i - $option" 1>&2
echo "$i - $option" 1>&2
fi
let i=$i+1
done
__estepn_ "Actions disponibles: " 1>&2
tooenc "$action_title" 1>&2
recho "$action_title" 1>&2
fi
if [ -n "$optyc" ]; then
__eecho_ "$optyc" 1>&2
else
__eecho_ "Entrez l'action et le numéro de l'option choisie" 1>&2
fi
tooenc_ ": " 1>&2
echo_ ": " 1>&2
uread choice
# vérifier la saisie
@ -572,7 +573,7 @@ function __options_actions_menu() {
let c=$c+1
if [ $c -eq 5 ]; then
# sauter une ligne toutes les 4 tentatives
tooenc "" 1>&2
echo "" 1>&2
c=0
fi
done

View File

@ -83,7 +83,7 @@ function err_isatty() {
################################################################################
function tooenc() {
function uecho() {
# $1 étant une chaine encodée en utf-8, l'afficher dans l'encoding de sortie $2
# qui vaut par défaut $NULIB_OUTPUT_ENCODING
local value="$1" to="${2:-$NULIB_OUTPUT_ENCODING}"
@ -93,9 +93,8 @@ function tooenc() {
iconv -f -utf-8 -t "$to" <<<"$value"
fi
}
function uecho() { tooenc "$*"; }
function tooenc_() {
function uecho_() {
# $1 étant une chaine encodée en utf-8, l'afficher sans passer à la ligne dans
# l'encoding de sortie $2 qui vaut par défaut $NULIB_OUTPUT_ENCODING
local value="$1" to="${2:-$NULIB_OUTPUT_ENCODING}"
@ -105,7 +104,6 @@ function tooenc_() {
recho_ "$value" | iconv -f utf-8 -t "$to"
fi
}
function uecho_() { tooenc_ "$*"; }
export NULIB_QUIETLOG
export NULIB__TMPLOG
@ -210,7 +208,7 @@ function __eindent() {
# indenter les lignes de $1, sauf la première
local -a lines; local line first=1
local indent="$(__eindent0)$2"
setx -a lines=echo "$1"
setx -a lines=recho "$1"
for line in "${lines[@]}"; do
if [ -n "$first" ]; then
recho "$line"
@ -232,7 +230,11 @@ function __complete() {
}
PRETTYOPTS=()
function set_verbosity() { :;}
function set_verbosity() {
case "$1" in
-D|--debug) NULIB_DEBUG=1;;
esac
}
function check_verbosity() { return 0; }
function get_verbosity_option() { :;}

View File

@ -2,20 +2,13 @@
## configuration par défaut
# branche upstream
UPSTREAM=
# branches de développement
DEVELOP=develop
FEATURE=wip/
# branche de préparation de release
RELEASE=release-
# branche de release
MAIN=master
TAG_PREFIX=
TAG_SUFFIX=
# branche de hotfix
HOTFIX=hotfix-
# branche de distribution
DIST=
# désactiver les releases automatiques?
NOAUTO=

View File

@ -5,15 +5,23 @@
# les branches sont mergées dans cet ordre:
# upstream --> develop --> [release -->] main --> dist
# feature _/ hotfix _/
# branche upstream
UPSTREAM=
# branches de développement
DEVELOP=develop
FEATURE=wip/
# branche de préparation de release
RELEASE=release-
# branche de release
MAIN=master
TAG_PREFIX=
TAG_SUFFIX=
# branche de hotfix
HOTFIX=hotfix-
# branche de distribution
DIST=
# désactiver les releases automatiques?
NOAUTO=
CONFIG_VARS=(
@ -45,7 +53,7 @@ $0 !~ /<pman>/ { print }
function _filter_changes() {
# enlever les commits "inutiles" pour générer le fichier CHANGES.md
grep -vE '^([+|] )?[0-9a-f]+ modifs\.mineures sans commentaires$' |
grep -vE '^([+|] )?[0-9a-f]+ (cosmetic|typo|bug|fix|maj projet|maj deps)\$'
grep -vE '^([+|] )?[0-9a-f]+ (cosmetic|typo|bug|fix|maj projet|maj deps)$'
}
function _format_md() {
@ -151,6 +159,7 @@ function load_branches() {
local what="${1:-all}"; shift
case "$what" in
all)
[ -n "$Origin" ] || Origin=origin
setx CurrentBranch=git_get_branch
setx -a LocalBranches=git_list_branches
setx -a RemoteBranches=git_list_rbranches "$Origin"
@ -167,7 +176,17 @@ function load_branches() {
"$HOTFIX"*) SrcType=hotfix; DestBranch="$MAIN";;
"$MAIN") SrcType=main; DestBranch="$DIST";;
"$DIST") SrcType=dist; DestBranch=;;
*) DestBranch=;;
*) SrcType=; DestBranch=;;
esac
case "$DestBranch" in
"$UPSTREAM") DestType=upstream;;
"$FEATURE"*) DestType=feature;;
"$DEVELOP") DestType=develop;;
"$RELEASE"*) DestType=release;;
"$HOTFIX"*) DestType=hotfix;;
"$MAIN") DestType=main;;
"$DIST") DestType=dist;;
*) DestType=;;
esac
local branch
@ -177,12 +196,12 @@ function load_branches() {
ReleaseBranch=
HotfixBranch=
MainBranch=
Dist=Branch
for branch in "${AllBranches[@]}"; do
DistBranch=
for branch in "${LocalBranches[@]}"; do
if [ "$branch" == "$UPSTREAM" ]; then
UpstreamBranch="$branch"
elif [[ "$branch" == "$FEATURE"* ]]; then
FeatureBranch+=("$branch")
FeatureBranches+=("$branch")
elif [ "$branch" == "$DEVELOP" ]; then
DevelopBranch="$branch"
elif [[ "$branch" == "$RELEASE"* ]]; then
@ -200,31 +219,50 @@ function load_branches() {
}
function load_config() {
if [ "${ConfigFile#::}" != "$ConfigFile" ]; then
ConfigFile="$NULIBDIR/bash/src/pman${ConfigFile#::}.conf.sh"
fi
if [ -n "$ConfigFile" ]; then
source "$ConfigFile" || die || return
elif [ -n "$ConfigBranch" ]; then
# c'est le seul cas où ConfigFile reste vide
if ! array_contains LocalBranches "$ConfigBranch"; then
die "$ConfigBranch: branche de configuration introuvable" || return
else
ac_set_tmpfile ConfigFile
git show "$ConfigBranch:.pman.conf" >"$ConfigFile" 2>/dev/null
[ -s "$ConfigFile" ] || die "$ConfigBranch: aucune configuration trouvée sur cette branche" || return
source "$ConfigFile"
local config
ac_set_tmpfile config
git show "$ConfigBranch:.pman.conf" >"$config" 2>/dev/null
[ -s "$config" ] || die "$ConfigBranch: aucune configuration trouvée sur cette branche" || return
source "$config"
fi
elif [ -f .pman.conf ]; then
ConfigFile="$(pwd)/.pman.conf"
source "$ConfigFile"
elif [ -n "${MYNAME#prel}" ]; then
elif [ -n "$1" -a -n "${MYNAME#$1}" ]; then
ConfigFile="$NULIBDIR/bash/src/pman${MYNAME#$1}.conf.sh"
source "$ConfigFile"
else
ConfigFile="$NULIBDIR/bash/src/pman.conf.sh"
fi
# S'assurer que nulib est dans le PATH pour que les scripts utilisateurs
# puissent utiliser les outils fournis
export PATH="$NULIBDIR/bin:$PATH"
}
################################################################################
# Divers
function resolve_should_push() {
local quiet="$1"
ShouldPush=1
if ! git_have_remote "$Origin" && [ -n "$Push" ]; then
[ -n "$quiet" ] || enote "L'option --no-push a été forcée puisque ce dépôt n'a pas d'origine"
ShouldPush=
fi
[ -z "$ShouldPush" ] && Push=
}
function _push_branches() {
[ ${#push_branches[*]} -gt 0 ] || return
[ -n "$Origin" ] || Origin=origin
@ -275,6 +313,11 @@ function _mscript_start() {
#!/bin/bash
$(qvals source "$NULIBDIR/load.sh") || exit 1
$(echo_setv SrcBranch="$SrcBranch")
$(echo_setv SrcType="$SrcType")
$(echo_setv DestBranch="$DestBranch")
$(echo_setv DestType="$DestType")
merge=
delete=
push=
@ -330,6 +373,14 @@ function _rscript_start() {
#!/bin/bash
$(qvals source "$NULIBDIR/load.sh") || exit 1
$(echo_setv SrcBranch="$SrcBranch")
$(echo_setv SrcType="$SrcType")
$(echo_setv Version="$Version")
$(echo_setv Tag="$Tag")
$(echo_setv ReleaseBranch="$ReleaseBranch")
$(echo_setv DestBranch="$DestBranch")
$(echo_setv DestType="$DestType")
create=
merge=
push=
@ -382,7 +433,7 @@ EOF
# Enregistrer les changements
_scripta "commit" <<EOF
$(qvals git commit -m "<pman>Init changelog & version $Version")
$(qvals git commit -m "<pman>Init changelog & version $Tag")
EOF
}

View File

@ -108,11 +108,17 @@ function set_interaction() {
# set_interaction en fonction des arguments de la ligne de commande. A utiliser
# de cette manière:
# parse_opts ... "${PRETTYOPTS[@]}" @ args -- ...
PRETTYOPTS=(
# NB: ce n'est pas nécessaire, sauf si on veut afficher ces options dans l'aide
LOGTOOPTS=(
-L:,--log-to:LOGFILE '$elogto $value_' "++enregistrer les messages dans le fichier spécifié"
)
VERBOSITYOPTS=(
-Q,--very-quiet,-q,--quiet,-v,--verbose,-D,--debug '$set_verbosity $option_' "++spécifier le niveau de verbiage"
)
INTERACTIONOPTS=(
-b,--batch,-y,--automatic,-i,--interactive '$set_interaction $option_' "++spécifier le niveau d'interaction"
)
PRETTYOPTS=("${LOGTOOPTS[@]}" "${VERBOSITYOPTS[@]}" "${INTERACTIONOPTS[@]}")
function show_error() { [ "$__verbosity" -ge 1 ]; }
function show_warn() { [ "$__verbosity" -ge 2 ]; }

View File

@ -19,7 +19,8 @@ Copier \$1 vers \$2 de façon inconditionnelle
Si \$2 n'est pas spécifié, on assume que \$1 est de la forme '.file.ext'
et \$2 vaudra alors 'file'
si un fichier \${2#.}.local existe, prendre ce fichier à la place comme source
si un fichier \${2#.}.local existe (e.g 'file.ext.local'), prendre ce fichier à
la place comme source
Ajouter file au tableau userfiles"
function template_copy_replace() {
@ -47,7 +48,8 @@ Copier \$1 vers \$2 si ce fichier n'existe pas déjà
Si \$2 n'est pas spécifié, on assume que \$1 est de la forme '.file.ext'
et \$2 vaudra alors 'file'
si un fichier \${2#.}.local existe, prendre ce fichier à la place comme source
si un fichier \${1#.}.local existe (e.g 'file.ext.local'), prendre ce fichier à
la place comme source
Ajouter file au tableau userfiles"
function template_copy_missing() {
@ -205,6 +207,18 @@ function template_generate_scripts() {
#etitle "sedscript" cat "$sedscript"
}
function: _template_can_process "\
Indiquer si \$1 est un fichier texte, qui peut être traité par
template_process_userfiles"
function _template_can_process() {
case "$1" in
*.png|*.jpg|*.gif|*.bmp) return 1;;
*.zip|*.jar|*.war|*.ear) return 1;;
*.tar|*.gz|*.tgz|*.bz2|*.tbz2) return 1;;
*) return 0;;
esac
}
function template_process_userfiles() {
local awkscript sedscript workfile userfile
ac_set_tmpfile awkscript
@ -213,6 +227,7 @@ function template_process_userfiles() {
ac_set_tmpfile workfile
for userfile in "${userfiles[@]}"; do
_template_can_process "$userfile" || continue
if cat "$userfile" | awk -f "$awkscript" | sed -rf "$sedscript" >"$workfile"; then
if testdiff "$workfile" "$userfile"; then
# n'écrire le fichier que s'il a changé

29
bash/tests/test-interaction.sh Executable file
View File

@ -0,0 +1,29 @@
#!/bin/bash
# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8
source "$(dirname -- "$0")/../src/nulib.sh" || exit 1
#NULIB_NO_DISABLE_SET_X=1
args=(
"tester diverses fonctions de saisie"
)
parse_args "$@"; set -- "${args[@]}"
estep "inter non auto non"
ask_yesno "oui ou non?" && echo oui || echo non
estep "inter oui auto oui"
ask_yesno "oui ou non?" O && echo oui || echo non
estep "inter non auto non"
ask_yesno "oui ou non?" N && echo oui || echo non
estep "inter non auto oui"
ask_yesno "oui ou non?" C && echo oui || echo non
estep "inter oui auto non"
ask_yesno "oui ou non?" X && echo oui || echo non
estep "valeur par défaut vide"
read_value "valeur" empty "" N; echo "valeur=$empty"
estep "valeur par défaut non vide"
read_value "valeur" default default N; echo "valeur=$default"
estep "valeur requise"
read_value "valeur" required; echo "valeur=$required"

View File

@ -7,7 +7,6 @@ Multiline=
Banner=
args=(
"afficher divers messages avec les fonctions e*"
-D,--debug '$set_debug'
-d,--date NULIB_ELOG_DATE=1
-m,--myname NULIB_ELOG_MYNAME=1
-n,--nc,--no-color '$__set_no_colors 1'

24
bash/tests/test-verbosity.sh Executable file
View 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")/../src/nulib.sh" || exit 1
#NULIB_NO_DISABLE_SET_X=1
args=(
"afficher divers messages avec les fonctions e*"
)
parse_args "$@"; set -- "${args[@]}"
eimportant "important (q)"
eattention "attention (q)"
eerror "error (q)"
ewarn "warn (q)"
enote "note (qv)"
einfo "info (qv)"
eecho "echo (qv)"
edebug "debug (D)"
estep "step (qv)"
estepe "stepe (qv)"
estepw "stepw (qv)"
estepn "stepn (qv)"
estepi "stepi (qv)"

View File

@ -1,4 +1,4 @@
#!/bin/bash
# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8
exec "$(dirname -- "$0")/pdev" --tech-merge -Bdev82 dev74 "$@"
exec "$(dirname -- "$0")/pdev" --tech-merge -Bdev82 dev74 -a "git checkout dev74" "$@"

View File

@ -0,0 +1,14 @@
#!/usr/bin/php
<?php
require __DIR__ . "/../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";
}

View File

@ -0,0 +1,22 @@
#!/usr/bin/php
<?php
require __DIR__ . "/../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();
}

34
bin/p
View File

@ -20,8 +20,18 @@ function git_status() {
fi
}
function git_statuses() {
local cwd="$(pwd)" dir
for dir in "$@"; do
cd "$dir" || die
git_status --porcelain
cd "$cwd"
done
}
chdir=
all=
composer=
args=(
"afficher l'état du dépôt"
"[-d chdir] [-a patterns...]
@ -29,6 +39,7 @@ args=(
Si l'option -a est utilisée, ce script accepte comme arguments une liste de patterns permettant de filtrer les répertoires concernés"
-d:,--chdir:BASEDIR chdir= "répertoire dans lequel se placer avant de lancer les opérations"
-a,--all all=1 "faire l'opération sur tous les sous-répertoires de BASEDIR qui sont des dépôts git"
-r,--composer composer=1 "faire l'opération sur tous les projets composer dépendants"
)
parse_args "$@"; set -- "${args[@]}"
@ -50,16 +61,23 @@ if [ -n "$all" ]; then
dirs+=("${dir%/.git}")
done
fi
setx cwd=pwd
for dir in "${dirs[@]}"; do
cd "$dir" || die
git_status --porcelain
cd "$cwd"
done
git_statuses "${dirs[@]}"
elif [ -n "$composer" ]; then
# projets dépendants
git_ensure_gitvcs
setx toplevel=git_get_toplevel
cd "$toplevel" || die
setx cwd=ppath2 . "$OrigCwd"
[ -f composer.json ] || die "$cwd: ce n'est pas un projet composer"
setx -a dirs="$MYDIR/_pman-composer_local_deps.php"
git_statuses "${dirs[@]}"
else
# répertoire courant uniquement
setx toplevel=git_get_toplevel
[ -n "$toplevel" ] && Cwd="$toplevel"
git_ensure_gitvcs
Cwd="$(git_get_toplevel)"
args=()
isatty || args+=(--porcelain)

130
bin/pdev
View File

@ -25,14 +25,18 @@ function ensure_branches() {
}
function merge_action() {
[ -z "$ShouldPush" ] && enote "\
L'option --no-push a été forcée puisque ce dépôt n'a pas d'origine"
enote "\
Ce script va
- fusionner la branche ${COULEUR_BLEUE}$SrcBranch${COULEUR_NORMALE} dans ${COULEUR_ROUGE}$DestBranch${COULEUR_NORMALE}${Push:+
- pousser les branches modifiées}"
ask_yesno "Voulez-vous continuer?" O || die
local script=".git/rel-merge.sh"
local script=".git/pman-merge.sh"
local -a push_branches delete_branches
local hook
local comment=
local or_die=" || exit 1"
@ -42,12 +46,23 @@ Ce script va
# merge
if [ -n "\$merge" ]; then
esection "Fusionner la branche"
EOF
hook="BEFORE_MERGE_${SrcType^^}"; [ -n "${!hook}" ] && _scripta <<EOF
(
${!hook}
)$or_die
EOF
_mscript_merge_branch
hook="AFTER_MERGE_${SrcType^^}"; [ -n "${!hook}" ] && _scripta <<EOF
(
${!hook}
)$or_die
EOF
_scripta <<EOF
fi
EOF
if [ -n "$ShouldDelete" ]; then
_scripta <<EOF
################################################################################
# delete
@ -55,15 +70,26 @@ if [ -n "\$delete" ]; then
esection "Supprimer la branche"
EOF
_mscript_delete_branch
hook="AFTER_DELETE_${SrcType^^}"; [ -n "${!hook}" ] && _scripta <<EOF
(
${!hook}
)$or_die
EOF
_scripta <<EOF
fi
EOF
fi
_scripta <<EOF
################################################################################
# push
if [ -n "\$push" ]; then
esection "Pousser les branches"
EOF
hook="BEFORE_PUSH_${DestType^^}"; [ -n "${!hook}" ] && _scripta <<EOF
(
${!hook}
)$or_die
EOF
_script_push_branches
if [ ${#delete_branches[*]} -gt 0 ]; then
@ -72,27 +98,42 @@ EOF
_script_push_branches
_scripta <<<fi
fi
hook="AFTER_PUSH_${DestType^^}"; [ -n "${!hook}" ] && _scripta <<EOF
(
${!hook}
)$or_die
EOF
_scripta <<EOF
fi
EOF
[ -n "$Delete" -o "$ForbidDelete" ] && Deleted=1 || Deleted=
[ -n "$Push" -o "$ForbidPush" ] && Pushed=1 || Pushed=
if [ -n "$_NoRunScript" ]; then
einfo "Veuillez consulter le script $script pour le détail des opérations à effectuer"
[ -n "$Delete" -o -z "$ShouldDelete" ] && Deleted=1 || Deleted=
[ -n "$ShouldDelete" -a -n "$Delete" ] && ShouldDelete=
[ -n "$ShouldPush" -a -n "$Push" ] && ShouldPush=
if [ -n "$_Fake" ]; then
cat "$script"
elif ! "$script" merge ${Delete:+delete} ${Push:+push}; then
eimportant "Veuillez consulter le script $script pour le détail des opérations qui n'ont pas pu êtres effectuées"
eimportant "\
Le script $script a été lancé avec les arguments 'merge${Delete:+ delete}${Push:+ push}'
En cas d'erreur de merge, veuillez corriger les erreurs puis continuer avec
git merge --continue
Sinon, veuillez consulter le script et/ou le relancer
./$script${Delete:+ delete}${Push:+ push}"
die
elif [ -n "$Deleted" -a -n "$Pushed" ]; then
elif [ -n "$Deleted" -a -n "$Push" ]; then
[ -n "$_KeepScript" ] || rm "$script"
[ -n "$AfterMerge" ] && eval "$AfterMerge"
else
local cmd
[ -n "$Deleted" ] || cmd="$cmd
./$script delete"
[ -n "$Pushed" ] || cmd="$cmd
./$script push"
einfo "Le script $script a été lancé avec les arguments 'merge${Delete:+ delete}${Push:+ push}'
Veuillez le consulter pour le détail des autres opérations à effectuer$cmd"
local msg="\
Le script $script a été lancé avec les arguments 'merge${Delete:+ delete}${Push:+ push}'
Vous pouvez consulter le script et/ou le relancer
./$script${ShouldDelete:+ delete}${ShouldPush:+ push}"
[ -n "$AfterMerge" ] && msg="$msg
Il y a aussi les commandes supplémentaires suivantes:
${AfterMerge//
/
}"
einfo "$msg"
fi
}
@ -104,16 +145,27 @@ chdir=
Origin=
ConfigBranch=
ConfigFile=
_Fake=
_KeepScript=
_NoRunScript=
action=merge
TechMerge=
SquashMsg=
[ -z "$PMAN_NO_PUSH" ] && Push=1 || Push=
[ -z "$PMAN_NO_DELETE" ] && Delete=1 || Delete=
AfterMerge=
args=(
"fusionner la branche source dans la branche destination correspondante"
" [source]"
" [source]
CONFIGURATION
Le fichier .pman.conf contient la configuration des branches. Les variables
supplémentaires suivantes peuvent être définies:
BEFORE_MERGE_<srcType>
AFTER_MERGE_<srcType>
AFTER_DELETE_<srcType>
BEFORE_PUSH_<destType>
AFTER_PUSH_<destType>
srcType et destType pouvant valoir UPSTREAM, DEVELOP, FEATURE, RELEASE, MAIN, HOTFIX, DIST"
-d:,--chdir:BASEDIR chdir= "répertoire dans lequel se placer avant de lancer les opérations"
-O:,--origin Origin= "++\
origine à partir de laquelle les branches distantes sont considérées"
@ -122,8 +174,8 @@ branche à partir de laquelle charger la configuration"
-c:,--config-file:CONFIG ConfigFile= "++\
fichier de configuration des branches. cette option est prioritaire sur --config-branch
par défaut, utiliser le fichier .pman.conf dans le répertoire du dépôt s'il existe"
--fake _Fake=1 "++option non documentée"
--keep-script _KeepScript=1 "++option non documentée"
--no-run-script _NoRunScript=1 "++option non documentée"
-w,--show action=show "\
lister les modifications qui seraient fusionnées dans la branche destination"
-b,--rebase action=rebase "\
@ -146,6 +198,10 @@ ne pas supprimer la branche après la fusion dans la destination"
--delete Delete=1 "++\
supprimer la branche après la fusion dans la destination.
c'est l'option par défaut"
-f,--force-merge ForceMerge=1 "++\
forcer la fusion pour une branche qui devrait être traitée par prel"
-a:,--after-merge AfterMerge= "\
évaluer le script spécifié après une fusion *réussie*"
)
parse_args "$@"; set -- "${args[@]}"
@ -155,13 +211,7 @@ load_branches all
load_config "$MYNAME"
load_branches current "$1"
ForbidPush=
[ -n "$Origin" ] || Origin=origin
if ! git_have_remote "$Origin" && [ -n "$Push" ]; then
ewarn "L'option --no-push a été forcée puisque ce dépôt n'a pas d'origine"
ForbidPush=1
fi
[ -n "$ForbidPush" ] && Push=
resolve_should_push quiet
# puis faire l'action que l'on nous demande
case "$action" in
@ -171,24 +221,26 @@ show)
show_action "$@"
;;
merge)
ForbidDelete=
case "$SrcType" in
develop|release|hotfix)
die "$SrcBranch: cette branche doit être fusionnée dans $DestBranch avec prel"
;;
*)
ShouldDelete=1
no_merge_msg="$SrcBranch: cette branche doit être fusionnée dans $DestBranch avec prel"
if [ "$SrcType" == develop ]; then
[ -z "$ForceMerge" ] && die "$no_merge_msg"
[ -n "$AfterMerge" ] || setx AfterMerge=qvals git checkout -q "$SrcBranch"
elif [ "$SrcType" == release -o "$SrcType" == hotfix ]; then
die "$no_merge_msg"
fi
# n'autoriser la suppression que pour feature
[ "$SrcType" == feature ] || ForbidDelete=1
;;
esac
[ -n "$ForbidDelete" ] && Delete=
git_ensure_cleancheckout
if ! array_contains LocalBranches "$SrcBranch"; then
# si la branche source n'existe pas, la créer
exec "$MYDIR/pman" "$FEATURE${SrcBranch#$FEATURE}"
else
[ "$SrcType" == feature ] || ShouldDelete=
[ -z "$ShouldDelete" ] && Delete=
[ -z "$_Fake" ] && git_ensure_cleancheckout
if array_contains LocalBranches "$SrcBranch"; then
ensure_branches
merge_action "$@"
elif array_contains AllBranches "$SrcBranch"; then
enote "$SrcBranch: une branche du même nom existe dans l'origine"
die "$SrcBranch: branche locale introuvable"
else
die "$SrcBranch: branche introuvable"
fi
;;
*)

189
bin/pman
View File

@ -34,16 +34,12 @@ function show_action() {
# Initialisation
################################################################################
function init_repo_action() {
[ ${#LocalBranches[*]} -eq 0 ] || die "Ce dépôt a déjà été initialisé"
local -a push_branches
if [ ! -f .pman.conf ]; then
function _init_config() {
if [ ! -f .pman.conf -o -n "$ForceCreate" ]; then
ac_set_tmpfile config
cp "$ConfigFile" "$config"
"${EDITOR:-nano}" "$config"
[ -s "$config" ] || exit_with ewarn "Initialisation du dépôt annulée"
[ -s "$config" ] || return 1
cp "$config" .pman.conf
if testdiff .pman.conf "$ConfigFile"; then
@ -59,6 +55,15 @@ function init_repo_action() {
.*.swp"
git add .gitignore
fi
return 0
}
function init_repo_action() {
local -a push_branches; local config
[ ${#LocalBranches[*]} -eq 0 ] || die "Ce dépôt a déjà été initialisé"
_init_config || exit_with ewarn "Initialisation du dépôt annulée"
einfo "Création de la branche $MAIN"
git symbolic-ref HEAD "refs/heads/$MAIN"
@ -72,17 +77,97 @@ function init_repo_action() {
_push_branches
}
function init_develop_action() {
if [ -z "$DevelopBranch" ]; then
[ -n "$DEVELOP" ] || die "La branche DEVELOP n'a pas été définie"
function init_config_action() {
local -a push_branches; config
[ -f .pman.conf -a -z "$ForceCreate" ] && die "La configuration pman a déjà été initialisée"
resolve_should_push
_init_config || exit_with ewarn "Initialisation de la configuration annulée"
git commit -m "configuration pman"
push_branches+=("$CurrentBranch")
_push_branches
}
function _init_composer() {
if [ ! -f .composer.pman.yml -o -n "$ForceCreate" ]; then
ac_set_tmpfile config
cat >"$config" <<EOF
# -*- coding: utf-8 mode: yaml -*- vim:sw=2:sts=2:et:ai:si:sta:fenc=utf-8
composer:
match_prefix:
match_prefix-dev:
profiles: [ dev, dist ]
dev:
link: true
require:
reqire-dev:
dist:
link: false
require:
reqire-dev:
EOF
"${EDITOR:-nano}" "$config"
[ -s "$config" ] || return 1
cp "$config" .composer.pman.yml
git add .composer.pman.yml
fi
return 0
}
function init_composer_action() {
local -a push_branches; local config
[ -f .composer.pman.yml -a -z "$ForceCreate" ] && die "La configuration pman composer a déjà été initialisée"
resolve_should_push
_init_composer || exit_with ewarn "Initialisation de la configuration annulée"
git commit -m "configuration pman composer"
push_branches+=("$CurrentBranch")
_push_branches
}
function _ensure_main_branch() {
[ -n "$MAIN" ] || die "La branche MAIN n'a pas été définie"
[ -n "$MainBranch" ] || die "$MAIN: cette branche n'existe pas (le dépôt a-t-il été initialisé?)"
}
function checkout_main_action() {
if [ -z "$MainBranch" ]; then
array_contains AllBranches "$MAIN" && exit_with enote "\
$MAIN: une branche du même nom existe dans l'origine
git checkout $MAIN"
_ensure_main_branch
fi
git checkout -q "$MAIN"
}
function _ensure_develop_branch() {
[ -n "$DEVELOP" ] || die "La branche DEVELOP n'a pas été définie"
[ "$1" == init -o -n "$DevelopBranch" ] || die "$DEVELOP: cette branche n'existe pas (le dépôt a-t-il été initialisé?)"
}
function init_develop_action() {
local -a push_branches
if [ -z "$DevelopBranch" ]; then
array_contains AllBranches "$DEVELOP" && exit_with enote "\
$DEVELOP: une branche du même nom existe dans l'origine
git checkout $DEVELOP"
_ensure_main_branch
_ensure_develop_branch init
resolve_should_push
enote "Vous allez créer la branche ${COULEUR_VERTE}$DEVELOP${COULEUR_NORMALE} <-- ${COULEUR_BLEUE}$MAIN${COULEUR_NORMALE}"
ask_yesno "Voulez-vous continuer?" O || die
local -a push_branches
einfo "Création de la branche $DEVELOP"
git checkout -b "$DEVELOP" "$MAIN" || die
push_branches+=("$DEVELOP")
@ -92,17 +177,26 @@ function init_develop_action() {
git checkout -q "$DEVELOP"
}
function init_upstream_action() {
if [ -z "$UpstreamBranch" ]; then
function _ensure_upstream_branch() {
[ -n "$UPSTREAM" ] || die "La branche UPSTREAM n'a pas été définie"
[ -n "$DEVELOP" ] || die "La branche DEVELOP n'a pas été définie"
[ -n "$DevelopBranch" ] || die "$DEVELOP: cette branche n'existe pas (le dépôt a-t-il été initialisé?)"
[ "$1" == init -o -n "$UpstreamBranch" ] || die "$UPSTREAM: cette branche n'existe pas (le dépôt a-t-il été initialisé?)"
}
function init_upstream_action() {
local -a push_branches; local config
if [ -z "$UpstreamBranch" ]; then
array_contains AllBranches "$UPSTREAM" && exit_with enote "\
$UPSTREAM: une branche du même nom existe dans l'origine
git checkout $UPSTREAM"
_ensure_develop_branch
_ensure_upstream_branch init
resolve_should_push
enote "Vous allez créer la branche ${COULEUR_VERTE}$UPSTREAM${COULEUR_NORMALE}"
ask_yesno "Voulez-vous continuer?" O || die
local -a push_branches; local config
# faire une copie de la configuration actuelle
ac_set_tmpfile config
cp "$ConfigFile" "$config"
@ -128,17 +222,26 @@ function init_upstream_action() {
git checkout -q "$UPSTREAM"
}
function init_dist_action() {
if [ -z "$DistBranch" ]; then
function _ensure_dist_branch() {
[ -n "$DIST" ] || die "La branche DIST n'a pas été définie"
[ -n "$MAIN" ] || die "La branche MAIN n'a pas été définie"
[ -n "$MainBranch" ] || die "$MAIN: cette branche n'existe pas (le dépôt a-t-il été initialisé?)"
[ "$1" == init -o -n "$DistBranch" ] || die "$DIST: cette branche n'existe pas (le dépôt a-t-il été initialisé?)"
}
function init_dist_action() {
local -a push_branches
if [ -z "$DistBranch" ]; then
array_contains AllBranches "$DIST" && exit_with enote "\
$DIST: une branche du même nom existe dans l'origine
git checkout $DIST"
_ensure_main_branch
_ensure_dist_branch init
resolve_should_push
enote "Vous allez créer la branche ${COULEUR_VERTE}$DIST${COULEUR_NORMALE} <-- ${COULEUR_BLEUE}$MAIN${COULEUR_NORMALE}"
ask_yesno "Voulez-vous continuer?" O || die
local -a push_branches
einfo "Création de la branche $DIST"
git checkout -b "$DIST" "$MAIN" || die
push_branches+=("$DIST")
@ -149,18 +252,24 @@ function init_dist_action() {
}
function init_feature_action() {
local branch="${1#$FEATURE}"
[ -n "$branch" ] || die "Vous devez définir la nom de la branche à créer"
local -a push_branches; local branch
[ -n "$FEATURE" ] || die "La branche FEATURE n'a pas été définie"
branch="${1#$FEATURE}"
[ -n "$branch" ] || die "Vous devez spécifier le nom de la branche"
branch="$FEATURE$branch"
if ! array_contains AllBranches "$branch"; then
[ -n "$DEVELOP" ] || die "La branche DEVELOP n'a pas été définie"
[ -n "$DevelopBranch" ] || die "$DEVELOP: cette branche n'existe pas (le dépôt a-t-il été initialisé?)"
if ! array_contains LocalBranches "$branch"; then
array_contains AllBranches "$branch" && exit_with enote "\
$branch: une branche du même nom existe dans l'origine
git checkout $branch"
_ensure_develop_branch
resolve_should_push
enote "Vous allez créer la branche ${COULEUR_VERTE}$branch${COULEUR_NORMALE} <-- ${COULEUR_BLEUE}$DEVELOP${COULEUR_NORMALE}"
ask_yesno "Voulez-vous continuer?" O || die
local -a push_branches
einfo "Création de la branche $branch"
git checkout -b "$branch" "$DEVELOP" || die
push_branches+=("$branch")
@ -174,7 +283,9 @@ function init_action() {
local what="${1:-develop}"; shift
case "$what" in
init|repo|r) init_repo_action "$@";;
main|m) git checkout -q "$MAIN";;
config) init_config_action "$@";;
composer) init_composer_action "$@";;
main|m) checkout_main_action;;
develop|dev|d) init_develop_action "$@";;
upstream|up|u) init_upstream_action "$@";;
dist|x) init_dist_action "$@";;
@ -191,10 +302,12 @@ ConfigBranch=
ConfigFile=
action=init
Origin=
Push=1
[ -z "$PMAN_NO_PUSH" ] && Push=1 || Push=
ForceCreate=
args=(
"gérer un projet git"
"repo|develop|upstream|dist
"repo|config|composer
develop|upstream|dist
INITIALISATION
@ -203,6 +316,7 @@ configurer certaines branches du dépôt si elles n'existent pas déjà
repo
initialiser un dépôt vide et créer les branches $MAIN et $DEVELOP
develop
créer la branche $DEVELOP
upstream
@ -219,6 +333,8 @@ fichier de configuration des branches. cette option est prioritaire sur --config
par défaut, utiliser le fichier .pman.conf dans le répertoire du dépôt s'il existe"
-w,--show-config action=show "++\
afficher la configuration chargée"
--composer-select-profile action=composer_select_profile "\
sélectionner le profil composer spécifié en argument"
-O:,--origin Origin= "++\
origine vers laquelle pousser les branches"
-n,--no-push Push= "\
@ -226,6 +342,8 @@ ne pas pousser les branches vers leur origine après leur création"
--push Push=1 "++\
pousser les branches vers leur origine après leur création.
c'est l'option par défaut"
-f,--force-create ForceCreate=1 "\
Avec config, forcer la (re)création du fichier .pman.conf"
)
parse_args "$@"; set -- "${args[@]}"
@ -244,6 +362,9 @@ init)
git_ensure_cleancheckout
init_action "$@"
;;
composer_select_profile)
exec "$MYDIR/_pman-$action.php" "$@"
;;
*)
die "$action: action non implémentée"
;;

103
bin/prel
View File

@ -34,12 +34,19 @@ function ensure_branches() {
function create_release_action() {
if [ -n "$ReleaseBranch" ]; then
Version="${ReleaseBranch#$RELEASE}"
Tag="$TAG_PREFIX$Version$TAG_SUFFIX"
merge_release_action "$@"; return $?
elif [ -n "$HotfixBranch" ]; then
Version="${HotfixBranch#$HOTFIX}"
Tag="$TAG_PREFIX$Version$TAG_SUFFIX"
merge_hotfix_action "$@"; return $?
fi
[ -n "$ManualRelease" ] && ewarn "\
L'option --no-merge a été forcée puisque ce dépôt ne supporte pas les releases automatiques"
[ -z "$ShouldPush" ] && enote "\
L'option --no-push a été forcée puisque ce dépôt n'a pas d'origine"
if [ -z "$Version" -a -n "$CurrentVersion" -a -f VERSION.txt ]; then
Version="$(<VERSION.txt)"
Tag="$TAG_PREFIX$Version$TAG_SUFFIX"
@ -67,7 +74,7 @@ Vous devrez:
fi
ask_yesno "Voulez-vous continuer?" O || die
local script=".git/rel-release.sh"
local script=".git/pman-release.sh"
local -a push_branches push_tags
local comment=
local or_die=" || exit 1"
@ -78,8 +85,18 @@ Vous devrez:
# create
if [ -n "\$create" ]; then
esection "Création de la release"
EOF
[ -n "$BEFORE_CREATE_RELEASE" ] && _scripta <<EOF
(
$BEFORE_CREATE_RELEASE
)$or_die
EOF
_rscript_create_release_branch
[ -n "$AFTER_CREATE_RELEASE" ] && _scripta <<EOF
(
$AFTER_CREATE_RELEASE
)$or_die
EOF
_scripta <<EOF
fi
EOF
@ -89,10 +106,20 @@ EOF
# merge
if [ -n "\$merge" ]; then
esection "Fusionner la release"
EOF
[ -n "$BEFORE_MERGE_RELEASE" ] && _scripta <<EOF
(
$BEFORE_MERGE_RELEASE
)$or_die
EOF
_rscript_merge_release_branch "$DestBranch" "$Tag"
_rscript_merge_release_branch "$SrcBranch"
_rscript_delete_release_branch
[ -n "$AFTER_MERGE_RELEASE" ] && _scripta <<EOF
(
$AFTER_MERGE_RELEASE
)$or_die
EOF
_scripta <<EOF
fi
EOF
@ -102,30 +129,42 @@ EOF
# push
if [ -n "\$push" ]; then
esection "Pousser branches et tags"
EOF
[ -n "$BEFORE_PUSH_RELEASE" ] && _scripta <<EOF
(
$BEFORE_PUSH_RELEASE
)$or_die
EOF
_script_push_branches
_script_push_tags
[ -n "$AFTER_PUSH_RELEASE" ] && _scripta <<EOF
(
$AFTER_PUSH_RELEASE
)$or_die
EOF
_scripta <<EOF
fi
EOF
[ -n "$Merge" ] && Merged=1 || Merged=
[ -n "$Push" -o "$ForbidPush" ] && Pushed=1 || Pushed=
if [ -n "$_NoRunScript" ]; then
einfo "Veuillez consulter le script $script pour le détail des opérations à effectuer"
[ -z "$ManualRelease" -a -n "$Merge" ] && ShouldMerge= || ShouldMerge=1
[ -n "$ShouldPush" -a -n "$Push" ] && ShouldPush=
if [ -n "$_Fake" ]; then
cat "$script"
elif ! "$script" create ${Merge:+merge} ${Push:+push}; then
eimportant "Veuillez consulter le script $script pour le détail des opérations qui n'ont pas pu êtres effectuées"
eimportant "\
Le script $script a été lancé avec les arguments 'create${Merge:+ merge}${Push:+ push}'
En cas d'erreur de merge, veuillez corriger les erreurs puis continuer avec
git merge --continue
Veuillez aussi consulter le script et/ou le relancer
./$script${Push:+ push}"
die
elif [ -n "$Merged" -a -n "$Pushed" ]; then
elif [ -n "$Merge" -a -n "$Push" ]; then
[ -n "$_KeepScript" ] || rm "$script"
else
local cmd
[ -n "$Merged" ] || cmd="$cmd
./$script merge"
[ -n "$Pushed" ] || cmd="$cmd
./$script push"
einfo "Le script $script a été lancé avec les arguments 'create${Merge:+ merge}${Push:+ push}'
Veuillez le consulter pour le détail des autres opérations à effectuer$cmd"
einfo "\
Le script $script a été lancé avec les arguments 'create${Merge:+ merge}${Push:+ push}'
Vous pouvez consulter le script et/ou le relancer
./$script${ShouldMerge:+ merge}${ShouldPush:+ push}"
fi
}
@ -152,8 +191,8 @@ chdir=
Origin=
ConfigBranch=
ConfigFile=
_Fake=
_KeepScript=
_NoRunScript=
action=release
[ -z "$PMAN_NO_MERGE" ] && Merge=1 || Merge=
[ -z "$PMAN_NO_PUSH" ] && Push=1 || Push=
@ -162,7 +201,17 @@ CurrentVersion=
ForceCreate=
args=(
"faire une nouvelle release à partir de la branche source"
" -v VERSION [source]"
" -v VERSION [source]
CONFIGURATION
Le fichier .pman.conf contient la configuration des branches. Les variables
supplémentaires suivantes peuvent être définies:
BEFORE_CREATE_RELEASE
AFTER_CREATE_RELEASE
BEFORE_MERGE_RELEASE
AFTER_MERGE_RELEASE
BEFORE_PUSH_RELEASE
AFTER_PUSH_RELEASE"
-d:,--chdir:BASEDIR chdir= "répertoire dans lequel se placer avant de lancer les opérations"
-O:,--origin Origin= "++\
origine à partir de laquelle les branches distantes sont considérées"
@ -171,8 +220,8 @@ branche à partir de laquelle charger la configuration"
-c:,--config-file:CONFIG ConfigFile= "++\
fichier de configuration des branches. cette option est prioritaire sur --config-branch
par défaut, utiliser le fichier .pman.conf dans le répertoire du dépôt s'il existe"
--fake _Fake=1 "++option non documentée"
--keep-script _KeepScript=1 "++option non documentée"
--no-run-script _NoRunScript=1 "++option non documentée"
-w,--show action=show "\
lister les modifications qui seraient intégrées dans la release"
--release action=release "++\
@ -188,11 +237,11 @@ ne pas pousser les branches vers leur origine après la création de la release"
--push Push=1 "++\
pousser les branches vers leur origine après la création de la release.
c'est l'option par défaut"
-v:,--version Version= "\
-v:,--version:VERSION Version= "\
spécifier la version de la release à créer"
-C,--current-version CurrentVersion=1 "++\
si aucune version n'est spécifiée, prendre la version présente dans le fichier VERSION.txt"
-f:,--force-create ForceCreate= "\
-f,--force-create ForceCreate=1 "\
forcer la création de la release même si le tag correspond à la version existe déjà"
)
parse_args "$@"; set -- "${args[@]}"
@ -203,19 +252,11 @@ load_branches all
load_config "$MYNAME"
load_branches current "$1"; shift
if [ -n "$Merge" -a -n "$NOAUTO" ]; then
ewarn "L'option --no-merge a été forcée puisque ce dépôt ne supporte pas les releases automatiques"
Merge=
fi
[ -n "$Merge" -a -n "$NOAUTO" ] && ManualRelease=1 || ManualRelease=
[ -n "$ManualRelease" ] && Merge=
[ -z "$Merge" ] && Push=
ForbidPush=
[ -n "$Origin" ] || Origin=origin
if ! git_have_remote "$Origin" && [ -n "$Push" ]; then
ewarn "L'option --no-push a été forcée puisque ce dépôt n'a pas d'origine"
ForbidPush=1
fi
[ -n "$ForbidPush" ] && Push=
resolve_should_push quiet
# puis faire l'action que l'on nous demande
case "$action" in
@ -225,7 +266,7 @@ show)
show_action "$@"
;;
release)
git_ensure_cleancheckout
[ -z "$_Fake" ] && git_ensure_cleancheckout
ensure_branches
case "$SrcType" in
release) merge_release_action "$@";;

60
bin/pwip Executable file
View File

@ -0,0 +1,60 @@
#!/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
require: git pman pman.conf
git_cleancheckout_DIRTY="\
Vous avez des modifications locales.
Enregistrez ces modifications avant de créer une nouvelle branche"
chdir=
Origin=
ConfigBranch=
ConfigFile=
[ -z "$PMAN_NO_PUSH" ] && Push=1 || Push=
args=(
"créer une branche de feature"
"<feature>"
-d:,--chdir:BASEDIR chdir= "répertoire dans lequel se placer avant de lancer les opérations"
-O:,--origin Origin= "++\
origine à partir de laquelle les branches distantes sont considérées"
-B:,--config-branch ConfigBranch= "++\
branche à partir de laquelle charger la configuration"
-c:,--config-file:CONFIG ConfigFile= "++\
fichier de configuration des branches. cette option est prioritaire sur --config-branch
par défaut, utiliser le fichier .pman.conf dans le répertoire du dépôt s'il existe"
-n,--no-push Push= "\
ne pas pousser les branches vers leur origine après la fusion"
--push Push=1 "++\
pousser les branches vers leur origine après la fusion.
c'est l'option par défaut"
)
parse_args "$@"; set -- "${args[@]}"
# charger la configuration
ensure_gitdir "$chdir"
load_branches all
load_config "$MYNAME"
load_branches current
branch="$1"
if [ -z "$branch" -a ${#FeatureBranches[*]} -eq 1 ]; then
branch="${FeatureBranches[0]}"
fi
[ -n "$branch" ] || die "Vous devez spécifier la branche à créer"
branch="$FEATURE${branch#$FEATURE}"
resolve_should_push
git_ensure_cleancheckout
if array_contains AllBranches "$branch"; then
git checkout -q "$branch"
else
# si la branche source n'existe pas, la créer
args=(--origin "$Origin")
if [ -n "$ConfigFile" ]; then args+=(--config-file "$ConfigFile")
elif [ -n "$ConfigBranch" ]; then args+=(--config-branch "$ConfigBranch")
fi
[ -z "$Push" ] && args+=(--no-push)
exec "$MYDIR/pman" "${args[@]}" "$branch"
fi

View File

@ -24,6 +24,8 @@
"ext-posix": "*",
"ext-pcntl": "*",
"ext-curl": "*",
"ext-pdo": "*",
"ext-pgsql": "*",
"ext-sqlite3": "*"
},
"autoload": {

31
composer.lock generated
View File

@ -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": "ab280aa4a5f5c83fa488537530b29759",
"content-hash": "a8b9dc80255663640bda855729ef2d47",
"packages": [
{
"name": "symfony/deprecation-contracts",
@ -301,16 +301,16 @@
},
{
"name": "myclabs/deep-copy",
"version": "1.12.1",
"version": "1.13.0",
"source": {
"type": "git",
"url": "https://github.com/myclabs/DeepCopy.git",
"reference": "123267b2c49fbf30d78a7b2d333f6be754b94845"
"reference": "024473a478be9df5fdaca2c793f2232fe788e414"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/123267b2c49fbf30d78a7b2d333f6be754b94845",
"reference": "123267b2c49fbf30d78a7b2d333f6be754b94845",
"url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/024473a478be9df5fdaca2c793f2232fe788e414",
"reference": "024473a478be9df5fdaca2c793f2232fe788e414",
"shasum": ""
},
"require": {
@ -349,7 +349,7 @@
],
"support": {
"issues": "https://github.com/myclabs/DeepCopy/issues",
"source": "https://github.com/myclabs/DeepCopy/tree/1.12.1"
"source": "https://github.com/myclabs/DeepCopy/tree/1.13.0"
},
"funding": [
{
@ -357,7 +357,7 @@
"type": "tidelift"
}
],
"time": "2024-11-08T17:47:46+00:00"
"time": "2025-02-12T12:17:51+00:00"
},
{
"name": "nikic/php-parser",
@ -423,13 +423,19 @@
"source": {
"type": "git",
"url": "https://git.univ-reunion.fr/sda-php/nulib-tests.git",
"reference": "9b5c9c295c3dee6fc02ccddbd8a70bca797c8045"
"reference": "8f641d9a7cf6aba1453cb42ebd15951aa7002e1b"
},
"require": {
"php": ">=7.3",
"php": "^7.3 || 8.0.*",
"phpunit/phpunit": "^9"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-pu9": "7.3.x-dev",
"dev-pu10": "8.1.x-dev"
}
},
"autoload": {
"psr-4": {
"nulib\\tests\\": "src"
@ -447,7 +453,7 @@
}
],
"description": "fonctions et classes pour les tests",
"time": "2025-01-30T13:18:31+00:00"
"time": "2025-02-28T17:12:35+00:00"
},
{
"name": "phar-io/manifest",
@ -2015,7 +2021,10 @@
"platform-dev": {
"ext-posix": "*",
"ext-pcntl": "*",
"ext-curl": "*"
"ext-curl": "*",
"ext-pdo": "*",
"ext-pgsql": "*",
"ext-sqlite3": "*"
},
"plugin-api-version": "2.2.0"
}

View File

@ -25,7 +25,7 @@ COPY --from=builder /src/su-exec/su-exec /g/
RUN /g/build
COPY --from=php /g/ /g/
RUN /g/build -a @apache-php-cas php-utils
RUN /g/build -a @php-apache-cas php-utils
EXPOSE 80 443
ENTRYPOINT ["/g/entrypoint"]

View File

@ -34,7 +34,7 @@ COPY --from=legacytools /g/ /g/
RUN /g/build nutools
COPY --from=php /g/ /g/
RUN /g/build -a @apache-php-cas php-utils
RUN /g/build -a @php-apache-cas php-utils
COPY --from=instantclient /g/ /g/
COPY --from=builder /opt/oracle/ /opt/oracle/

View File

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

View File

@ -2,7 +2,7 @@
namespace nulib;
use ArrayAccess;
use nulib\php\nur_func;
use nulib\php\func;
use Traversable;
/**
@ -348,12 +348,12 @@ class cl {
#############################################################################
static final function map(callable $callback, ?iterable $array): array {
static final function map($func, ?iterable $array): array {
$result = [];
if ($array !== null) {
$ctx = nur_func::_prepare($callback);
$func = func::with($func);
foreach ($array as $key => $value) {
$result[$key] = nur_func::_call($ctx, [$value, $key]);
$result[$key] = $func->invoke([$value, $key]);
}
}
return $result;
@ -610,6 +610,42 @@ class cl {
#############################################################################
/**
* tester si $array a en début de tableau les mêmes clés que $ref, et dans le
* même ordre. $array peut avoir d'autres clés, ça n'influe pas sur le résultat
*
* $keys obtient la liste des clés de $ref trouvées, dans l'ordre de $array
* $remainKeys obtient la liste des clés de $array qui ne sont pas dans $ref
* $missingKeys obtient la liste des clés de $ref qui ne sont pas dans $array
*/
static function same_keys(?array $array, ?array $ref, ?array &$keys=null, ?array &$remainKeys=null, ?array &$missingKeys=null): bool {
$keys = [];
$remainKeys = [];
$missingKeys = [];
if ($array === null || $array === []) {
if ($ref === null || $ref === []) return true;
$missingKeys = array_keys($ref);
return false;
} elseif ($ref === null || $ref === []) {
$remainKeys = array_keys($array);
return true;
}
$refKeys = array_keys($ref);
$refCount = count($ref);
$index = 0;
$sameKeys = true;
foreach (array_keys($array) as $key) {
if ($index < $refCount) {
if ($key !== $refKeys[$index]) $sameKeys = false;
$index++;
}
if (array_key_exists($key, $ref)) $keys[] = $key;
else $remainKeys[] = $key;
}
$missingKeys = array_values(array_diff($refKeys, $keys));
return $sameKeys && $index == $refCount;
}
/**
* retourner le tableau $array en "renommant" les clés selon le tableau
* $mappings qui contient des associations de la forme [$from => $to]

View File

@ -1,7 +1,7 @@
<?php
namespace nulib\db;
use nulib\php\nur_func;
use nulib\php\func;
use nulib\ValueException;
use Traversable;
@ -87,7 +87,7 @@ class Capacitor implements ITransactor {
if ($func !== null) {
$commited = false;
try {
nur_func::call($func, $this);
func::call($func, $this);
if ($commit) {
$this->commit();
$commited = true;
@ -120,10 +120,6 @@ class Capacitor implements ITransactor {
if ($db->inTransaction()) $db->rollback();
}
function getCreateSql(): string {
return $this->storage->_getCreateSql($this->channel);
}
function exists(): bool {
return $this->storage->_exists($this->channel);
}

View File

@ -17,6 +17,8 @@ class CapacitorChannel {
const PRIMARY_KEYS = null;
const MIGRATION = null;
const MANAGE_TRANSACTIONS = true;
const EACH_COMMIT_THRESHOLD = 100;
@ -63,15 +65,29 @@ class CapacitorChannel {
$this->created = false;
$columnDefinitions = cl::withn(static::COLUMN_DEFINITIONS);
$primaryKeys = cl::withn(static::PRIMARY_KEYS);
if ($primaryKeys === null && $columnDefinitions !== null) {
$migration = cl::withn(static::MIGRATION);
if ($columnDefinitions !== null) {
# mettre à jour la liste des clés primaires et des migrations
$index = 0;
foreach ($columnDefinitions as $col => $def) {
if ($col === $index) {
# si définition séquentielle, seules les définitions de clé
# primaires sont supportées
$index++;
if (preg_match('/\bprimary\s+key\s+\((.+)\)/i', $def, $ms)) {
$primaryKeys = preg_split('/\s*,\s*/', trim($ms[1]));
}
} elseif (is_array($def)) {
# tableau: c'est une migration
$def = implode(" ", $def);
if ($def) {
$migration["add_$col"] = "alter table $tableName add column $col $def";
} else {
$migration["drop_$col"] = "alter table $tableName drop column $col";
}
} elseif (is_scalar($def)) {
# chaine: c'est une définition
$def = strval($def);
if (preg_match('/\bprimary\s+key\b/i', $def)) {
$primaryKeys[] = $col;
}
@ -80,6 +96,7 @@ class CapacitorChannel {
}
$this->columnDefinitions = $columnDefinitions;
$this->primaryKeys = $primaryKeys;
$this->migration = $migration;
}
protected string $name;
@ -192,6 +209,12 @@ class CapacitorChannel {
return $this->columnDefinitions;
}
protected ?array $migration;
function getMigration(): ?array {
return $this->migration;
}
protected ?array $primaryKeys;
function getPrimaryKeys(): ?array {
@ -245,9 +268,6 @@ class CapacitorChannel {
return $serial !== null? unserialize($serial): null;
}
const SERIAL_DEFINITION = "mediumtext";
const SUM_DEFINITION = "varchar(40)";
final function sum(?string $serial, $value=null): ?string {
if ($serial === null) $serial = $this->serialize($value);
return $serial !== null? sha1($serial): null;

View File

@ -2,8 +2,9 @@
namespace nulib\db;
use nulib\cl;
use nulib\db\_private\_migration;
use nulib\db\cache\cache;
use nulib\php\nur_func;
use nulib\php\func;
use nulib\ValueException;
use Traversable;
@ -35,14 +36,18 @@ abstract class CapacitorStorage {
/** DOIT être défini dans les classes dérivées */
const PRIMARY_KEY_DEFINITION = null;
const SERDATA_DEFINITION = "mediumtext";
const SERSUM_DEFINITION = "varchar(40)";
const SERTS_DEFINITION = "datetime";
const COLUMN_DEFINITIONS = [
"item__" => CapacitorChannel::SERIAL_DEFINITION,
"item__sum_" => CapacitorChannel::SUM_DEFINITION,
"created_" => "datetime",
"modified_" => "datetime",
"item__" => "serdata",
"item__sum_" => "sersum",
"created_" => "serts",
"modified_" => "serts",
];
protected function ColumnDefinitions(CapacitorChannel $channel): array {
protected function ColumnDefinitions(CapacitorChannel $channel, bool $ignoreMigrations=false): array {
$definitions = [];
if ($channel->getPrimaryKeys() === null) {
$definitions[] = static::PRIMARY_KEY_DEFINITION;
@ -56,16 +61,31 @@ abstract class CapacitorStorage {
$constraints = [];
$index = 0;
foreach ($tmp as $col => $def) {
switch ($def) {
case "serdata": $def = static::SERDATA_DEFINITION; break;
case "sersum": $def = static::SERSUM_DEFINITION; break;
case "serts": $def = static::SERTS_DEFINITION; break;
}
if ($col === $index) {
$index++;
$constraints[] = $def;
} else {
} elseif (is_array($def)) {
# éventuellement, ignorer les migrations
$def = implode(" ", $def);
if ($def && !$ignoreMigrations) {
$definitions[$col] = $def;
}
} elseif (is_scalar($def)) {
$definitions[$col] = strval($def);
}
}
return cl::merge($definitions, $constraints);
}
protected function getMigration(CapacitorChannel $channel): ?array {
return $channel->getMigration();
}
/** sérialiser les valeurs qui doivent l'être dans $values */
protected function serialize(CapacitorChannel $channel, ?array $values): ?array {
if ($values === null) return null;
@ -128,11 +148,10 @@ abstract class CapacitorStorage {
}
protected function _createSql(CapacitorChannel $channel): array {
$cols = $this->ColumnDefinitions($channel);
return [
"create table if not exists",
"table" => $channel->getTableName(),
"cols" => $cols,
"cols" => $this->ColumnDefinitions($channel, true),
];
}
@ -147,20 +166,45 @@ $sql;
EOT;
}
abstract function _getCreateSql(CapacitorChannel $channel): string;
abstract function _getMigration(CapacitorChannel $channel): _migration;
/** obtenir la requête SQL utilisée pour créer la table */
function getCreateSql(?string $channel): string {
return $this->_getCreateSql($this->getChannel($channel));
const CHANNELS_TABLE = "_channels";
const CHANNELS_COLS = [
"name" => "varchar primary key",
"table_name" => "varchar",
"class_name" => "varchar",
];
protected function _createChannelsSql(): array {
return [
"create table if not exists",
"table" => static::CHANNELS_TABLE,
"cols" => static::CHANNELS_COLS,
];
}
protected function _addToChannelsSql(CapacitorChannel $channel): array {
return [
"insert",
"into" => static::CHANNELS_TABLE,
"values" => [
"name" => $channel->getName(),
"table_name" => $channel->getTableName(),
"class_name" => get_class($channel),
],
];
}
protected function _afterCreate(CapacitorChannel $channel): void {
$db = $this->db();
$db->exec($this->_createChannelsSql());
$db->exec($this->_addToChannelsSql($channel));
}
protected function _create(CapacitorChannel $channel): void {
$channel->ensureSetup();
if (!$channel->isCreated()) {
$this->db->exec($this->_createSql($channel));
$this->_getMigration($channel)->migrate($this->db());
$this->_afterCreate($channel);
$channel->setCreated();
}
@ -183,12 +227,28 @@ EOT;
}
protected function _beforeReset(CapacitorChannel $channel): void {
$db = $this->db;
$name = $channel->getName();
$db->exec([
"delete",
"from" => _migration::MIGRATION_TABLE,
"where" => [
"channel" => $name,
],
]);
$db->exec([
"delete",
"from" => static::CHANNELS_TABLE,
"where" => [
"name" => $name,
],
]);
}
/** supprimer le canal spécifié */
function _reset(CapacitorChannel $channel, bool $recreate=false): void {
$this->_beforeReset($channel);
$this->db->exec([
$this->db()->exec([
"drop table if exists",
$channel->getTableName(),
]);
@ -230,10 +290,7 @@ EOT;
$db = $this->db();
$args ??= [];
$initFunc = [$channel, "getItemValues"];
$initArgs = $args;
nur_func::ensure_func($initFunc, null, $initArgs);
$values = nur_func::call($initFunc, $item, ...$initArgs);
$values = func::call([$channel, "getItemValues"], $item, ...$args);
if ($values === [false]) return 0;
$row = cl::merge(
@ -259,9 +316,7 @@ EOT;
"modified_" => $now,
]);
$insert = true;
$initFunc = [$channel, "onCreate"];
$initArgs = $args;
nur_func::ensure_func($initFunc, null, $initArgs);
$initFunc = func::with([$channel, "onCreate"], $args);
$values = $this->unserialize($channel, $row);
$pvalues = null;
} else {
@ -276,14 +331,12 @@ EOT;
} else {
$row = cl::merge($prow, $row);
}
$initFunc = [$channel, "onUpdate"];
$initArgs = $args;
nur_func::ensure_func($initFunc, null, $initArgs);
$initFunc = func::with([$channel, "onUpdate"], $args);
$values = $this->unserialize($channel, $row);
$pvalues = $this->unserialize($channel, $prow);
}
$updates = nur_func::call($initFunc, $item, $values, $pvalues, ...$initArgs);
$updates = $initFunc->prependArgs([$item, $values, $pvalues])->invoke();
if ($updates === [false]) return 0;
if (is_array($updates) && $updates) {
if ($insert === null) $insert = false;
@ -295,8 +348,10 @@ EOT;
}
if ($func !== null) {
nur_func::ensure_func($func, $channel, $args);
$updates = nur_func::call($func, $item, $values, $pvalues, ...$args);
$updates = func::with($func)
->prependArgs([$item, $values, $pvalues])
->bind($channel, true)
->invoke();
if ($updates === [false]) return 0;
if (is_array($updates) && $updates) {
if ($insert === null) $insert = false;
@ -510,8 +565,7 @@ EOT;
function _each(CapacitorChannel $channel, $filter, $func, ?array $args, ?array $mergeQuery=null, ?int &$nbUpdated=null): int {
$this->_create($channel);
if ($func === null) $func = CapacitorChannel::onEach;
nur_func::ensure_func($func, $channel, $args);
$onEach = nur_func::_prepare($func);
$onEach = func::with($func)->bind($channel, true);
$db = $this->db();
# si on est déjà dans une transaction, désactiver la gestion des transactions
$manageTransactions = $channel->isManageTransactions() && !$db->inTransaction();
@ -528,7 +582,7 @@ EOT;
$all = $this->_allCached("each", $channel, $filter, $mergeQuery);
foreach ($all as $values) {
$rowIds = $this->getRowIds($channel, $values);
$updates = nur_func::_call($onEach, [$values["item"], $values, ...$args]);
$updates = $onEach->invoke([$values["item"], $values, ...$args]);
if (is_array($updates) && $updates) {
if (!array_key_exists("modified_", $updates)) {
$updates["modified_"] = date("Y-m-d H:i:s");
@ -579,8 +633,7 @@ EOT;
function _delete(CapacitorChannel $channel, $filter, $func, ?array $args): int {
$this->_create($channel);
if ($func === null) $func = CapacitorChannel::onDelete;
nur_func::ensure_func($func, $channel, $args);
$onEach = nur_func::_prepare($func);
$onEach = func::with($func)->bind($channel, true);
$db = $this->db();
# si on est déjà dans une transaction, désactiver la gestion des transactions
$manageTransactions = $channel->isManageTransactions() && !$db->inTransaction();
@ -596,7 +649,7 @@ EOT;
$all = $this->_allCached("delete", $channel, $filter);
foreach ($all as $values) {
$rowIds = $this->getRowIds($channel, $values);
$delete = boolval(nur_func::_call($onEach, [$values["item"], $values, ...$args]));
$delete = boolval($onEach->invoke([$values["item"], $values, ...$args]));
if ($delete) {
$db->exec([
"delete",

View File

@ -15,5 +15,9 @@ interface IDatabase extends ITransactor {
function one($query, ?array $params=null): ?array;
/**
* si $primaryKeys est fourni, le résultat est indexé sur la(es) colonne(s)
* spécifiée(s)
*/
function all($query, ?array $params=null, $primaryKeys=null): iterable;
}

View File

@ -11,12 +11,13 @@ interface ITransactor {
*/
function willUpdate(...$transactors): self;
/** Indiquer si une transaction est en cours */
function inTransaction(): bool;
/**
* démarrer une transaction
*
* si $func!==null, l'apppeler. ensuite, si $commit===true, commiter la
* si $func!==null, l'apppeler. ensuite, si $commit===true, valider la
* transaction. si une erreur se produit lors de l'appel de la fonction,
* annuler la transaction
*
@ -24,7 +25,9 @@ interface ITransactor {
*/
function beginTransaction(?callable $func=null, bool $commit=true): void;
/** valider la transaction */
function commit(): void;
/** annuler la transaction */
function rollback(): void;
}

View File

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

View File

@ -1,38 +0,0 @@
<?php
namespace nulib\db\_private;
trait Tdelete {
static function isa(string $sql): bool {
return preg_match("/^delete(?:\s+from)?\b/i", $sql);
}
static function parse(array $query, ?array &$bindings=null): string {
#XXX implémentation minimale
$tmpsql = self::merge_seq($query);
self::consume('delete(?:\s+from)?\b', $tmpsql);
$sql = ["delete from", $tmpsql];
## préfixe
if (($prefix = $query["prefix"] ?? null) !== null) $sql[] = $prefix;
## table
$sql[] = $query["from"];
## where
$where = $query["where"] ?? null;
if ($where !== null) {
self::parse_conds($where, $wheresql, $bindings);
if ($wheresql) {
$sql[] = "where";
$sql[] = implode(" and ", $wheresql);
}
}
## suffixe
if (($suffix = $query["suffix"] ?? null) !== null) $sql[] = $suffix;
## fin de la requête
return implode(" ", $sql);
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -5,255 +5,57 @@ use nulib\cl;
use nulib\str;
use nulib\ValueException;
abstract class _base {
protected static function consume(string $pattern, string &$string, ?array &$ms=null): bool {
if (!preg_match("/^$pattern/i", $string, $ms)) return false;
$string = substr($string, strlen($ms[0]));
return true;
}
/** fusionner toutes les parties séquentielles d'une requête */
protected static function merge_seq(array $query): string {
$index = 0;
$sql = "";
foreach ($query as $key => $value) {
if ($key === $index) {
$index++;
if ($sql && !str::ends_with(" ", $sql) && !str::starts_with(" ", $value)) {
$sql .= " ";
}
$sql .= $value;
}
}
return $sql;
}
protected static function is_sep(&$cond): bool {
if (!is_string($cond)) return false;
if (!preg_match('/^\s*(and|or|not)\s*$/i', $cond, $ms)) return false;
$cond = $ms[1];
return true;
}
static function parse_conds(?array $conds, ?array &$sql, ?array &$bindings): void {
if (!$conds) return;
$sep = null;
$index = 0;
$condsql = [];
foreach ($conds as $key => $cond) {
if ($key === $index) {
## séquentiel
if ($index === 0 && self::is_sep($cond)) {
$sep = $cond;
} elseif (is_bool($cond)) {
# ignorer les valeurs true et false
} elseif (is_array($cond)) {
# condition récursive
self::parse_conds($cond, $condsql, $bindings);
abstract class _base extends _common {
protected static function verifix(&$sql, ?array &$bindings=null, ?array &$meta=null): void {
if (is_array($sql)) {
$prefix = $sql[0] ?? null;
if ($prefix === null) {
throw new ValueException("requête invalide");
} elseif (_create::isa($prefix)) {
$sql = _create::parse($sql, $bindings);
$meta = ["isa" => "create", "type" => "ddl"];
} elseif (_select::isa($prefix)) {
$sql = _select::parse($sql, $bindings);
$meta = ["isa" => "select", "type" => "dql"];
} elseif (_insert::isa($prefix)) {
$sql = _insert::parse($sql, $bindings);
$meta = ["isa" => "insert", "type" => "dml"];
} elseif (_update::isa($prefix)) {
$sql = _update::parse($sql, $bindings);
$meta = ["isa" => "update", "type" => "dml"];
} elseif (_delete::isa($prefix)) {
$sql = _delete::parse($sql, $bindings);
$meta = ["isa" => "delete", "type" => "dml"];
} elseif (_generic::isa($prefix)) {
$sql = _generic::parse($sql, $bindings);
$meta = ["isa" => "generic", "type" => null];
} else {
# condition litérale
$condsql[] = strval($cond);
}
$index++;
} elseif ($cond === false) {
## associatif
# condition litérale ignorée car condition false
} elseif ($cond === true) {
# condition litérale sélectionnée car condition true
$condsql[] = strval($key);
} else {
## associatif
# paramètre
$param0 = preg_replace('/^.+\./', "", $key);
$i = false;
if ($bindings !== null && array_key_exists($param0, $bindings)) {
$i = 2;
while (array_key_exists("$param0$i", $bindings)) {
$i++;
}
}
# value ou [operator, value]
$condprefix = $condsep = $condsuffix = null;
if (is_array($cond)) {
$condkey = 0;
$condkeys = array_keys($cond);
$op = null;
if (array_key_exists("op", $cond)) {
$op = $cond["op"];
} elseif (array_key_exists($condkey, $condkeys)) {
$op = $cond[$condkeys[$condkey]];
$condkey++;
}
$op = strtolower($op);
$condvalues = null;
switch ($op) {
case "between":
# ["between", $upper, $lower]
$condsep = " and ";
if (array_key_exists("lower", $cond)) {
$condvalues[] = $cond["lower"];
} elseif (array_key_exists($condkey, $condkeys)) {
$condvalues[] = $cond[$condkeys[$condkey]];
$condkey++;
}
if (array_key_exists("upper", $cond)) {
$condvalues[] = $cond["upper"];
} elseif (array_key_exists($condkey, $condkeys)) {
$condvalues[] = $cond[$condkeys[$condkey]];
$condkey++;
}
break;
case "any":
case "all":
case "not any":
case "not all":
# ["list", $values]
if ($op === "any" || $op === "all") {
$condprefix = $op;
$op = "=";
} elseif ($op === "not any" || $op === "not all") {
$condprefix = substr($op, strlen("not "));
$op = "<>";
}
$condprefix .= "(array[";
$condsep = ", ";
$condsuffix = "])";
$condvalues = null;
if (array_key_exists("values", $cond)) {
$condvalues = cl::with($cond["values"]);
} elseif (array_key_exists($condkey, $condkeys)) {
$condvalues = cl::with($cond[$condkeys[$condkey]]);
$condkey++;
}
break;
case "in":
# ["in", $values]
$condprefix = "(";
$condsep = ", ";
$condsuffix = ")";
$condvalues = null;
if (array_key_exists("values", $cond)) {
$condvalues = cl::with($cond["values"]);
} elseif (array_key_exists($condkey, $condkeys)) {
$condvalues = cl::with($cond[$condkeys[$condkey]]);
$condkey++;
}
break;
case "null":
case "is null":
$op = "is null";
break;
case "not null":
case "is not null":
$op = "is not null";
break;
default:
if (array_key_exists("value", $cond)) {
$condvalues = [$cond["value"]];
} elseif (array_key_exists($condkey, $condkeys)) {
$condvalues = [$cond[$condkeys[$condkey]]];
$condkey++;
}
}
} elseif ($cond !== null) {
$op = "=";
$condvalues = [$cond];
} else {
$op = "is null";
$condvalues = null;
}
$cond = [$key, $op];
if ($condvalues !== null) {
$parts = [];
foreach ($condvalues as $condvalue) {
if (is_array($condvalue)) {
$first = true;
foreach ($condvalue as $value) {
if ($first) {
$first = false;
} else {
if ($sep === null) $sep = "and";
$parts[] = " $sep ";
$parts[] = $key;
$parts[] = " $op ";
}
$param = "$param0$i";
$parts[] = ":$param";
$bindings[$param] = $value;
if ($i === false) $i = 2;
else $i++;
throw ValueException::invalid_kind($sql, "query");
}
} else {
$param = "$param0$i";
$parts[] = ":$param";
$bindings[$param] = $condvalue;
if ($i === false) $i = 2;
else $i++;
if (!is_string($sql)) $sql = strval($sql);
if (_create::isa($sql)) {
$meta = ["isa" => "create", "type" => "ddl"];
} elseif (_select::isa($sql)) {
$meta = ["isa" => "select", "type" => "dql"];
} elseif (_insert::isa($sql)) {
$meta = ["isa" => "insert", "type" => "dml"];
} elseif (_update::isa($sql)) {
$meta = ["isa" => "update", "type" => "dml"];
} elseif (_delete::isa($sql)) {
$meta = ["isa" => "delete", "type" => "dml"];
} elseif (_generic::isa($sql)) {
$meta = ["isa" => "generic", "type" => null];
} else {
$meta = ["isa" => "generic", "type" => null];
}
}
$cond[] = $condprefix.implode($condsep, $parts).$condsuffix;
}
$condsql[] = implode(" ", $cond);
}
}
if ($sep === null) $sep = "and";
$count = count($condsql);
if ($count > 1) {
$sql[] = "(" . implode(" $sep ", $condsql) . ")";
} elseif ($count == 1) {
$sql[] = $condsql[0];
}
}
static function parse_set_values(?array $values, ?array &$sql, ?array &$bindings): void {
if (!$values) return;
$index = 0;
$parts = [];
foreach ($values as $key => $part) {
if ($key === $index) {
## séquentiel
if (is_array($part)) {
# paramètres récursifs
self::parse_set_values($part, $parts, $bindings);
} else {
# paramètre litéral
$parts[] = strval($part);
static function with($sql, ?array $params=null): array {
static::verifix($sql, $params);
return [$sql, $params];
}
$index++;
} else {
## associatif
# paramètre
$param = $param0 = preg_replace('/^.+\./', "", $key);
if ($bindings !== null && array_key_exists($param0, $bindings)) {
$i = 2;
while (array_key_exists("$param0$i", $bindings)) {
$i++;
}
$param = "$param0$i";
}
# value
$value = $part;
$part = [$key, "="];
if ($value === null) {
$part[] = "null";
} else {
$part[] = ":$param";
$bindings[$param] = $value;
}
$parts[] = implode(" ", $part);
}
}
$sql = cl::merge($sql, $parts);
}
protected static function check_eof(string $tmpsql, string $usersql): void {
self::consume(';\s*', $tmpsql);
if ($tmpsql) {
throw new ValueException("unexpected value at end: $usersql");
}
}
abstract protected static function verifix(&$sql, ?array &$bindinds=null, ?array &$meta=null): void;
function __construct($sql, ?array $bindings=null) {
static::verifix($sql, $bindings, $meta);

View File

@ -0,0 +1,255 @@
<?php
namespace nulib\db\_private;
use nulib\cl;
use nulib\str;
use nulib\ValueException;
class _common {
protected static function consume(string $pattern, string &$string, ?array &$ms=null): bool {
if (!preg_match("/^$pattern/i", $string, $ms)) return false;
$string = substr($string, strlen($ms[0]));
return true;
}
/** fusionner toutes les parties séquentielles d'une requête */
protected static function merge_seq(array $query): string {
$index = 0;
$sql = "";
foreach ($query as $key => $value) {
if ($key === $index) {
$index++;
if ($sql && !str::ends_with(" ", $sql) && !str::starts_with(" ", $value)) {
$sql .= " ";
}
$sql .= $value;
}
}
return $sql;
}
protected static function is_sep(&$cond): bool {
if (!is_string($cond)) return false;
if (!preg_match('/^\s*(and|or|not)\s*$/i', $cond, $ms)) return false;
$cond = $ms[1];
return true;
}
static function parse_conds(?array $conds, ?array &$sql, ?array &$bindings): void {
if (!$conds) return;
$sep = null;
$index = 0;
$condsql = [];
foreach ($conds as $key => $cond) {
if ($key === $index) {
## séquentiel
if ($index === 0 && self::is_sep($cond)) {
$sep = $cond;
} elseif (is_bool($cond)) {
# ignorer les valeurs true et false
} elseif (is_array($cond)) {
# condition récursive
self::parse_conds($cond, $condsql, $bindings);
} else {
# condition litérale
$condsql[] = strval($cond);
}
$index++;
} elseif ($cond === false) {
## associatif
# condition litérale ignorée car condition false
} elseif ($cond === true) {
# condition litérale sélectionnée car condition true
$condsql[] = strval($key);
} else {
## associatif
# paramètre
$param0 = preg_replace('/^.+\./', "", $key);
$i = false;
if ($bindings !== null && array_key_exists($param0, $bindings)) {
$i = 2;
while (array_key_exists("$param0$i", $bindings)) {
$i++;
}
}
# value ou [operator, value]
$condprefix = $condsep = $condsuffix = null;
if (is_array($cond)) {
$condkey = 0;
$condkeys = array_keys($cond);
$op = null;
if (array_key_exists("op", $cond)) {
$op = $cond["op"];
} elseif (array_key_exists($condkey, $condkeys)) {
$op = $cond[$condkeys[$condkey]];
$condkey++;
}
$op = strtolower($op);
$condvalues = null;
switch ($op) {
case "between":
# ["between", $upper, $lower]
$condsep = " and ";
if (array_key_exists("lower", $cond)) {
$condvalues[] = $cond["lower"];
} elseif (array_key_exists($condkey, $condkeys)) {
$condvalues[] = $cond[$condkeys[$condkey]];
$condkey++;
}
if (array_key_exists("upper", $cond)) {
$condvalues[] = $cond["upper"];
} elseif (array_key_exists($condkey, $condkeys)) {
$condvalues[] = $cond[$condkeys[$condkey]];
$condkey++;
}
break;
case "any":
case "all":
case "not any":
case "not all":
# ["list", $values]
if ($op === "any" || $op === "all") {
$condprefix = $op;
$op = "=";
} elseif ($op === "not any" || $op === "not all") {
$condprefix = substr($op, strlen("not "));
$op = "<>";
}
$condprefix .= "(array[";
$condsep = ", ";
$condsuffix = "])";
$condvalues = null;
if (array_key_exists("values", $cond)) {
$condvalues = cl::with($cond["values"]);
} elseif (array_key_exists($condkey, $condkeys)) {
$condvalues = cl::with($cond[$condkeys[$condkey]]);
$condkey++;
}
break;
case "in":
# ["in", $values]
$condprefix = "(";
$condsep = ", ";
$condsuffix = ")";
$condvalues = null;
if (array_key_exists("values", $cond)) {
$condvalues = cl::with($cond["values"]);
} elseif (array_key_exists($condkey, $condkeys)) {
$condvalues = cl::with($cond[$condkeys[$condkey]]);
$condkey++;
}
break;
case "null":
case "is null":
$op = "is null";
break;
case "not null":
case "is not null":
$op = "is not null";
break;
default:
if (array_key_exists("value", $cond)) {
$condvalues = [$cond["value"]];
} elseif (array_key_exists($condkey, $condkeys)) {
$condvalues = [$cond[$condkeys[$condkey]]];
$condkey++;
}
}
} elseif ($cond !== null) {
$op = "=";
$condvalues = [$cond];
} else {
$op = "is null";
$condvalues = null;
}
$cond = [$key, $op];
if ($condvalues !== null) {
$parts = [];
foreach ($condvalues as $condvalue) {
if (is_array($condvalue)) {
$first = true;
foreach ($condvalue as $value) {
if ($first) {
$first = false;
} else {
if ($sep === null) $sep = "and";
$parts[] = " $sep ";
$parts[] = $key;
$parts[] = " $op ";
}
$param = "$param0$i";
$parts[] = ":$param";
$bindings[$param] = $value;
if ($i === false) $i = 2;
else $i++;
}
} else {
$param = "$param0$i";
$parts[] = ":$param";
$bindings[$param] = $condvalue;
if ($i === false) $i = 2;
else $i++;
}
}
$cond[] = $condprefix.implode($condsep, $parts).$condsuffix;
}
$condsql[] = implode(" ", $cond);
}
}
if ($sep === null) $sep = "and";
$count = count($condsql);
if ($count > 1) {
$sql[] = "(" . implode(" $sep ", $condsql) . ")";
} elseif ($count == 1) {
$sql[] = $condsql[0];
}
}
static function parse_set_values(?array $values, ?array &$sql, ?array &$bindings): void {
if (!$values) return;
$index = 0;
$parts = [];
foreach ($values as $key => $part) {
if ($key === $index) {
## séquentiel
if (is_array($part)) {
# paramètres récursifs
self::parse_set_values($part, $parts, $bindings);
} else {
# paramètre litéral
$parts[] = strval($part);
}
$index++;
} else {
## associatif
# paramètre
$param = $param0 = preg_replace('/^.+\./', "", $key);
if ($bindings !== null && array_key_exists($param0, $bindings)) {
$i = 2;
while (array_key_exists("$param0$i", $bindings)) {
$i++;
}
$param = "$param0$i";
}
# value
$value = $part;
$part = [$key, "="];
if ($value === null) {
$part[] = "null";
} else {
$part[] = ":$param";
$bindings[$param] = $value;
}
$parts[] = implode(" ", $part);
}
}
$sql = cl::merge($sql, $parts);
}
protected static function check_eof(string $tmpsql, string $usersql): void {
self::consume(';\s*', $tmpsql);
if ($tmpsql) {
throw new ValueException("unexpected value at end: $usersql");
}
}
}

View File

@ -1,7 +1,8 @@
<?php
namespace nulib\db\pdo;
namespace nulib\db\_private;
use nulib\php\nur_func;
use nulib\db\IDatabase;
use nulib\php\func;
class _config {
static function with($configs): self {
@ -23,13 +24,12 @@ class _config {
/** @var array */
protected $configs;
function configure(Pdo $pdo): void {
function configure(IDatabase $db): void {
foreach ($this->configs as $key => $config) {
if (is_string($config) && !nur_func::is_method($config)) {
$pdo->exec($config);
if (is_string($config) && !func::is_method($config)) {
$db->exec($config);
} else {
nur_func::ensure_func($config, $this, $args);
nur_func::call($config, $pdo, $key, ...$args);
func::with($config)->bind($this, true)->invoke([$db, $key]);
}
}
}

View File

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

View File

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

View File

@ -1,7 +1,21 @@
<?php
namespace nulib\db\_private;
class _generic {
use nulib\cl;
use nulib\ValueException;
class _generic extends _common {
const SCHEMA = [
];
static function isa(string $sql): bool {
return preg_match('/^drop\s+table\b/i', $sql);
}
static function parse(array $query, ?array &$bindings=null): string {
if (!cl::is_list($query)) {
throw new ValueException("Seuls les tableaux séquentiels sont supportés");
}
return self::merge_seq($query);
}
}

View File

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

View File

@ -0,0 +1,72 @@
<?php
namespace nulib\db\_private;
use nulib\db\IDatabase;
use nulib\php\func;
abstract class _migration {
const MIGRATION = null;
function __construct($migrations, string $channel="", ?IDatabase $db=null) {
$this->db = $db;
$this->channel = $channel;
if ($migrations === null) $migrations = static::MIGRATION;
if ($migrations === null) $migrations = [];
elseif (is_string($migrations)) $migrations = [$migrations];
elseif (is_callable($migrations)) $migrations = [$migrations];
elseif (!is_array($migrations)) $migrations = [strval($migrations)];
$this->migrations = $migrations;
}
protected ?IDatabase $db;
protected string $channel;
const MIGRATION_TABLE = "_migration";
const MIGRATION_COLS = [
"channel" => "varchar not null",
"name" => "varchar not null",
"done" => "integer not null default 0",
"primary key (channel, name)",
];
protected function ensureTable(): void {
$this->db->exec([
"create table if not exists",
"table" => static::MIGRATION_TABLE,
"cols" => static::MIGRATION_COLS,
]);
}
protected function isMigrated(string $name): bool {
return boolval($this->db->get([
"select 1",
"from" => static::MIGRATION_TABLE,
"where" => [
"channel" => $this->channel,
"name" => $name,
"done" => 1,
],
]));
}
abstract protected function setMigrated(string $name, bool $done): void;
/** @var callable[]|string[] */
protected $migrations;
function migrate(?IDatabase $db=null): void {
$db = ($this->db ??= $db);
$this->ensureTable();
foreach ($this->migrations as $name => $migration) {
if ($this->isMigrated($name)) continue;
$this->setMigrated($name, false);
if (is_string($migration) || !func::is_callable($migration)) {
$db->exec($migration);
} else {
func::with($migration)->bind($this, true)->invoke([$db, $name]);
}
$this->setMigrated($name, true);
}
}
}

View File

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

View File

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

View File

@ -1,6 +1,7 @@
<?php
namespace nulib\db\mysql;
use nulib\cl;
use nulib\db\CapacitorChannel;
use nulib\db\CapacitorStorage;
@ -12,8 +13,7 @@ class MysqlStorage extends CapacitorStorage {
$this->db = Mysql::with($mysql);
}
/** @var Mysql */
protected $db;
protected Mysql $db;
function db(): Mysql {
return $this->db;
@ -23,17 +23,35 @@ class MysqlStorage extends CapacitorStorage {
"id_" => "integer primary key auto_increment",
];
function _getMigration(CapacitorChannel $channel): _mysqlMigration {
return new _mysqlMigration(cl::merge([
$this->_createSql($channel),
], $channel->getMigration()), $channel->getName());
}
function _getCreateSql(CapacitorChannel $channel): string {
$query = new _query_base($this->_createSql($channel));
$query = new _mysqlQuery($this->_createSql($channel));
return self::format_sql($channel, $query->getSql());
}
const CHANNELS_COLS = [
"name" => "varchar(255) primary key",
"table_name" => "varchar(64)",
"class_name" => "varchar(255)",
];
protected function _addToChannelsSql(CapacitorChannel $channel): array {
return cl::merge(parent::_addToChannelsSql($channel), [
"suffix" => "on duplicate key update name = name",
]);
}
function _exists(CapacitorChannel $channel): bool {
$db = $this->db;
$tableName = $db->get([
$mysql = $this->db;
$tableName = $mysql->get([
"select table_name from information_schema.tables",
"where" => [
"table_schema" => $db->getDbname(),
"table_schema" => $mysql->getDbname(),
"table_name" => $channel->getTableName(),
],
]);

View File

@ -0,0 +1,31 @@
<?php
namespace nulib\db\mysql;
use nulib\db\_private\_migration;
class _mysqlMigration extends _migration {
static function with($migration): self {
if ($migration instanceof self) return $migration;
else return new static($migration);
}
const MIGRATION_COLS = [
"channel" => "varchar(64) not null",
"name" => "varchar(64) not null",
"done" => "integer not null default 0",
"primary key (channel, name)",
];
protected function setMigrated(string $name, bool $done): void {
$this->db->exec([
"insert",
"into" => static::MIGRATION_TABLE,
"values" => [
"channel" => $this->channel,
"name" => $name,
"done" => $done? 1: 0,
],
"suffix" => "on duplicate key update done = :done",
]);
}
}

View File

@ -0,0 +1,8 @@
<?php
namespace nulib\db\mysql;
use nulib\db\pdo\_pdoQuery;
class _mysqlQuery extends _pdoQuery {
const DEBUG_QUERIES = false;
}

View File

@ -1,52 +0,0 @@
<?php
namespace nulib\db\mysql;
use nulib\ValueException;
class _query_base extends \nulib\db\pdo\_query_base {
protected static function verifix(&$sql, ?array &$bindinds=null, ?array &$meta=null): void {
if (is_array($sql)) {
$prefix = $sql[0] ?? null;
if ($prefix === null) {
throw new ValueException("requête invalide");
} elseif (_query_create::isa($prefix)) {
$sql = _query_create::parse($sql, $bindinds);
$meta = ["isa" => "create", "type" => "ddl"];
} elseif (_query_select::isa($prefix)) {
$sql = _query_select::parse($sql, $bindinds);
$meta = ["isa" => "select", "type" => "dql"];
} elseif (_query_insert::isa($prefix)) {
$sql = _query_insert::parse($sql, $bindinds);
$meta = ["isa" => "insert", "type" => "dml"];
} elseif (_query_update::isa($prefix)) {
$sql = _query_update::parse($sql, $bindinds);
$meta = ["isa" => "update", "type" => "dml"];
} elseif (_query_delete::isa($prefix)) {
$sql = _query_delete::parse($sql, $bindinds);
$meta = ["isa" => "delete", "type" => "dml"];
} elseif (_query_generic::isa($prefix)) {
$sql = _query_generic::parse($sql, $bindinds);
$meta = ["isa" => "generic", "type" => null];
} else {
throw ValueException::invalid_kind($sql, "query");
}
} else {
if (!is_string($sql)) $sql = strval($sql);
if (_query_create::isa($sql)) {
$meta = ["isa" => "create", "type" => "ddl"];
} elseif (_query_select::isa($sql)) {
$meta = ["isa" => "select", "type" => "dql"];
} elseif (_query_insert::isa($sql)) {
$meta = ["isa" => "insert", "type" => "dml"];
} elseif (_query_update::isa($sql)) {
$meta = ["isa" => "update", "type" => "dml"];
} elseif (_query_delete::isa($sql)) {
$meta = ["isa" => "delete", "type" => "dml"];
} elseif (_query_generic::isa($sql)) {
$meta = ["isa" => "generic", "type" => null];
} else {
$meta = ["isa" => "generic", "type" => null];
}
}
}
}

View File

@ -1,10 +0,0 @@
<?php
namespace nulib\db\mysql;
use nulib\db\_private\_create;
use nulib\db\_private\Tcreate;
class _query_create extends _query_base {
use Tcreate;
const SCHEMA = _create::SCHEMA;
}

View File

@ -1,10 +0,0 @@
<?php
namespace nulib\db\mysql;
use nulib\db\_private\_delete;
use nulib\db\_private\Tdelete;
class _query_delete extends _query_base {
use Tdelete;
const SCHEMA = _delete::SCHEMA;
}

View File

@ -1,10 +0,0 @@
<?php
namespace nulib\db\mysql;
use nulib\db\_private\_generic;
use nulib\db\_private\Tgeneric;
class _query_generic extends _query_base {
use Tgeneric;
const SCHEMA = _generic::SCHEMA;
}

View File

@ -1,10 +0,0 @@
<?php
namespace nulib\db\mysql;
use nulib\db\_private\_insert;
use nulib\db\_private\Tinsert;
class _query_insert extends _query_base {
use Tinsert;
const SCHEMA = _insert::SCHEMA;
}

View File

@ -1,10 +0,0 @@
<?php
namespace nulib\db\mysql;
use nulib\db\_private\_select;
use nulib\db\_private\Tselect;
class _query_select extends _query_base {
use Tselect;
const SCHEMA = _select::SCHEMA;
}

View File

@ -1,10 +0,0 @@
<?php
namespace nulib\db\mysql;
use nulib\db\_private\_update;
use nulib\db\_private\Tupdate;
class _query_update extends _query_base {
use Tupdate;
const SCHEMA = _update::SCHEMA;
}

View File

@ -1,12 +0,0 @@
<?php
namespace nulib\db\mysql;
/**
* Class query: classe outil temporaire pour générer les requêtes
*/
class query extends _query_base {
static function with($sql, ?array $params=null): array {
self::verifix($sql, $params);
return [$sql, $params];
}
}

View File

@ -3,10 +3,11 @@ namespace nulib\db\pdo;
use Generator;
use nulib\cl;
use nulib\db\_private\_config;
use nulib\db\_private\Tvalues;
use nulib\db\IDatabase;
use nulib\db\ITransactor;
use nulib\php\nur_func;
use nulib\php\func;
use nulib\ValueException;
class Pdo implements IDatabase {
@ -21,7 +22,7 @@ class Pdo implements IDatabase {
"dbconn" => $pdo->dbconn,
"options" => $pdo->options,
"config" => $pdo->config,
"migrate" => $pdo->migration,
"migration" => $pdo->migration,
], $params));
} else {
return new static($pdo, $params);
@ -49,7 +50,7 @@ class Pdo implements IDatabase {
protected const CONFIG = null;
protected const MIGRATE = null;
protected const MIGRATION = null;
const dbconn_SCHEMA = [
"name" => "string",
@ -62,7 +63,7 @@ class Pdo implements IDatabase {
"options" => ["?array|callable"],
"replace_config" => ["?array|callable"],
"config" => ["?array|callable"],
"migrate" => ["?array|string|callable"],
"migration" => ["?array|string|callable"],
"auto_open" => ["bool", true],
];
@ -93,7 +94,7 @@ class Pdo implements IDatabase {
}
$this->config = $config;
# migrations
$this->migration = $params["migrate"] ?? static::MIGRATE;
$this->migration = $params["migration"] ?? static::MIGRATION;
#
$defaultAutoOpen = self::params_SCHEMA["auto_open"][1];
if ($params["auto_open"] ?? $defaultAutoOpen) {
@ -104,7 +105,7 @@ class Pdo implements IDatabase {
protected ?array $dbconn;
/** @var array|callable */
protected array $options;
protected $options;
/** @var array|string|callable */
protected $config;
@ -119,8 +120,7 @@ class Pdo implements IDatabase {
$dbconn = $this->dbconn;
$options = $this->options;
if (is_callable($options)) {
nur_func::ensure_func($options, $this, $args);
$options = nur_func::call($options, ...$args);
$options = func::with($options)->bind($this, true)->invoke();
}
$this->db = new \PDO($dbconn["name"], $dbconn["user"], $dbconn["pass"], $options);
_config::with($this->config)->configure($this);
@ -143,21 +143,16 @@ class Pdo implements IDatabase {
return $this->db()->exec($query);
}
private static function is_insert(?string $sql): bool {
if ($sql === null) return false;
return preg_match('/^\s*insert\b/i', $sql);
}
function exec($query, ?array $params=null) {
$db = $this->db();
$query = new _query_base($query, $params);
if ($query->useStmt($db, $stmt, $sql)) {
$query = new _pdoQuery($query, $params);
if ($query->_use_stmt($db, $stmt, $sql)) {
if ($stmt->execute() === false) return false;
if ($query->isInsert()) return $db->lastInsertId();
else return $stmt->rowCount();
} else {
$rowCount = $db->exec($sql);
if (self::is_insert($sql)) return $db->lastInsertId();
if ($query->isInsert()) return $db->lastInsertId();
else return $rowCount;
}
}
@ -191,7 +186,7 @@ class Pdo implements IDatabase {
if ($func !== null) {
$commited = false;
try {
nur_func::call($func, $this);
func::call($func, $this);
if ($commit) {
$this->commit();
$commited = true;
@ -222,11 +217,11 @@ class Pdo implements IDatabase {
function get($query, ?array $params=null, bool $entireRow=false) {
$db = $this->db();
$query = new _query_base($query, $params);
$query = new _pdoQuery($query, $params);
$stmt = null;
try {
/** @var \PDOStatement $stmt */
if ($query->useStmt($db, $stmt, $sql)) {
if ($query->_use_stmt($db, $stmt, $sql)) {
if ($stmt->execute() === false) return null;
} else {
$stmt = $db->query($sql);
@ -245,22 +240,18 @@ class Pdo implements IDatabase {
return $this->get($query, $params, true);
}
/**
* si $primaryKeys est fourni, le résultat est indexé sur la(es) colonne(s)
* spécifiée(s)
*/
function all($query, ?array $params=null, $primaryKeys=null): Generator {
function all($query, ?array $params=null, $primaryKeys=null): iterable {
$db = $this->db();
$query = new _query_base($query, $params);
$query = new _pdoQuery($query, $params);
$stmt = null;
try {
/** @var \PDOStatement $stmt */
if ($query->useStmt($db, $stmt, $sql)) {
if ($query->_use_stmt($db, $stmt, $sql)) {
if ($stmt->execute() === false) return;
} else {
$stmt = $db->query($sql);
}
if ($primaryKeys !== null) $primaryKeys = cl::with($primaryKeys);
$primaryKeys = cl::withn($primaryKeys);
while (($row = $stmt->fetch(\PDO::FETCH_ASSOC)) !== false) {
$this->verifixRow($row);
if ($primaryKeys !== null) {

View File

@ -0,0 +1,30 @@
<?php
namespace nulib\db\pdo;
use nulib\db\_private\_base;
use nulib\db\_private\Tbindings;
use nulib\output\msg;
class _pdoQuery extends _base {
use Tbindings;
const DEBUG_QUERIES = false;
function _use_stmt(\PDO $db, ?\PDOStatement &$stmt=null, ?string &$sql=null): bool {
if (static::DEBUG_QUERIES) {#XXX
msg::info($this->sql);
//msg::info(var_export($this->bindings, true));
}
if ($this->bindings !== null) {
$stmt = $db->prepare($this->sql);
foreach ($this->bindings as $name => $value) {
$this->verifixBindings($value);
$stmt->bindValue($name, $value);
}
return true;
} else {
$sql = $this->sql;
return false;
}
}
}

View File

@ -1,76 +0,0 @@
<?php
namespace nulib\db\pdo;
use nulib\db\_private\_base;
use nulib\db\_private\Tbindings;
use nulib\ValueException;
class _query_base extends _base {
use Tbindings;
protected static function verifix(&$sql, ?array &$bindinds=null, ?array &$meta=null): void {
if (is_array($sql)) {
$prefix = $sql[0] ?? null;
if ($prefix === null) {
throw new ValueException("requête invalide");
} elseif (_query_create::isa($prefix)) {
$sql = _query_create::parse($sql, $bindinds);
$meta = ["isa" => "create", "type" => "ddl"];
} elseif (_query_select::isa($prefix)) {
$sql = _query_select::parse($sql, $bindinds);
$meta = ["isa" => "select", "type" => "dql"];
} elseif (_query_insert::isa($prefix)) {
$sql = _query_insert::parse($sql, $bindinds);
$meta = ["isa" => "insert", "type" => "dml"];
} elseif (_query_update::isa($prefix)) {
$sql = _query_update::parse($sql, $bindinds);
$meta = ["isa" => "update", "type" => "dml"];
} elseif (_query_delete::isa($prefix)) {
$sql = _query_delete::parse($sql, $bindinds);
$meta = ["isa" => "delete", "type" => "dml"];
} elseif (_query_generic::isa($prefix)) {
$sql = _query_generic::parse($sql, $bindinds);
$meta = ["isa" => "generic", "type" => null];
} else {
throw ValueException::invalid_kind($sql, "query");
}
} else {
if (!is_string($sql)) $sql = strval($sql);
if (_query_create::isa($sql)) {
$meta = ["isa" => "create", "type" => "ddl"];
} elseif (_query_select::isa($sql)) {
$meta = ["isa" => "select", "type" => "dql"];
} elseif (_query_insert::isa($sql)) {
$meta = ["isa" => "insert", "type" => "dml"];
} elseif (_query_update::isa($sql)) {
$meta = ["isa" => "update", "type" => "dml"];
} elseif (_query_delete::isa($sql)) {
$meta = ["isa" => "delete", "type" => "dml"];
} elseif (_query_generic::isa($sql)) {
$meta = ["isa" => "generic", "type" => null];
} else {
$meta = ["isa" => "generic", "type" => null];
}
}
}
const DEBUG_QUERIES = false;
function useStmt(\PDO $db, ?\PDOStatement &$stmt=null, ?string &$sql=null): bool {
if (static::DEBUG_QUERIES) { #XXX
error_log($this->sql);
//error_log(var_export($this->bindings, true));
}
if ($this->bindings !== null) {
$stmt = $db->prepare($this->sql);
foreach ($this->bindings as $name => $value) {
$this->verifixBindings($value);
$stmt->bindValue($name, $value);
}
return true;
} else {
$sql = $this->sql;
return false;
}
}
}

View File

@ -1,10 +0,0 @@
<?php
namespace nulib\db\pdo;
use nulib\db\_private\_create;
use nulib\db\_private\Tcreate;
class _query_create extends _query_base {
use Tcreate;
const SCHEMA = _create::SCHEMA;
}

View File

@ -1,10 +0,0 @@
<?php
namespace nulib\db\pdo;
use nulib\db\_private\_delete;
use nulib\db\_private\Tdelete;
class _query_delete extends _query_base {
use Tdelete;
const SCHEMA = _delete::SCHEMA;
}

View File

@ -1,10 +0,0 @@
<?php
namespace nulib\db\pdo;
use nulib\db\_private\_generic;
use nulib\db\_private\Tgeneric;
class _query_generic extends _query_base {
use Tgeneric;
const SCHEMA = _generic::SCHEMA;
}

View File

@ -1,10 +0,0 @@
<?php
namespace nulib\db\pdo;
use nulib\db\_private\_insert;
use nulib\db\_private\Tinsert;
class _query_insert extends _query_base {
use Tinsert;
const SCHEMA = _insert::SCHEMA;
}

View File

@ -1,10 +0,0 @@
<?php
namespace nulib\db\pdo;
use nulib\db\_private\_select;
use nulib\db\_private\Tselect;
class _query_select extends _query_base {
use Tselect;
const SCHEMA = _select::SCHEMA;
}

View File

@ -1,10 +0,0 @@
<?php
namespace nulib\db\pdo;
use nulib\db\_private\_update;
use nulib\db\_private\Tupdate;
class _query_update extends _query_base {
use Tupdate;
const SCHEMA = _update::SCHEMA;
}

294
php/src/db/pgsql/Pgsql.php Normal file
View File

@ -0,0 +1,294 @@
<?php
namespace nulib\db\pgsql;
use nulib\cl;
use nulib\db\_private\_config;
use nulib\db\_private\Tvalues;
use nulib\db\IDatabase;
use nulib\db\ITransactor;
use nulib\php\func;
use nulib\ValueException;
class Pgsql implements IDatabase {
use Tvalues;
static function with($pgsql, ?array $params=null): self {
if ($pgsql instanceof static) {
return $pgsql;
} elseif ($pgsql instanceof self) {
# recréer avec les mêmes paramètres
return new static(null, cl::merge([
"dbconn" => $pgsql->dbconn,
"options" => $pgsql->options,
"config" => $pgsql->config,
"migration" => $pgsql->migration,
], $params));
} else {
return new static($pgsql, $params);
}
}
protected const OPTIONS = [
"persistent" => true,
"force_new" => false,
"serial_support" => true,
];
const CONFIG = null;
const MIGRATION = null;
const params_SCHEMA = [
"dbconn" => ["array"],
"options" => ["?array|callable"],
"replace_config" => ["?array|callable"],
"config" => ["?array|callable"],
"migration" => ["?array|string|callable"],
"auto_open" => ["bool", true],
];
const dbconn_SCHEMA = [
"" => "?string",
"host" => "string",
"hostaddr" => "?string",
"port" => "?int",
"dbname" => "string",
"user" => "string",
"password" => "string",
"connect_timeout" => "?int",
"options" => "?string",
"sslmode" => "?string",
"service" => "?string",
];
protected const dbconn_MAP = [
"name" => "dbname",
"pass" => "password",
];
const options_SCHEMA = [
"persistent" => ["bool", self::OPTIONS["persistent"]],
"force_new" => ["bool", self::OPTIONS["force_new"]],
];
function __construct($dbconn=null, ?array $params=null) {
if ($dbconn !== null) {
if (!is_array($dbconn)) {
$dbconn = ["" => $dbconn];
#XXX à terme, il faudra interroger config
#$tmp = config::db($dbconn);
#if ($tmp !== null) $dbconn = $tmp;
#else $dbconn = ["" => $dbconn];
}
$params["dbconn"] = $dbconn;
}
# dbconn
$this->dbconn = $params["dbconn"] ?? null;
# options
$this->options = $params["options"] ?? static::OPTIONS;
# configuration
$config = $params["replace_config"] ?? null;
if ($config === null) {
$config = $params["config"] ?? static::CONFIG;
if (is_callable($config)) $config = [$config];
}
$this->config = $config;
# migrations
$this->migration = $params["migration"] ?? static::MIGRATION;
#
$defaultAutoOpen = self::params_SCHEMA["auto_open"][1];
if ($params["auto_open"] ?? $defaultAutoOpen) {
$this->open();
}
}
protected ?array $dbconn;
/** @var array|callable|null */
protected $options;
/** @var array|string|callable */
protected $config;
/** @var array|string|callable */
protected $migration;
/** @var resource */
protected $db = null;
function open(): self {
if ($this->db === null) {
$dbconn = $this->dbconn;
$connection_string = [$dbconn[""] ?? null];
unset($dbconn[""]);
foreach ($dbconn as $key => $value) {
if ($value === null) continue;
$value = strval($value);
if ($value === "" || preg_match("/[ '\\\\]/", $value)) {
$value = str_replace("\\", "\\\\", $value);
$value = str_replace("'", "\\'", $value);
$value = "'$value'";
}
$key = cl::get(self::dbconn_MAP, $key, $key);
$connection_string[] = "$key=$value";
}
$connection_string = implode(" ", array_filter($connection_string));
$options = $this->options;
if (is_callable($options)) {
$options = func::with($options)->bind($this, true)->invoke();
}
$forceNew = $options["force_new"] ?? false;
$flags = $forceNew? PGSQL_CONNECT_FORCE_NEW: 0;
if ($options["persistent"] ?? true) $db = pg_pconnect($connection_string, $flags);
else $db = pg_connect($connection_string, $flags);
if ($db === false) throw new PgsqlException("unable to connect");
$this->db = $db;
_config::with($this->config)->configure($this);
//_migration::with($this->migration)->migrate($this);
}
return $this;
}
function close(): self {
if ($this->db !== null) {
pg_close($this->db);
$this->db = null;
}
return $this;
}
protected function db() {
$this->open();
return $this->db;
}
function _exec(string $query): bool {
$result = pg_query($this->db(), $query);
if ($result === false) return false;
pg_free_result($result);
return true;
}
function getLastSerial() {
$db = $this->db();
$result = @pg_query($db, "select lastval()");
if ($result === false) return false;
$lastSerial = pg_fetch_row($result)[0];
pg_free_result($result);
return $lastSerial;
}
function exec($query, ?array $params=null) {
$db = $this->db();
$query = new _pgsqlQuery($query, $params);
$result = $query->_exec($db);
$serialSupport = $this->options["serial_support"] ?? true;
if ($serialSupport && $query->isInsert()) return $this->getLastSerial();
$affected_rows = pg_affected_rows($result);
pg_free_result($result);
return $affected_rows;
}
/** @var ITransactor[] */
protected ?array $transactors = null;
function willUpdate(...$transactors): self {
foreach ($transactors as $transactor) {
if ($transactor instanceof ITransactor) {
$this->transactors[] = $transactor;
$transactor->willUpdate();
} else {
throw ValueException::invalid_type($transactor, ITransactor::class);
}
}
return $this;
}
function inTransaction(?bool &$inerror=null): bool {
$status = pg_transaction_status($this->db());
if ($status === PGSQL_TRANSACTION_ACTIVE || $status === PGSQL_TRANSACTION_INTRANS) {
$inerror = false;
return true;
} elseif ($status === PGSQL_TRANSACTION_INERROR) {
$inerror = true;
return true;
} else {
return false;
}
}
function beginTransaction(?callable $func=null, bool $commit=true): void {
$this->_exec("begin");
if ($this->transactors !== null) {
foreach ($this->transactors as $transactor) {
$transactor->beginTransaction();
}
}
if ($func !== null) {
$commited = false;
try {
func::call($func, $this);
if ($commit) {
$this->commit();
$commited = true;
}
} finally {
if ($commit && !$commited) $this->rollback();
}
}
}
function commit(): void {
$this->_exec("commit");
if ($this->transactors !== null) {
foreach ($this->transactors as $transactor) {
$transactor->commit();
}
}
}
function rollback(): void {
$this->_exec("rollback");
if ($this->transactors !== null) {
foreach ($this->transactors as $transactor) {
$transactor->rollback();
}
}
}
function get($query, ?array $params=null, bool $entireRow=false) {
$db = $this->db();
$query = new _pgsqlQuery($query, $params);
$result = $query->_exec($db);
$row = pg_fetch_assoc($result);
pg_free_result($result);
if ($row === false) return null;
$this->verifixRow($row);
if ($entireRow) return $row;
else return cl::first($row);
}
function one($query, ?array $params=null): ?array {
return $this->get($query, $params, true);
}
function all($query, ?array $params=null, $primaryKeys=null): iterable {
$db = $this->db();
$query = new _pgsqlQuery($query, $params);
$result = $query->_exec($db);
$primaryKeys = cl::withn($primaryKeys);
while (($row = pg_fetch_assoc($result)) !== false) {
$this->verifixRow($row);
if ($primaryKeys !== null) {
$key = implode("-", cl::select($row, $primaryKeys));
yield $key => $row;
} else {
yield $row;
}
}
pg_free_result($result);
}
}

View File

@ -0,0 +1,16 @@
<?php
namespace nulib\db\pgsql;
use Exception;
use RuntimeException;
use SQLite3;
class PgsqlException extends RuntimeException {
static final function last_error($db): self {
return new static(pg_last_error($db));
}
static final function wrap(Exception $e): self {
return new static($e->getMessage(), $e->getCode(), $e);
}
}

View File

@ -0,0 +1,64 @@
<?php
namespace nulib\db\pgsql;
use nulib\cl;
use nulib\db\CapacitorChannel;
use nulib\db\CapacitorStorage;
class PgsqlStorage extends CapacitorStorage {
const SERDATA_DEFINITION = "text";
const SERSUM_DEFINITION = "varchar(40)";
const SERTS_DEFINITION = "timestamp";
function __construct($pgsql) {
$this->db = Pgsql::with($pgsql);
}
protected Pgsql $db;
function db(): Pgsql {
return $this->db;
}
const PRIMARY_KEY_DEFINITION = [
"id_" => "serial primary key",
];
function _getMigration(CapacitorChannel $channel): _pgsqlMigration {
return new _pgsqlMigration(cl::merge([
$this->_createSql($channel),
], $channel->getMigration()), $channel->getName());
}
function _getCreateSql(CapacitorChannel $channel): string {
$query = new _pgsqlQuery($this->_createSql($channel));
return self::format_sql($channel, $query->getSql());
}
protected function _addToChannelsSql(CapacitorChannel $channel): array {
return cl::merge(parent::_addToChannelsSql($channel), [
"suffix" => "on conflict (name) do nothing",
]);
}
function _exists(CapacitorChannel $channel): bool {
$tableName = $channel->getTableName();
if (($index = strpos($tableName, ".")) !== false) {
$schemaName = substr($tableName, 0, $index);
$tableName = substr($tableName, $index + 1);
} else {
$schemaName = "public";
}
return null !== $this->db->get([
"select tablename from pg_tables",
"where" => [
"schemaname" => $schemaName,
"tablename" => $tableName,
],
]);
}
function close(): void {
$this->db->close();
}
}

View File

@ -0,0 +1,24 @@
<?php
namespace nulib\db\pgsql;
use nulib\db\_private\_migration;
class _pgsqlMigration extends _migration {
static function with($migration): self {
if ($migration instanceof self) return $migration;
else return new static($migration);
}
protected function setMigrated(string $name, bool $done): void {
$this->db->exec([
"insert",
"into" => static::MIGRATION_TABLE,
"values" => [
"channel" => $this->channel,
"name" => $name,
"done" => $done? 1: 0,
],
"suffix" => "on conflict (channel, name) do update set done = :done",
]);
}
}

View File

@ -0,0 +1,44 @@
<?php
namespace nulib\db\pgsql;
use nulib\cv;
use nulib\db\_private\_base;
use nulib\db\_private\Tbindings;
use nulib\output\msg;
class _pgsqlQuery extends _base {
use Tbindings;
const DEBUG_QUERIES = false;
/**
* @return resource
*/
function _exec($db) {
$sql = $this->sql;
$bindings = $this->bindings;
if (static::DEBUG_QUERIES) {#XXX
msg::info($sql);
//msg::info(var_export($bindings, true));
}
if ($bindings !== null) {
# trier d'abord les champ par ordre de longueur, pour éviter les overlaps
$names = array_keys($bindings);
usort($names, function ($a, $b) {
return -cv::compare(strlen(strval($a)), strlen(strval($b)));
});
$bparams = [];
$number = 1;
foreach ($names as $name) {
$sql = str_replace(":$name", "\$$number", $sql);
$bparams[] = $bindings[$name];
$number++;
}
$result = pg_query_params($db, $sql, $bparams);
} else {
$result = pg_query($db, $sql);
}
if ($result === false) throw PgsqlException::last_error($db);
return $result;
}
}

View File

@ -3,10 +3,11 @@ namespace nulib\db\sqlite;
use Generator;
use nulib\cl;
use nulib\db\_private\_config;
use nulib\db\_private\Tvalues;
use nulib\db\IDatabase;
use nulib\db\ITransactor;
use nulib\php\nur_func;
use nulib\php\func;
use nulib\ValueException;
use SQLite3;
use SQLite3Result;
@ -29,7 +30,7 @@ class Sqlite implements IDatabase {
"encryption_key" => $sqlite->encryptionKey,
"allow_wal" => $sqlite->allowWal,
"config" => $sqlite->config,
"migrate" => $sqlite->migration,
"migration" => $sqlite->migration,
], $params));
} elseif (is_array($sqlite)) {
return new static(null, cl::merge($sqlite, $params));
@ -71,7 +72,7 @@ class Sqlite implements IDatabase {
const CONFIG = null;
const MIGRATE = null;
const MIGRATION = null;
const params_SCHEMA = [
"file" => ["string", ""],
@ -80,7 +81,7 @@ class Sqlite implements IDatabase {
"allow_wal" => ["?bool"],
"replace_config" => ["?array|callable"],
"config" => ["?array|callable"],
"migrate" => ["?array|string|callable"],
"migration" => ["?array|string|callable"],
"auto_open" => ["bool", true],
];
@ -108,7 +109,7 @@ class Sqlite implements IDatabase {
}
$this->config = $config;
# migrations
$this->migration = $params["migrate"] ?? static::MIGRATE;
$this->migration = $params["migration"] ?? static::MIGRATION;
#
$defaultAutoOpen = self::params_SCHEMA["auto_open"][1];
$this->inTransaction = false;
@ -149,7 +150,7 @@ class Sqlite implements IDatabase {
if ($this->db === null) {
$this->db = new SQLite3($this->file, $this->flags, $this->encryptionKey);
_config::with($this->config)->configure($this);
_migration::with($this->migration)->migrate($this);
_sqliteMigration::with($this->migration)->migrate($this);
$this->inTransaction = false;
}
return $this;
@ -180,15 +181,10 @@ class Sqlite implements IDatabase {
return $this->db()->exec($query);
}
private static function is_insert(?string $sql): bool {
if ($sql === null) return false;
return preg_match('/^\s*insert\b/i', $sql);
}
function exec($query, ?array $params=null) {
$db = $this->db();
$query = new _query_base($query, $params);
if ($query->useStmt($db, $stmt, $sql)) {
$query = new _sqliteQuery($query, $params);
if ($query->_use_stmt($db, $stmt, $sql)) {
try {
$result = $stmt->execute();
if ($result === false) return false;
@ -201,7 +197,7 @@ class Sqlite implements IDatabase {
} else {
$result = $db->exec($sql);
if ($result === false) return false;
if (self::is_insert($sql)) return $db->lastInsertRowID();
if ($query->isInsert()) return $db->lastInsertRowID();
else return $db->changes();
}
}
@ -237,7 +233,7 @@ class Sqlite implements IDatabase {
if ($func !== null) {
$commited = false;
try {
nur_func::call($func, $this);
func::call($func, $this);
if ($commit) {
$this->commit();
$commited = true;
@ -274,8 +270,8 @@ class Sqlite implements IDatabase {
function get($query, ?array $params=null, bool $entireRow=false) {
$db = $this->db();
$query = new _query_base($query, $params);
if ($query->useStmt($db, $stmt, $sql)) {
$query = new _sqliteQuery($query, $params);
if ($query->_use_stmt($db, $stmt, $sql)) {
try {
$result = $this->checkResult($stmt->execute());
try {
@ -300,7 +296,7 @@ class Sqlite implements IDatabase {
}
protected function _fetchResult(SQLite3Result $result, ?SQLite3Stmt $stmt=null, $primaryKeys=null): Generator {
if ($primaryKeys !== null) $primaryKeys = cl::with($primaryKeys);
$primaryKeys = cl::withn($primaryKeys);
try {
while (($row = $result->fetchArray(SQLITE3_ASSOC)) !== false) {
$this->verifixRow($row);
@ -317,14 +313,10 @@ class Sqlite implements IDatabase {
}
}
/**
* si $primaryKeys est fourni, le résultat est indexé sur la(es) colonne(s)
* spécifiée(s)
*/
function all($query, ?array $params=null, $primaryKeys=null): iterable {
$db = $this->db();
$query = new _query_base($query, $params);
if ($query->useStmt($db, $stmt, $sql)) {
$query = new _sqliteQuery($query, $params);
if ($query->_use_stmt($db, $stmt, $sql)) {
$result = $this->checkResult($stmt->execute());
return $this->_fetchResult($result, $stmt, $primaryKeys);
} else {

View File

@ -1,6 +1,7 @@
<?php
namespace nulib\db\sqlite;
use nulib\cl;
use nulib\db\CapacitorChannel;
use nulib\db\CapacitorStorage;
@ -12,8 +13,7 @@ class SqliteStorage extends CapacitorStorage {
$this->db = Sqlite::with($sqlite);
}
/** @var Sqlite */
protected $db;
protected Sqlite $db;
function db(): Sqlite {
return $this->db;
@ -23,8 +23,14 @@ class SqliteStorage extends CapacitorStorage {
"id_" => "integer primary key autoincrement",
];
function _getMigration(CapacitorChannel $channel): _sqliteMigration {
return new _sqliteMigration(cl::merge([
$this->_createSql($channel),
], $channel->getMigration()), $channel->getName());
}
function _getCreateSql(CapacitorChannel $channel): string {
$query = new _query_base($this->_createSql($channel));
$query = new _sqliteQuery($this->_createSql($channel));
return self::format_sql($channel, $query->getSql());
}
@ -39,54 +45,33 @@ class SqliteStorage extends CapacitorStorage {
}
function channelExists(string $name): bool {
$name = $this->db->get([
"select name from _channels",
return null !== $this->db->get([
"select name",
"from" => static::CHANNELS_TABLE,
"where" => ["name" => $name],
]);
return $name !== null;
}
protected function _addToChannelsSql(CapacitorChannel $channel): array {
$sql = parent::_addToChannelsSql($channel);
$sql[0] = "insert or ignore";
return $sql;
}
protected function _afterCreate(CapacitorChannel $channel): void {
$db = $this->db;
if (!$this->tableExists("_channels")) {
if (!$this->tableExists(static::CHANNELS_TABLE)) {
# ne pas créer si la table existe déjà, pour éviter d'avoir besoin d'un
# verrou en écriture
$db->exec([
"create table if not exists",
"table" => "_channels",
"cols" => [
"name" => "varchar primary key",
"table_name" => "varchar",
"class" => "varchar",
],
]);
$db->exec($this->_createChannelsSql());
}
if (!$this->channelExists($channel->getName())) {
# ne pas insérer si la ligne existe déjà, pour éviter d'avoir besoin d'un
# verrou en écriture
$db->exec([
"insert",
"into" => "_channels",
"values" => [
"name" => $channel->getName(),
"table_name" => $channel->getTableName(),
"class" => get_class($channel),
],
"suffix" => "on conflict do nothing",
]);
$db->exec($this->_addToChannelsSql($channel));
}
}
protected function _beforeReset(CapacitorChannel $channel): void {
$this->db->exec([
"delete",
"from" => "_channels",
"where" => [
"name" => $channel->getName(),
],
]);
}
function _exists(CapacitorChannel $channel): bool {
return $this->tableExists($channel->getTableName());
}

View File

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

View File

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

View File

@ -1,62 +0,0 @@
<?php
namespace nulib\db\sqlite;
use nulib\db\_private\_base;
use nulib\db\_private\Tbindings;
use nulib\output\msg;
use nulib\ValueException;
use SQLite3;
use SQLite3Stmt;
class _query_base extends _base {
use Tbindings;
protected static function verifix(&$sql, ?array &$bindinds=null, ?array &$meta=null): void {
if (is_array($sql)) {
$prefix = $sql[0] ?? null;
if ($prefix === null) {
throw new ValueException("requête invalide");
} elseif (_query_create::isa($prefix)) {
$sql = _query_create::parse($sql, $bindinds);
} elseif (_query_select::isa($prefix)) {
$sql = _query_select::parse($sql, $bindinds);
} elseif (_query_insert::isa($prefix)) {
$sql = _query_insert::parse($sql, $bindinds);
} elseif (_query_update::isa($prefix)) {
$sql = _query_update::parse($sql, $bindinds);
} elseif (_query_delete::isa($prefix)) {
$sql = _query_delete::parse($sql, $bindinds);
} elseif (_query_generic::isa($prefix)) {
$sql = _query_generic::parse($sql, $bindinds);
} else {
throw SqliteException::wrap(ValueException::invalid_kind($sql, "query"));
}
} elseif (!is_string($sql)) {
$sql = strval($sql);
}
}
const DEBUG_QUERIES = false;
function useStmt(SQLite3 $db, ?SQLite3Stmt &$stmt=null, ?string &$sql=null): bool {
if (static::DEBUG_QUERIES) msg::info($this->sql); #XXX
if ($this->bindings !== null) {
/** @var SQLite3Stmt $stmt */
$stmt = SqliteException::check($db, $db->prepare($this->sql));
$close = true;
try {
foreach ($this->bindings as $param => $value) {
$this->verifixBindings($value);
SqliteException::check($db, $stmt->bindValue($param, $value));
}
$close = false;
return true;
} finally {
if ($close) $stmt->close();
}
} else {
$sql = $this->sql;
return false;
}
}
}

View File

@ -1,10 +0,0 @@
<?php
namespace nulib\db\sqlite;
use nulib\db\_private\_create;
use nulib\db\_private\Tcreate;
class _query_create extends _query_base {
use Tcreate;
const SCHEMA = _create::SCHEMA;
}

View File

@ -1,10 +0,0 @@
<?php
namespace nulib\db\sqlite;
use nulib\db\_private\_delete;
use nulib\db\_private\Tdelete;
class _query_delete extends _query_base {
use Tdelete;
const SCHEMA = _delete::SCHEMA;
}

View File

@ -1,10 +0,0 @@
<?php
namespace nulib\db\sqlite;
use nulib\db\_private\_generic;
use nulib\db\_private\Tgeneric;
class _query_generic extends _query_base {
use Tgeneric;
const SCHEMA = _generic::SCHEMA;
}

View File

@ -1,10 +0,0 @@
<?php
namespace nulib\db\sqlite;
use nulib\db\_private\_insert;
use nulib\db\_private\Tinsert;
class _query_insert extends _query_base {
use Tinsert;
const SCHEMA = _insert::SCHEMA;
}

View File

@ -1,10 +0,0 @@
<?php
namespace nulib\db\sqlite;
use nulib\db\_private\_select;
use nulib\db\_private\Tselect;
class _query_select extends _query_base {
use Tselect;
const SCHEMA = _select::SCHEMA;
}

View File

@ -1,10 +0,0 @@
<?php
namespace nulib\db\sqlite;
use nulib\db\_private\_update;
use nulib\db\_private\Tupdate;
class _query_update extends _query_base {
use Tupdate;
const SCHEMA = _update::SCHEMA;
}

View File

@ -0,0 +1,23 @@
<?php
namespace nulib\db\sqlite;
use nulib\db\_private\_migration;
class _sqliteMigration extends _migration {
static function with($migration): self {
if ($migration instanceof self) return $migration;
else return new static($migration);
}
protected function setMigrated(string $name, bool $done): void {
$this->db->exec([
"insert or replace",
"into" => static::MIGRATION_TABLE,
"values" => [
"channel" => $this->channel,
"name" => $name,
"done" => $done? 1: 0,
],
]);
}
}

View File

@ -0,0 +1,46 @@
<?php
namespace nulib\db\sqlite;
use nulib\db\_private\_base;
use nulib\db\_private\_create;
use nulib\db\_private\_delete;
use nulib\db\_private\_generic;
use nulib\db\_private\_insert;
use nulib\db\_private\_select;
use nulib\db\_private\_update;
use nulib\db\_private\Tbindings;
use nulib\output\msg;
use nulib\ValueException;
use SQLite3;
use SQLite3Stmt;
class _sqliteQuery extends _base {
use Tbindings;
const DEBUG_QUERIES = false;
function _use_stmt(SQLite3 $db, ?SQLite3Stmt &$stmt=null, ?string &$sql=null): bool {
if (static::DEBUG_QUERIES) {#XXX
msg::info($this->sql);
//msg::info(var_export($this->bindings, true));
}
if ($this->bindings !== null) {
/** @var SQLite3Stmt $stmt */
$stmt = SqliteException::check($db, $db->prepare($this->sql));
$close = true;
try {
foreach ($this->bindings as $param => $value) {
$this->verifixBindings($value);
SqliteException::check($db, $stmt->bindValue($param, $value));
}
$close = false;
return true;
} finally {
if ($close) $stmt->close();
}
} else {
$sql = $this->sql;
return false;
}
}
}

View File

@ -2,7 +2,6 @@
namespace nulib\ext;
use Exception;
use nulib\ext\json\JsonException;
use nulib\file;
use nulib\os\IOException;

View File

@ -5,7 +5,7 @@ use DateTimeInterface;
use nulib\cl;
use nulib\file\TempStream;
use nulib\os\path;
use nulib\php\nur_func;
use nulib\php\func;
use nulib\php\time\DateTime;
use nulib\web\http;
@ -35,13 +35,8 @@ abstract class AbstractBuilder extends TempStream implements IBuilder {
$this->rows = $rows;
$this->index = 0;
$cookFunc = $params["cook_func"] ?? null;
$cookCtx = $cookArgs = null;
if ($cookFunc !== null) {
nur_func::ensure_func($cookFunc, $this, $cookArgs);
$cookCtx = nur_func::_prepare($cookFunc);
}
$this->cookCtx = $cookCtx;
$this->cookArgs = $cookArgs;
if ($cookFunc !== null) $cookFunc = func::with($cookFunc)->bind($this, true);
$this->cookFunc = $cookFunc;
$this->output = $params["output"] ?? static::OUTPUT;
$maxMemory = $params["max_memory"] ?? null;
$throwOnError = $params["throw_on_error"] ?? null;
@ -60,9 +55,7 @@ abstract class AbstractBuilder extends TempStream implements IBuilder {
protected ?string $output;
protected ?array $cookCtx;
protected ?array $cookArgs;
protected ?func $cookFunc;
protected function ensureHeaders(?array $row=null): void {
if ($this->headers !== null || !$this->useHeaders) return;
@ -87,9 +80,8 @@ abstract class AbstractBuilder extends TempStream implements IBuilder {
}
protected function cookRow(?array $row): ?array {
if ($this->cookCtx !== null) {
$args = cl::merge([$row], $this->cookArgs);
$row = nur_func::_call($this->cookCtx, $args);
if ($this->cookFunc !== null) {
$row = $this->cookFunc->prependArgs([$row])->invoke();
}
if ($row !== null) {
foreach ($row as &$col) {

View File

@ -2,7 +2,7 @@
namespace nulib\output;
use nulib\output\std\ProxyMessenger;
use nulib\php\nur_func;
use nulib\php\func;
/**
* Class msg: inscrire un message dans les logs ET l'afficher à l'utilisateur
@ -39,30 +39,21 @@ class msg extends _messenger {
if ($log !== null && $log !== false) {
if ($log instanceof IMessenger) log::set_messenger($log);
elseif (is_string($log)) log::set_messenger_class($log);
elseif (is_array($log)) {
nur_func::ensure_class($log, $args);
$log = nur_func::cons($log, $args);
}
else $log = func::call($log);
log::set_messenger($log);
$msgs[] = $log;
}
if ($console !== null && $console !== false) {
if ($console instanceof IMessenger) console::set_messenger($console);
elseif (is_string($console)) console::set_messenger_class($console);
elseif (is_array($console)) {
nur_func::ensure_class($console, $args);
$console = nur_func::cons($console, $args);
}
else $console = func::call($console);
console::set_messenger($console);
$msgs[] = $console;
}
if ($say !== null && $say !== false) {
if ($say instanceof IMessenger) say::set_messenger($say);
elseif (is_string($say)) say::set_messenger_class($say);
elseif (is_array($say)) {
nur_func::ensure_class($say, $args);
$say = nur_func::cons($say, $args);
}
else $say = func::call($say);
say::set_messenger($say);
$msgs[] = $say;
}

View File

@ -3,7 +3,7 @@ namespace nulib\php\content;
use Closure;
use nulib\cl;
use nulib\php\nur_func;
use nulib\php\func;
/**
* Class c: classe outil pour gérer du contenu
@ -62,8 +62,7 @@ class c {
# contenu dynamique: le contenu est la valeur de retour de la fonction
# ce contenu est rajouté à la suite après avoir été quoté avec self::q()
$func = $value;
nur_func::ensure_func($func, $object_or_class, $args);
$values = self::q(nur_func::call($func, ...$args));
$values = self::q(func::call($func));
self::add_static_content($dest, $values, $key, $seq);
continue;
}
@ -83,16 +82,7 @@ class c {
$arg = self::resolve($arg, $object_or_class, false);
if (!$array) $arg = $arg[0];
}; unset($arg);
if (nur_func::is_static($func)) {
nur_func::ensure_func($func, $object_or_class, $args);
$value = nur_func::call($func, ...$args);
} elseif (nur_func::is_class($func)) {
nur_func::fix_class_args($func, $args);
$value = nur_func::cons($func, ...$args);
} else {
nur_func::ensure_func($func, $object_or_class, $args);
$value = nur_func::call($func, ...$args);
}
$value = func::with($func, $args)->bind($object_or_class, true)->invoke();
}
}
if ($seq) $dest[] = $value;

View File

@ -3,13 +3,16 @@ namespace nulib\php;
use Closure;
use Exception;
use Generator;
use nulib\A;
use nulib\cl;
use nulib\cv;
use nulib\StateException;
use nulib\ValueException;
use ReflectionClass;
use ReflectionFunction;
use ReflectionMethod;
use Traversable;
/**
* Class func: outils pour appeler fonctions et méthodes dynamiquement
@ -58,10 +61,7 @@ class func {
* la fonction (ne pas uniquement faire une vérification syntaxique)
*/
static function verifix_function(&$func, bool $strict=true, ?string &$reason=null): bool {
if ($strict) {
$msg = var_export($func, true);
$reason = null;
}
if ($strict) $reason = null;
if ($func instanceof ReflectionFunction) return true;
if (is_string($func)) {
$c = false;
@ -82,11 +82,11 @@ class func {
if ($strict) {
$reason = null;
if (class_exists($f)) {
$reason = "$msg: is a class";
$reason = "$f: is a class";
return false;
}
if (!function_exists($f)) {
$reason = "$msg: function not found";
$reason = "$f: function not found";
return false;
}
}
@ -117,10 +117,7 @@ class func {
* faire une vérification syntaxique)
*/
static function verifix_class(&$func, bool $strict=true, ?string &$reason=null): bool {
if ($strict) {
$msg = var_export($func, true);
$reason = null;
}
if ($strict) $reason = null;
if ($func instanceof ReflectionClass) return true;
if (is_string($func)) {
$c = $func;
@ -138,12 +135,10 @@ class func {
if (self::_parse_static($c)) return false;
if (self::_parse_method($c)) return false;
if ($f !== false) return false;
if ($strict) {
if (!class_exists($c)) {
$reason = "$msg: class not found";
if ($strict && !class_exists($c)) {
$reason = "$c: class not found";
return false;
}
}
$func = [$c, false];
return true;
}
@ -207,10 +202,7 @@ class func {
* la méthode est liée (ne pas uniquement faire une vérification syntaxique)
*/
static function verifix_static(&$func, bool $strict=true, ?bool &$bound=null, ?string &$reason=null): bool {
if ($strict) {
$msg = var_export($func, true);
$reason = null;
}
if ($strict) $reason = null;
if ($func instanceof ReflectionMethod) {
$bound = false;
return true;
@ -265,18 +257,19 @@ class func {
return false;
}
if ($strict) {
[$c, $f] = $cf;
$reason = null;
if ($bound) {
if (!class_exists($c)) {
$reason = "$msg: class not found";
$reason = "$c: class not found";
return false;
}
if (!method_exists($c, $f)) {
$reason = "$msg: method not found";
$reason = "$c::$f: method not found";
return false;
}
} else {
$reason = "$msg: not bound";
$reason = "$c::$f: not bound";
}
}
$func = $cf;
@ -342,10 +335,7 @@ class func {
* la méthode est liée (ne pas uniquement faire une vérification syntaxique)
*/
static function verifix_method(&$func, bool $strict=true, ?bool &$bound=null, ?string &$reason=null): bool {
if ($strict) {
$msg = var_export($func, true);
$reason = null;
}
if ($strict) $reason = null;
if ($func instanceof ReflectionMethod) {
$bound = false;
return true;
@ -401,18 +391,19 @@ class func {
return false;
}
if ($strict) {
[$c, $f] = $cf;
$reason = null;
if ($bound) {
if (!is_object($c) && !class_exists($c)) {
$reason = "$msg: class not found";
$reason = "$c: class not found";
return false;
}
if (!method_exists($c, $f)) {
$reason = "$msg: method not found";
$reason = "$c::$f: method not found";
return false;
}
} else {
$reason = "$msg: not bound";
$reason = "$c::$f: not bound";
}
}
$func = $cf;
@ -446,7 +437,7 @@ class func {
return new ValueException($reason);
}
static function with($func, ?array $args=null, bool $strict=true): self {
private static function _with($func, ?array $args=null, bool $strict=true, ?string &$reason=null): ?self {
if (!is_array($func)) {
if ($func instanceof Closure) {
return new self(self::TYPE_CLOSURE, $func, $args);
@ -467,6 +458,12 @@ class func {
} elseif (self::verifix_static($func, $strict, $bound, $reason)) {
return new self(self::TYPE_STATIC, $func, $args, $bound, $reason);
}
return null;
}
static function with($func, ?array $args=null, bool $strict=true): self {
$func = self::_with($func, $args, $strict, $reason);
if ($func !== null) return $func;
throw self::not_a_callable($func, $reason);
}
@ -487,10 +484,45 @@ class func {
}
}
static function is_callable($func): bool {
$func = self::_with($func);
if ($func === null) return false;
if (!$func->isBound()) return false;
return $func->type !== self::TYPE_CLASS;
}
static function call($func, ...$args) {
return self::with($func)->invoke($args);
}
/**
* si $value est une fonction, l'appeler
* si $value ou le résultat de l'appel est un Traversable, le résoudre
* sinon retourner $value tel quel
*
* en définitive, la valeur de retour de cette fonction est soit un scalaire,
* soit un array, soit un objet qui n'est pas Traversable
* @return mixed
*/
static function get_value($value, ...$args) {
if ($value instanceof self) $value = $value->invoke($args);
elseif (is_callable($value)) $value = self::call($value, ...$args);
if ($value instanceof Traversable) $value = cl::all($value);
return $value;
}
/**
* si $value est une fonction, l'appeler
* si $value ou le résultat de l'appel est un Traversable, le retourner
* sinon retourner $value en tant qu'array
*/
static function get_iterable($value, ...$args): ?iterable {
if ($value instanceof self) $value = $value->invoke($args);
elseif (is_callable($value)) $value = self::call($value, ...$args);
if ($value instanceof Traversable) return $value;
else return cl::withn($value);
}
#############################################################################
protected function __construct(int $type, $func, ?array $args=null, bool $bound=false, ?string $reason=null) {
@ -561,6 +593,27 @@ class func {
protected int $maxArgs;
function replaceArgs(?array $args): self {
$this->prefixArgs = $args?? [];
return $this;
}
function prependArgs(?array $args, ?int $stripCount=null): self {
if ($stripCount !== null || $args !== null) {
array_splice($this->prefixArgs, 0, $stripCount ?? 0, $args);
}
return $this;
}
function appendArgs(?array $args, ?int $stripCount=null): self {
if ($stripCount !== null || $args !== null) {
$stripCount ??= 0;
if ($stripCount > 0) array_splice($this->prefixArgs, -$stripCount);
$this->prefixArgs = array_merge($this->prefixArgs, $args);
}
return $this;
}
protected function updateReflection($reflection): void {
$variadic = false;
$minArgs = $maxArgs = 0;
@ -596,11 +649,16 @@ class func {
else return $this->bound && $this->object !== null;
}
function bind($object): self {
function bind($object, bool $unlessAlreadyBound=false, bool $replace=false): self {
if ($this->type !== self::TYPE_METHOD) return $this;
if ($this->bound && $unlessAlreadyBound) return $this;
[$c, $f] = $this->func;
if ($this->reflection === null) {
if ($replace) {
$c = $object;
$this->func = [$c, $f];
$this->updateReflection(new ReflectionMethod($c, $f));
} elseif ($this->reflection === null) {
$this->func[0] = $c = $object;
$this->updateReflection(new ReflectionMethod($c, $f));
}

View File

@ -44,7 +44,7 @@ class mprop {
} catch (ReflectionException $e) {
return oprop::get($object, $property, $default);
}
return nur_func::call([$object, $m], $default);
return func::call([$object, $m], $default);
}
/** spécifier la valeur d'une propriété */
@ -60,7 +60,7 @@ class mprop {
} catch (ReflectionException $e) {
return oprop::_set($c, $object, $property, $value);
}
nur_func::call([$object, $m], $value);
func::call([$object, $m], $value);
return $value;
}

View File

@ -1,453 +0,0 @@
<?php
namespace nulib\php;
use Closure;
use nulib\cl;
use nulib\ref\php\ref_func;
use nulib\ValueException;
use ReflectionClass;
use ReflectionFunction;
use ReflectionMethod;
/**
* Class func: outils pour appeler des fonctions et méthodes dynamiquement
*/
class nur_func {
/**
* tester si $func est une chaine de la forme "XXX::method" XXX est une
* chaine quelconque éventuellement vide, ou un tableau de la forme ["method"]
* ou [anything, "method", ...]
*
* Avec la forme tableau, "method" ne doit pas contenir le caractère '\', pour
* pouvoir utiliser conjointement {@link is_class()}
*/
static final function is_static($func, bool $allowClass=false): bool {
if (is_string($func)) {
$pos = strpos($func, "::");
if ($pos === false) return false;
return $pos + 2 < strlen($func);
} elseif (is_array($func) && array_key_exists(0, $func)) {
$count = count($func);
if ($count == 1) {
if (!is_string($func[0]) || strlen($func[0]) == 0) return false;
if (strpos($func[0], "\\") !== false) return false;
return true;
} elseif ($count > 1) {
if (!array_key_exists(1, $func)) return false;
if (!is_string($func[1]) || strlen($func[1]) == 0) return false;
if (strpos($func[1], "\\") !== false) return false;
return true;
}
}
return false;
}
/**
* si $func est une chaine de la forme "::method" alors la remplacer par la
* chaine "$class::method"
*
* si $func est un tableau de la forme ["method"] ou [null, "method"], alors
* le remplacer par [$class, "method"]
*
* on assume que {@link is_static()}($func) retourne true
*
* @return bool true si la correction a été faite
*/
static final function fix_static(&$func, $class): bool {
if (is_object($class)) $class = get_class($class);
if (is_string($func) && substr($func, 0, 2) == "::") {
$func = "$class$func";
return true;
} elseif (is_array($func) && array_key_exists(0, $func)) {
$count = count($func);
if ($count == 1) {
$func = [$class, $func[0]];
return true;
} elseif ($count > 1 && $func[0] === null) {
$func[0] = $class;
return true;
}
}
return false;
}
/** tester si $method est une chaine de la forme "->method" */
private static function isam($method): bool {
return is_string($method)
&& strlen($method) > 2
&& substr($method, 0, 2) == "->";
}
/**
* tester si $func est une chaine de la forme "->method" ou un tableau de la
* forme ["->method", ...] ou [anything, "->method", ...]
*/
static final function is_method($func): bool {
if (is_string($func)) {
return self::isam($func);
} elseif (is_array($func) && array_key_exists(0, $func)) {
if (self::isam($func[0])) {
# ["->method", ...]
return true;
}
if (array_key_exists(1, $func) && self::isam($func[1])) {
# [anything, "->method", ...]
return true;
}
}
return false;
}
/**
* si $func est une chaine de la forme "->method" alors la remplacer par le
* tableau [$object, "method"]
*
* si $func est un tableau de la forme ["->method"] ou [anything, "->method"],
* alors le remplacer par [$object, "method"]
*
* @return bool true si la correction a été faite
*/
static final function fix_method(&$func, $object): bool {
if (!is_object($object)) return false;
if (is_string($func)) {
if (self::isam($func)) {
$func = [$object, substr($func, 2)];
return true;
}
} elseif (is_array($func) && array_key_exists(0, $func)) {
if (self::isam($func[0])) $func = array_merge([null], $func);
if (count($func) > 1 && array_key_exists(1, $func) && self::isam($func[1])) {
$func[0] = $object;
$func[1] = substr($func[1], 2);
return true;
}
}
return false;
}
/**
* si $func est un tableau de plus de 2 éléments, alors déplacer les éléments
* supplémentaires au début de $args. par exemple:
* ~~~
* $func = ["class", "method", "arg1", "arg2"];
* $args = ["arg3"];
* func::fix_args($func, $args)
* # $func === ["class", "method"]
* # $args === ["arg1", "arg2", "arg3"]
* ~~~
*
* @return bool true si la correction a été faite
*/
static final function fix_args(&$func, ?array &$args): bool {
if ($args === null) $args = [];
if (is_array($func) && count($func) > 2) {
$prefix_args = array_slice($func, 2);
$func = array_slice($func, 0, 2);
$args = array_merge($prefix_args, $args);
return true;
}
return false;
}
/**
* s'assurer que $func est un appel de méthode ou d'une méthode statique;
* et renseigner le cas échéant les arguments. si $func ne fait pas mention
* de la classe ou de l'objet, le renseigner avec $class_or_object.
*
* @return bool true si c'est une fonction valide. il ne reste plus qu'à
* l'appeler avec {@link call()}
*/
static final function check_func(&$func, $class_or_object, &$args=null): bool {
if ($func instanceof Closure) return true;
if (self::is_method($func)) {
# méthode
self::fix_method($func, $class_or_object);
self::fix_args($func, $args);
return true;
} elseif (self::is_static($func)) {
# méthode statique
self::fix_static($func, $class_or_object);
self::fix_args($func, $args);
return true;
}
return false;
}
/**
* Comme {@link check_func()} mais lance une exception si la fonction est
* invalide
*
* @throws ValueException si $func n'est pas une fonction ou une méthode valide
*/
static final function ensure_func(&$func, $class_or_object, &$args=null): void {
if (!self::check_func($func, $class_or_object, $args)) {
throw ValueException::invalid_type($func, "callable");
}
}
static final function _prepare($func): array {
$object = null;
if (is_callable($func)) {
if (is_array($func)) {
$rf = new ReflectionMethod(...$func);
$object = $func[0];
if (is_string($object)) $object = null;
} elseif ($func instanceof Closure) {
$rf = new ReflectionFunction($func);
} elseif (is_string($func) && strpos($func, "::") === false) {
$rf = new ReflectionFunction($func);
} else {
$rf = new ReflectionMethod($func);
}
} elseif ($func instanceof ReflectionMethod) {
$rf = $func;
} elseif ($func instanceof ReflectionFunction) {
$rf = $func;
} elseif (is_array($func) && count($func) == 2 && isset($func[0]) && isset($func[1])
&& ($func[1] instanceof ReflectionMethod || $func[1] instanceof ReflectionFunction)) {
$object = $func[0];
if (is_string($object)) $object = null;
$rf = $func[1];
} elseif (is_string($func) && strpos($func, "::") === false) {
$rf = new ReflectionFunction($func);
} else {
throw ValueException::invalid_type($func, "callable");
}
$minArgs = $rf->getNumberOfRequiredParameters();
$maxArgs = $rf->getNumberOfParameters();
$variadic = $rf->isVariadic();
return [$rf instanceof ReflectionMethod, $object, $rf, $minArgs, $maxArgs, $variadic];
}
static final function _fill(array $context, array &$args): void {
$minArgs = $context[3];
$maxArgs = $context[4];
$variadic = $context[5];
if (!$variadic) $args = array_slice($args, 0, $maxArgs);
while (count($args) < $minArgs) $args[] = null;
}
static final function _call($context, array $args) {
self::_fill($context, $args);
$use_object = $context[0];
$object = $context[1];
$method = $context[2];
if ($use_object) {
if (count($args) === 0) return $method->invoke($object);
else return $method->invokeArgs($object, $args);
} else {
if (count($args) === 0) return $method->invoke();
else return $method->invokeArgs($args);
}
}
/**
* Appeler la fonction spécifiée avec les arguments spécifiés.
* Adapter $args en fonction du nombre réel d'arguments de $func
*
* @param callable|ReflectionFunction|ReflectionMethod $func
*/
static final function call($func, ...$args) {
return self::_call(self::_prepare($func), $args);
}
/** remplacer $value par $func($value, ...$args) */
static final function apply(&$value, $func, ...$args): void {
if ($func !== null) {
if ($args) $args = array_merge([$value], $args);
else $args = [$value];
$value = self::call($func, ...$args);
}
}
const MASK_PS = ReflectionMethod::IS_PUBLIC | ReflectionMethod::IS_STATIC;
const MASK_P = ReflectionMethod::IS_PUBLIC;
const METHOD_PS = ReflectionMethod::IS_PUBLIC | ReflectionMethod::IS_STATIC;
const METHOD_P = ReflectionMethod::IS_PUBLIC;
private static function matches(string $name, array $includes, array $excludes): bool {
if ($includes) {
$matches = false;
foreach ($includes as $include) {
if (substr($include, 0, 1) == "/") {
# expression régulière
if (preg_match($include, $name)) {
$matches = true;
break;
}
} else {
# tester la présence de la sous-chaine
if (strpos($name, $include) !== false) {
$matches = true;
break;
}
}
}
if (!$matches) return false;
}
foreach ($excludes as $exclude) {
if (substr($exclude, 0, 1) == "/") {
# expression régulière
if (preg_match($exclude, $name)) return false;
} else {
# tester la présence de la sous-chaine
if (strpos($name, $exclude) !== false) return false;
}
}
return true;
}
/** @var Schema */
private static $call_all_params_schema;
/**
* retourner la liste des méthodes de $class_or_object qui correspondent au
* filtre $options. le filtre doit respecter le schéme {@link CALL_ALL_PARAMS_SCHEMA}
*/
static function get_all($class_or_object, $params=null): array {
Schema::nv($paramsv, $params, null
, self::$call_all_params_schema, ref_func::CALL_ALL_PARAMS_SCHEMA);
if (is_callable($class_or_object, true) && is_array($class_or_object)) {
# callable sous forme de tableau
$class_or_object = $class_or_object[0];
}
if (is_string($class_or_object)) {
# lister les méthodes publiques statiques de la classe
$mask = self::MASK_PS;
$expected = self::METHOD_PS;
$c = new ReflectionClass($class_or_object);
} elseif (is_object($class_or_object)) {
# lister les méthodes publiques de la classe
$c = new ReflectionClass($class_or_object);
$mask = $params["static_only"]? self::MASK_PS: self::MASK_P;
$expected = $params["static_only"]? self::METHOD_PS: self::METHOD_P;
} else {
throw new ValueException("$class_or_object: vous devez spécifier une classe ou un objet");
}
$prefix = $params["prefix"]; $prefixlen = strlen($prefix);
$args = $params["args"];
$includes = $params["include"];
$excludes = $params["exclude"];
$methods = [];
foreach ($c->getMethods() as $m) {
if (($m->getModifiers() & $mask) != $expected) continue;
$name = $m->getName();
if (substr($name, 0, $prefixlen) != $prefix) continue;
if (!self::matches($name, $includes, $excludes)) continue;
$methods[] = cl::merge([$class_or_object, $name], $args);
}
return $methods;
}
/**
* Appeler toutes les méthodes publiques de $object_or_class et retourner un
* tableau [$method_name => $return_value] des valeurs de retour.
*/
static final function call_all($class_or_object, $params=null): array {
$methods = self::get_all($class_or_object, $params);
$values = [];
foreach ($methods as $method) {
self::fix_args($method, $args);
$values[$method[1]] = self::call($method, ...$args);
}
return $values;
}
/**
* tester si $func est une chaine de la forme "XXX" XXX est une classe
* valide, ou un tableau de la forme ["XXX", ...]
*
* NB: il est possible d'avoir {@link is_static()} et {@link is_class()}
* vraies pour la même valeur. s'il faut supporter les deux cas, appeler
* {@link is_static()} d'abord, mais dans ce cas, on ne supporte que les
* classes qui sont dans un package
*/
static final function is_class($class): bool {
if (is_string($class)) {
return class_exists($class);
} elseif (is_array($class) && array_key_exists(0, $class)) {
return class_exists($class[0]);
}
return false;
}
/**
* en assumant que {@link is_class()} est vrai, si $class est un tableau de
* plus de 1 éléments, alors déplacer les éléments supplémentaires au début de
* $args. par exemple:
* ~~~
* $class = ["class", "arg1", "arg2"];
* $args = ["arg3"];
* func::fix_class_args($class, $args)
* # $class === "class"
* # $args === ["arg1", "arg2", "arg3"]
* ~~~
*
* @return bool true si la correction a été faite
*/
static final function fix_class_args(&$class, ?array &$args): bool {
if ($args === null) $args = [];
if (is_array($class)) {
if (count($class) > 1) {
$prefix_args = array_slice($class, 1);
$class = array_slice($class, 0, 1)[0];
$args = array_merge($prefix_args, $args);
} else {
$class = $class[0];
}
return true;
}
return false;
}
/**
* s'assurer que $class est une classe et renseigner le cas échéant les
* arguments.
*
* @return bool true si c'est une classe valide. il ne reste plus qu'à
* l'instancier avec {@link cons()}
*/
static final function check_class(&$class, &$args=null): bool {
if (self::is_class($class)) {
self::fix_class_args($class, $args);
return true;
}
return false;
}
/**
* Comme {@link check_class()} mais lance une exception si la classe est
* invalide
*
* @throws ValueException si $class n'est pas une classe valide
*/
static final function ensure_class(&$class, &$args=null): void {
if (!self::check_class($class, $args)) {
throw ValueException::invalid_type($class, "class");
}
}
/**
* Instancier la classe avec les arguments spécifiés.
* Adapter $args en fonction du nombre réel d'arguments du constructeur
*/
static final function cons(string $class, ...$args) {
$c = new ReflectionClass($class);
$rf = $c->getConstructor();
if ($rf === null) {
return $c->newInstance();
} else {
if (!$rf->isVariadic()) {
$minArgs = $rf->getNumberOfRequiredParameters();
$maxArgs = $rf->getNumberOfParameters();
$args = array_slice($args, 0, $maxArgs);
while (count($args) < $minArgs) {
$args[] = null;
}
}
return $c->newInstanceArgs($args);
}
}
}

View File

@ -0,0 +1,42 @@
<?php
namespace nulib\ref\schema;
class ref_input {
const ACCESS_AUTO = 0, ACCESS_KEY = 1, ACCESS_PROPERTY = 2;
const INPUT_PARAMS_SCHEMA = [
"access_type" => ["int", self::ACCESS_AUTO, "type d'accès: clé ou propriété"],
"allow_empty" => ["bool", true, "la chaine vide est-elle autorisée?"],
"allow_null" => ["bool", true, "la valeur null est-elle autorisée?"],
];
const ACCESS_PARAMS_SCHEMA = [
"allow_empty" => ["bool", true, "la chaine vide est-elle autorisée?"],
"allow_null" => ["bool", null, "la valeur null est-elle autorisée?"],
"allow_false" => ["bool", null, "la valeur false est-elle autorisée?"],
"protect_dest" => ["bool", null, "faut-il protéger la destination?"],
];
const VALUE_ACCESS_PARAMS_SCHEMA = [
"allow_null" => ["bool", false],
"allow_false" => ["bool", true],
"protect_dest" => ["bool", false],
];
const ARRAY_ACCESS_PARAMS_SCHEMA = [
"allow_null" => ["bool", true],
"allow_false" => ["bool", false],
"protect_dest" => ["bool", true],
"key_prefix" => ["?string", null, "préfixe des clés pour les méthodes ensureXxx()"],
"key_suffix" => ["?string", null, "suffixe des clés pour les méthodes ensureXxx()"],
];
const PROPERTY_ACCESS_PARAMS_SCHEMA = [
"allow_null" => ["bool", true],
"allow_false" => ["bool", false],
"protect_dest" => ["bool", true],
"key_prefix" => ["?string", null, "préfixe des clés pour les méthodes ensureXxx()"],
"key_suffix" => ["?string", null, "suffixe des clés pour les méthodes ensureXxx()"],
"map_names" => ["bool", true, "faut-il mapper les clés en camelCase?"]
];
}

View File

@ -4,20 +4,15 @@ namespace nulib\ref\schema;
class ref_schema {
/** @var array schéma des natures de schéma */
const NATURE_METASCHEMA = [
"nature" => ["string", null, "nature du schéma",
"pkey" => 0,
"allowed_values" => ["assoc", "list", "scalar"],
0 => ["string", null, "nature du schéma",
"allowed_values" => ["scalar", "assoc", "list"],
],
"title" => ["?string", null, "libellé de la valeur"],
"required" => ["bool", false, "la valeur est-elle requise?"],
"nullable" => ["?bool", null, "la valeur peut-elle être nulle?"],
"desc" => ["?content", null, "description de la valeur"],
"name" => ["?key", null, "identifiant de la valeur"],
"schema" => ["?array", null, "définition du schéma"],
"compute_func" => ["?callable", null, "fonction qui calcule les valeurs des champs computed"],
"validate_func" => ["?callable", null, "fonction qui vérifie la conformité de l'objet dans son ensemble"],
];
/** @var array meta-schema d'un schéma de nature scalaire */
const SCALAR_METASCHEMA = [
/** @var array meta-schéma d'une valeur */
const VALUE_METASCHEMA = [
"type" => ["array", null, "types possibles de la valeur", "required" => true],
"default" => [null, null, "valeur par défaut si la valeur n'existe pas"],
"title" => ["?string", null, "libellé de la valeur"],
@ -31,28 +26,61 @@ class ref_schema {
"messages" => ["?array", null, "messages à afficher en cas d'erreur d'analyse"],
"formatter_func" => ["?callable", null, "fonction qui formatte la valeur pour affichage"],
"format" => [null, null, "format à utiliser pour l'affichage"],
"" => ["array", "scalar", "nature du schéma",
"" => ["assoc", "schema" => self::NATURE_METASCHEMA],
"size" => ["?int", null, "nom de caractères ou de chiffres de la valeur"],
"precision" => ["?int", null, "nombre de chiffres après la virgule pour une valeur numérique flottante"],
"" => ["array", ["scalar"], "nature du schéma",
"schema" => self::NATURE_METASCHEMA,
],
"schema" => ["?array", null, "schéma de la valeur si c'est un array"],
"name" => ["?string", null, "identifiant de la valeur"],
"pkey" => ["?pkey", null, "chemin de clé de la valeur dans un tableau associatif"],
"header" => ["?string", null, "nom de l'en-tête s'il faut présenter cette donnée dans un tableau"],
"composite" => ["?bool", null, "ce champ fait-il partie d'une valeur composite?"],
"computed" => ["?bool", null, "ce champ est-il calculé? si oui, il n'est pas demandé en entrée ni validé"],
];
const MESSAGES = [
"missing" => "{key}: Vous devez spécifier cette valeur",
"unavailable" => "{key}: Vous devez spécifier cette valeur",
"null" => "{key}: cette valeur ne doit pas être nulle",
"empty" => "{key}: cette valeur ne doit pas être vide",
"invalid" => "{key}: {orig}: cette valeur est invalide",
"missing" => "Vous devez spécifier cette valeur",
"unavailable" => "Vous devez spécifier cette valeur",
"null" => "Cette valeur ne doit pas être nulle",
"empty" => "Cette valeur ne doit pas être vide",
"invalid" => "Cette valeur est invalide",
];
/** @var array meta-schema d'un schéma de nature associative */
const ASSOC_METASCHEMA = [
const PARAMS_SCHEMA = [
"analyze" => ["bool", true, "faut-il analyser la valeur?"],
"reanalyze" => ["bool", true, "faut-il forcer l'analyse de la valeur?"],
"normalize" => ["bool", true, "faut-il normaliser la valeur?"],
"renormalize" => ["bool", true, "faut-il forcer la normalisation de la valeur?"],
"throw" => ["bool", true, "faut-il lancer une exception en cas d'erreur?"],
//...ref_input::INPUT_PARAMS_SCHEMA,
];
/** @var array meta-schema d'un schéma de nature liste */
const LIST_METASCHEMA = [
/** @var array clés supplémentaires de schéma de la nature scalaire */
const SCALAR_NATURE_METASCHEMA = [
];
const SCALAR_PARAMS_SCHEMA = [
];
/** @var array clés supplémentaires de schéma de la nature associative */
const ASSOC_NATURE_METASCHEMA = [
"ensure_array" => ["bool", null, "faut-il s'assurer que le tableau destination est non nul?"],
"ensure_assoc" => ["bool", null, "faut-il s'assurer que le tableau destination est associatif?"],
"ensure_keys" => ["bool", null, "faut-il s'assurer que toutes les clés existent avec la valeur par défaut?"],
"ensure_order" => ["bool", null, "faut-il s'assurer que les clés soient dans l'ordre?"],
];
const ASSOC_PARAMS_SCHEMA = [
"ensure_array" => ["bool", false],
"ensure_assoc" => ["bool", true],
"ensure_keys" => ["bool", true],
"ensure_order" => ["bool", true],
];
/** @var array clés supplémentaires de schéma de la nature liste */
const LIST_NATURE_METASCHEMA = [
];
const LIST_PARAMS_SCHEMA = [
];
}

View File

@ -6,5 +6,6 @@ class ref_types {
"boolean" => "bool",
"integer" => "int",
"flt" => "float", "double" => "float", "dbl" => "float",
"func" => "callable", "function" => "callable",
];
}

View File

@ -242,6 +242,21 @@ class str {
return true;
}
/**
* vérifier si $s a le préfixe $prefix
* - si $prefix commence par /, c'est une expression régulière, et elle doit
* matcher $s
* - sinon $s doit commencer par la chaine $prefix
*/
static final function match_prefix(?string $s, ?string $prefix): bool {
if ($s === null || $prefix === null) return false;
if (substr($prefix, 0, 1) === "/") {
return preg_match($prefix, $s);
} else {
return self::_starts_with($prefix, $s);
}
}
/**
* ajouter $sep$prefix$text$suffix à $s si $text est non vide
*

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