Compare commits

...

80 Commits

Author SHA1 Message Date
b4e28ade02 modifs.mineures sans commentaires 2025-04-19 09:06:26 +04:00
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 /.composer.lock.runphp
.~lock*# .~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 ## Release 0.3.2p74 du 28/02/2025-20:28
(tech) Migration vers pman (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}" local length="${COLUMNS:-80}"
setx lsep=__complete "$prefix" "$length" - setx lsep=__complete "$prefix" "$length" -
tooenc "$COULEUR_BLEUE$lsep$COULEUR_NORMALE" recho "$COULEUR_BLEUE$lsep$COULEUR_NORMALE"
[ -n "$*" ] || return 0 [ -n "$*" ] || return 0
length=$((length - 1)) length=$((length - 1))
setx -a lines=echo "$1" setx -a lines=echo "$1"
for line in "${lines[@]}"; do for line in "${lines[@]}"; do
setx line=__complete "$prefix- $line" "$length" setx line=__complete "$prefix- $line" "$length"
tooenc "$COULEUR_BLEUE$line-$COULEUR_NORMALE" recho "$COULEUR_BLEUE$line-$COULEUR_NORMALE"
done done
tooenc "$COULEUR_BLEUE$lsep$COULEUR_NORMALE" recho "$COULEUR_BLEUE$lsep$COULEUR_NORMALE"
} }
function __etitle() { function __etitle() {
local -a lines; local maxlen=0 local -a lines; local maxlen=0
@ -23,10 +23,10 @@ function __etitle() {
setx -a lines=echo "$1" setx -a lines=echo "$1"
for line in "${lines[@]}"; do for line in "${lines[@]}"; do
[ ${#line} -gt $maxlen ] && maxlen=${#line} [ ${#line} -gt $maxlen ] && maxlen=${#line}
tooenc "${prefix}${COULEUR_BLEUE}T $line$COULEUR_NORMALE" recho "${prefix}${COULEUR_BLEUE}T $line$COULEUR_NORMALE"
done done
maxlen=$((maxlen + 2)) maxlen=$((maxlen + 2))
tooenc "${prefix}${COULEUR_BLEUE}T$(__complete "" $maxlen -)${COULEUR_NORMALE}" recho "${prefix}${COULEUR_BLEUE}T$(__complete "" $maxlen -)${COULEUR_NORMALE}"
} }
function __edesc() { function __edesc() {
local -a lines local -a lines
@ -34,7 +34,7 @@ function __edesc() {
setx -a lines=echo "$1" setx -a lines=echo "$1"
for line in "${lines[@]}"; do for line in "${lines[@]}"; do
tooenc "${prefix}${COULEUR_BLEUE}>${COULEUR_NORMALE} $line" recho "${prefix}${COULEUR_BLEUE}>${COULEUR_NORMALE} $line"
done done
} }
function __ebanner() { function __ebanner() {
@ -43,35 +43,35 @@ function __ebanner() {
local length="${COLUMNS:-80}" local length="${COLUMNS:-80}"
setx lsep=__complete "$prefix" "$length" = setx lsep=__complete "$prefix" "$length" =
tooenc "$COULEUR_ROUGE$lsep" recho "$COULEUR_ROUGE$lsep"
length=$((length - 1)) length=$((length - 1))
setx -a lines=echo "$1" setx -a lines=echo "$1"
for line in "" "${lines[@]}" ""; do for line in "" "${lines[@]}" ""; do
setx line=__complete "$prefix= $line" "$length" setx line=__complete "$prefix= $line" "$length"
tooenc "$line=" recho "$line="
done done
tooenc "$lsep$COULEUR_NORMALE" recho "$lsep$COULEUR_NORMALE"
} }
function __eimportant() { tooenc "$(__edate)$(__eindent0)${COULEUR_ROUGE}!${COULEUR_NORMALE} $(__eindent "$1" " ")"; } function __eimportant() { recho "$(__edate)$(__eindent0)${COULEUR_ROUGE}!${COULEUR_NORMALE} $(__eindent "$1" " ")"; }
function __eattention() { tooenc "$(__edate)$(__eindent0)${COULEUR_JAUNE}*${COULEUR_NORMALE} $(__eindent "$1" " ")"; } function __eattention() { recho "$(__edate)$(__eindent0)${COULEUR_JAUNE}*${COULEUR_NORMALE} $(__eindent "$1" " ")"; }
function __eerror() { tooenc "$(__edate)$(__eindent0)${COULEUR_ROUGE}E${COULEUR_NORMALE} $(__eindent "$1" " ")"; } function __eerror() { recho "$(__edate)$(__eindent0)${COULEUR_ROUGE}E${COULEUR_NORMALE} $(__eindent "$1" " ")"; }
function __ewarn() { tooenc "$(__edate)$(__eindent0)${COULEUR_JAUNE}W${COULEUR_NORMALE} $(__eindent "$1" " ")"; } function __ewarn() { recho "$(__edate)$(__eindent0)${COULEUR_JAUNE}W${COULEUR_NORMALE} $(__eindent "$1" " ")"; }
function __enote() { tooenc "$(__edate)$(__eindent0)${COULEUR_VERTE}N${COULEUR_NORMALE} $(__eindent "$1" " ")"; } function __enote() { recho "$(__edate)$(__eindent0)${COULEUR_VERTE}N${COULEUR_NORMALE} $(__eindent "$1" " ")"; }
function __einfo() { tooenc "$(__edate)$(__eindent0)${COULEUR_BLEUE}I${COULEUR_NORMALE} $(__eindent "$1" " ")"; } function __einfo() { recho "$(__edate)$(__eindent0)${COULEUR_BLEUE}I${COULEUR_NORMALE} $(__eindent "$1" " ")"; }
function __edebug() { tooenc "$(__edate)$(__eindent0)${COULEUR_BLANCHE}D${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 __estep() { recho "$(__edate)$(__eindent0)${COULEUR_BLANCHE}.${COULEUR_NORMALE} $(__eindent "$1" " ")"; }
function __estepe() { tooenc "$(__edate)$(__eindent0)${COULEUR_ROUGE}.${COULEUR_NORMALE} $(__eindent "$1" " ")"; } function __estepe() { recho "$(__edate)$(__eindent0)${COULEUR_ROUGE}.${COULEUR_NORMALE} $(__eindent "$1" " ")"; }
function __estepw() { tooenc "$(__edate)$(__eindent0)${COULEUR_JAUNE}.${COULEUR_NORMALE} $(__eindent "$1" " ")"; } function __estepw() { recho "$(__edate)$(__eindent0)${COULEUR_JAUNE}.${COULEUR_NORMALE} $(__eindent "$1" " ")"; }
function __estepn() { tooenc "$(__edate)$(__eindent0)${COULEUR_VERTE}.${COULEUR_NORMALE} $(__eindent "$1" " ")"; } function __estepn() { recho "$(__edate)$(__eindent0)${COULEUR_VERTE}.${COULEUR_NORMALE} $(__eindent "$1" " ")"; }
function __estepi() { tooenc "$(__edate)$(__eindent0)${COULEUR_BLEUE}.${COULEUR_NORMALE} $(__eindent "$1" " ")"; } function __estepi() { recho "$(__edate)$(__eindent0)${COULEUR_BLEUE}.${COULEUR_NORMALE} $(__eindent "$1" " ")"; }
function __estep_() { tooenc_ "$(__edate)$(__eindent0)${COULEUR_BLANCHE}.${COULEUR_NORMALE} $(__eindent "$1" " ")"; } function __estep_() { recho_ "$(__edate)$(__eindent0)${COULEUR_BLANCHE}.${COULEUR_NORMALE} $(__eindent "$1" " ")"; }
function __estepe_() { tooenc_ "$(__edate)$(__eindent0)${COULEUR_ROUGE}.${COULEUR_NORMALE} $(__eindent "$1" " ")"; } function __estepe_() { recho_ "$(__edate)$(__eindent0)${COULEUR_ROUGE}.${COULEUR_NORMALE} $(__eindent "$1" " ")"; }
function __estepw_() { tooenc_ "$(__edate)$(__eindent0)${COULEUR_JAUNE}.${COULEUR_NORMALE} $(__eindent "$1" " ")"; } function __estepw_() { recho_ "$(__edate)$(__eindent0)${COULEUR_JAUNE}.${COULEUR_NORMALE} $(__eindent "$1" " ")"; }
function __estepn_() { tooenc_ "$(__edate)$(__eindent0)${COULEUR_VERTE}.${COULEUR_NORMALE} $(__eindent "$1" " ")"; } function __estepn_() { recho_ "$(__edate)$(__eindent0)${COULEUR_VERTE}.${COULEUR_NORMALE} $(__eindent "$1" " ")"; }
function __estepi_() { tooenc_ "$(__edate)$(__eindent0)${COULEUR_BLEUE}.${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 __action() { recho "$(__edate)$(__eindent0)${COULEUR_BLANCHE}.${COULEUR_NORMALE} $(__eindent "$1" " ")"; }
function __asuccess() { tooenc "$(__edate)$(__eindent0)${COULEUR_VERTE}${COULEUR_NORMALE} $(__eindent "$1" " ")"; } function __asuccess() { recho "$(__edate)$(__eindent0)${COULEUR_VERTE}${COULEUR_NORMALE} $(__eindent "$1" " ")"; }
function __afailure() { tooenc "$(__edate)$(__eindent0)${COULEUR_ROUGE}${COULEUR_NORMALE} $(__eindent "$1" " ")"; } function __afailure() { recho "$(__edate)$(__eindent0)${COULEUR_ROUGE}${COULEUR_NORMALE} $(__eindent "$1" " ")"; }
function __adone() { tooenc "$(__edate)$(__eindent0)$(__eindent "$1")"; } function __adone() { recho "$(__edate)$(__eindent0)$(__eindent "$1")"; }

View File

@ -6,23 +6,23 @@ function __esection() {
local length="${COLUMNS:-80}" local length="${COLUMNS:-80}"
setx lsep=__complete "$prefix" "$length" - setx lsep=__complete "$prefix" "$length" -
tooenc "$lsep" recho "$lsep"
[ -n "$*" ] || return 0 [ -n "$*" ] || return 0
length=$((length - 1)) length=$((length - 1))
setx -a lines=echo "$1" setx -a lines=echo "$1"
for line in "${lines[@]}"; do for line in "${lines[@]}"; do
setx line=__complete "$prefix- $line" "$length" setx line=__complete "$prefix- $line" "$length"
tooenc "$line-" recho "$line-"
done done
tooenc "$lsep" recho "$lsep"
} }
function __etitle() { function __etitle() {
local p="TITLE: " i=" " local p="TITLE: " i=" "
tooenc "$(__edate)$(__eindent0)${p}$(__eindent "$1" "$i")" recho "$(__edate)$(__eindent0)${p}$(__eindent "$1" "$i")"
} }
function __edesc() { function __edesc() {
local p="DESC: " i=" " local p="DESC: " i=" "
tooenc "$(__edate)$(__eindent0)${p}$(__eindent "$1" "$i")" recho "$(__edate)$(__eindent0)${p}$(__eindent "$1" "$i")"
} }
function __ebanner() { function __ebanner() {
local -a lines local -a lines
@ -30,37 +30,37 @@ function __ebanner() {
local length="${COLUMNS:-80}" local length="${COLUMNS:-80}"
setx lsep=__complete "$prefix" "$length" = setx lsep=__complete "$prefix" "$length" =
tooenc "$lsep" recho "$lsep"
length=$((length - 1)) length=$((length - 1))
setx -a lines=echo "$1" setx -a lines=echo "$1"
for line in "" "${lines[@]}" ""; do for line in "" "${lines[@]}" ""; do
setx line=__complete "$prefix= $line" "$length" setx line=__complete "$prefix= $line" "$length"
tooenc "$line=" recho "$line="
done done
tooenc "$lsep" recho "$lsep"
} }
function __eimportant() { tooenc "$(__edate)$(__eindent0)IMPORTANT! $(__eindent "$1" " ")"; } function __eimportant() { recho "$(__edate)$(__eindent0)IMPORTANT! $(__eindent "$1" " ")"; }
function __eattention() { tooenc "$(__edate)$(__eindent0)ATTENTION! $(__eindent "$1" " ")"; } function __eattention() { recho "$(__edate)$(__eindent0)ATTENTION! $(__eindent "$1" " ")"; }
function __eerror() { tooenc "$(__edate)$(__eindent0)ERROR: $(__eindent "$1" " ")"; } function __eerror() { recho "$(__edate)$(__eindent0)ERROR: $(__eindent "$1" " ")"; }
function __ewarn() { tooenc "$(__edate)$(__eindent0)WARNING: $(__eindent "$1" " ")"; } function __ewarn() { recho "$(__edate)$(__eindent0)WARNING: $(__eindent "$1" " ")"; }
function __enote() { tooenc "$(__edate)$(__eindent0)NOTE: $(__eindent "$1" " ")"; } function __enote() { recho "$(__edate)$(__eindent0)NOTE: $(__eindent "$1" " ")"; }
function __einfo() { tooenc "$(__edate)$(__eindent0)INFO: $(__eindent "$1" " ")"; } function __einfo() { recho "$(__edate)$(__eindent0)INFO: $(__eindent "$1" " ")"; }
function __edebug() { tooenc "$(__edate)$(__eindent0)DEBUG: $(__eindent "$1" " ")"; } function __edebug() { recho "$(__edate)$(__eindent0)DEBUG: $(__eindent "$1" " ")"; }
function __eecho() { tooenc "$(__edate)$(__eindent0)$(__eindent "$1")"; } function __eecho() { recho "$(__edate)$(__eindent0)$(__eindent "$1")"; }
function __eecho_() { tooenc_ "$(__edate)$(__eindent0)$(__eindent "$1")"; } function __eecho_() { recho_ "$(__edate)$(__eindent0)$(__eindent "$1")"; }
function __estep() { tooenc "$(__edate)$(__eindent0). $(__eindent "$1" " ")"; } function __estep() { recho "$(__edate)$(__eindent0). $(__eindent "$1" " ")"; }
function __estepe() { tooenc "$(__edate)$(__eindent0).E $(__eindent "$1" " ")"; } function __estepe() { recho "$(__edate)$(__eindent0).E $(__eindent "$1" " ")"; }
function __estepw() { tooenc "$(__edate)$(__eindent0).W $(__eindent "$1" " ")"; } function __estepw() { recho "$(__edate)$(__eindent0).W $(__eindent "$1" " ")"; }
function __estepn() { tooenc "$(__edate)$(__eindent0).N $(__eindent "$1" " ")"; } function __estepn() { recho "$(__edate)$(__eindent0).N $(__eindent "$1" " ")"; }
function __estepi() { tooenc "$(__edate)$(__eindent0).I $(__eindent "$1" " ")"; } function __estepi() { recho "$(__edate)$(__eindent0).I $(__eindent "$1" " ")"; }
function __estep_() { tooenc_ "$(__edate)$(__eindent0). $(__eindent "$1" " ")"; } function __estep_() { recho_ "$(__edate)$(__eindent0). $(__eindent "$1" " ")"; }
function __estepe_() { tooenc_ "$(__edate)$(__eindent0).E $(__eindent "$1" " ")"; } function __estepe_() { recho_ "$(__edate)$(__eindent0).E $(__eindent "$1" " ")"; }
function __estepw_() { tooenc_ "$(__edate)$(__eindent0).W $(__eindent "$1" " ")"; } function __estepw_() { recho_ "$(__edate)$(__eindent0).W $(__eindent "$1" " ")"; }
function __estepn_() { tooenc_ "$(__edate)$(__eindent0).N $(__eindent "$1" " ")"; } function __estepn_() { recho_ "$(__edate)$(__eindent0).N $(__eindent "$1" " ")"; }
function __estepi_() { tooenc_ "$(__edate)$(__eindent0).I $(__eindent "$1" " ")"; } function __estepi_() { recho_ "$(__edate)$(__eindent0).I $(__eindent "$1" " ")"; }
function __action() { tooenc "$(__edate)$(__eindent0)ACTION: $(__eindent "$1" " ")"; } function __action() { recho "$(__edate)$(__eindent0)ACTION: $(__eindent "$1" " ")"; }
function __asuccess() { tooenc "$(__edate)$(__eindent0)(OK) $(__eindent "$1" " ")"; } function __asuccess() { recho "$(__edate)$(__eindent0)(OK) $(__eindent "$1" " ")"; }
function __afailure() { tooenc "$(__edate)$(__eindent0)(KO) $(__eindent "$1" " ")"; } function __afailure() { recho "$(__edate)$(__eindent0)(KO) $(__eindent "$1" " ")"; }
function __adone() { tooenc "$(__edate)$(__eindent0)$(__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." commence par ++, c'est une option avancée qui n'est pas affichée par défaut."
function parse_args() { function parse_args() {
eval "$NULIB__DISABLE_SET_X" eval "$NULIB__DISABLE_SET_X"
[ -n "$NULIB_ARGS_ONERROR_RETURN" ] && set_die_return
local __r= local __r=
local __DIE='[ -n "$NULIB_ARGS_ONERROR_RETURN" ] && return 1 || die'
if ! is_array args; then if ! is_array args; then
eerror "Invalid args definition: args must be defined" eerror "Invalid args definition: args must be defined"
@ -145,17 +145,33 @@ function parse_args() {
__r=1 __r=1
else else
__DEFS=("$@") __DEFS=("$@")
__parse_args || __r=1 __nulib_args_parse_args || __r=1
fi fi
eval "$NULIB__ENABLE_SET_X" eval "$NULIB__ENABLE_SET_X"
if [ -n "$__r" ]; then if [ -n "$__r" ]; then
eval "$__DIE" die || return
fi 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 ## tout d'abord, construire la liste des options
local __AUTOH=1 __AUTOHELP=1 # faut-il gérer automatiquement l'affichage de l'aide? 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 __ADVHELP # y a-t-il des options avancées?
local __popt __sopts __lopts local __popt __sopts __lopts
local -a __defs local -a __defs
@ -165,10 +181,10 @@ function __parse_args() {
+) __popt="$1"; shift; continue;; +) __popt="$1"; shift; continue;;
-) __popt="$1"; shift; continue;; -) __popt="$1"; shift; continue;;
-*) IFS=, read -a __defs <<<"$1"; shift;; -*) 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 esac
# est-ce que l'option prend un argument? # est-ce que l'option prend un argument?
local __def __witharg __valdesc local __def __longdef __witharg __valdesc
__witharg= __witharg=
for __def in "${__defs[@]}"; do for __def in "${__defs[@]}"; do
if [ "${__def%::*}" != "$__def" ]; then if [ "${__def%::*}" != "$__def" ]; then
@ -180,23 +196,36 @@ function __parse_args() {
# définitions __sopts et __lopts # définitions __sopts et __lopts
for __def in "${__defs[@]}"; do for __def in "${__defs[@]}"; do
__def="${__def%%:*}" __def="${__def%%:*}"
__longdef=
if [[ "$__def" == --* ]]; then if [[ "$__def" == --* ]]; then
# --longopt # --longopt
__def="${__def#--}" __def="${__def#--}"
__lopts="$__lopts${__lopts:+,}$__def$__witharg" __lopts="$__lopts${__lopts:+,}$__def$__witharg"
__longdef=1
elif [[ "$__def" == -* ]] && [ ${#__def} -eq 2 ]; then elif [[ "$__def" == -* ]] && [ ${#__def} -eq 2 ]; then
# -o # -o
__def="${__def#-}" __def="${__def#-}"
__sopts="$__sopts$__def$__witharg" __sopts="$__sopts$__def$__witharg"
[ "$__def" == h ] && __AUTOH= case "$__def" in
[ "$__def" == D ] && __AUTOD= h) __AUTOH=;;
L) __NOAUTOL+=("-$__def");;
Q|q|v|D) __NOAUTOV+=("-$__def");;
b|y|i) __NOAUTOI+=("-$__def");;
esac
else else
# -longopt ou longopt # -longopt ou longopt
__def="${__def#-}" __def="${__def#-}"
__lopts="$__lopts${__lopts:+,}$__def$__witharg" __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 fi
[ "$__def" == help -o "$__def" == help++ ] && __AUTOHELP=
[ "$__def" == debug ] && __AUTODEBUG=
done done
# sauter l'action # sauter l'action
shift shift
@ -209,8 +238,12 @@ function __parse_args() {
[ -n "$__AUTOH" ] && __sopts="${__sopts}h" [ -n "$__AUTOH" ] && __sopts="${__sopts}h"
[ -n "$__AUTOHELP" ] && __lopts="$__lopts${__lopts:+,}help,help++" [ -n "$__AUTOHELP" ] && __lopts="$__lopts${__lopts:+,}help,help++"
[ -n "$__AUTOD" ] && __sopts="${__sopts}D" __nulib_args_add_sopt __NOAUTOL L
[ -n "$__AUTODEBUG" ] && __lopts="$__lopts${__lopts:+,}debug" __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" __sopts="$__popt$__sopts"
local -a __getopt_args local -a __getopt_args
@ -222,7 +255,7 @@ function __parse_args() {
else else
# relancer pour avoir le message d'erreur # relancer pour avoir le message d'erreur
LANG=C getopt "${__getopt_args[@]}" 2>&1 1>/dev/null LANG=C getopt "${__getopt_args[@]}" 2>&1 1>/dev/null
eval "$__DIE" die || return
fi fi
## puis traiter les options ## puis traiter les options
@ -373,7 +406,7 @@ $prefix$usage"
fi fi
[[ "$1" == -* ]] || break [[ "$1" == -* ]] || break
option_="$1"; shift option_="$1"; shift
__parse_opt "$option_" __nulib_args_parse_opt "$option_"
if [ -n "$__witharg" ]; then if [ -n "$__witharg" ]; then
# l'option prend un argument # l'option prend un argument
value_="$1"; shift value_="$1"; shift
@ -387,7 +420,7 @@ $prefix$usage"
unset -f inc@ res@ add@ set@ showhelp@ unset -f inc@ res@ add@ set@ showhelp@
args=("$@") args=("$@")
} }
function __parse_opt() { function __nulib_args_parse_opt() {
# $1 est l'option spécifiée # $1 est l'option spécifiée
local option_="$1" local option_="$1"
set -- "${__DEFS[@]}" set -- "${__DEFS[@]}"
@ -460,27 +493,62 @@ function __parse_opt() {
[ -n "$__found" ] && return 0 [ -n "$__found" ] && return 0
done done
if [ -n "$__AUTOH" -a "$option_" == -h ]; then case "$option_" in
__action="showhelp@" -h)
if [ -n "$__AUTOH" ]; then
__action='showhelp@'
return 0 return 0
fi fi
;;
--help)
if [ -n "$__AUTOHELP" ]; then if [ -n "$__AUTOHELP" ]; then
if [ "$option_" == --help ]; then __action='showhelp@'
__action="showhelp@"
return 0
elif [ "$option_" == --help++ ]; then
__action="showhelp@ ++"
return 0 return 0
fi fi
fi ;;
if [ -n "$__AUTOD" -a "$option_" == -D ]; then --help++)
__action=set_debug if [ -n "$__AUTOHELP" ]; then
__action='showhelp@ ++'
return 0 return 0
fi fi
if [ -n "$__AUTODEBUG" -a "$option_" == --debug ]; then ;;
__action=set_debug -L)
if ! array_contains __NOAUTOL "$option_"; then
__action='elogto $value_'
return 0 return 0
fi 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 # 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 else
__eecho_ "Voulez-vous continuer?" 1>&2 __eecho_ "Voulez-vous continuer?" 1>&2
fi fi
tooenc_ " $prompt " 1>&2 echo_ " $prompt " 1>&2
uread r uread r
is_yes "${r:-$default}" is_yes "${r:-$default}"
else else
@ -198,17 +198,17 @@ function __rv_read() {
__eecho_ "Entrez la valeur" 1>&2 __eecho_ "Entrez la valeur" 1>&2
fi fi
if [ -n "$__rv_readline" ]; then if [ -n "$__rv_readline" ]; then
tooenc_ ": " 1>&2 echo_ ": " 1>&2
uread -e ${__rv_d:+-i"$__rv_d"} "${__rv_opts[@]}" __rv_r uread -e ${__rv_d:+-i"$__rv_d"} "${__rv_opts[@]}" __rv_r
else else
if [ -n "$__rv_d" ]; then if [ -n "$__rv_d" ]; then
if [ -n "$__rv_showdef" ]; then if [ -n "$__rv_showdef" ]; then
tooenc_ " [$__rv_d]" 1>&2 echo_ " [$__rv_d]" 1>&2
else else
tooenc_ " [****]" 1>&2 echo_ " [****]" 1>&2
fi fi
fi fi
tooenc_ ": " 1>&2 echo_ ": " 1>&2
uread "${__rv_opts[@]}" __rv_r uread "${__rv_opts[@]}" __rv_r
[ -n "$__rv_nl" ] && echo [ -n "$__rv_nl" ] && echo
fi fi
@ -217,6 +217,7 @@ function __rv_read() {
_setv "$__rv_v" "$__rv_r" _setv "$__rv_v" "$__rv_r"
return 0 return 0
fi fi
echo
done done
} }
@ -268,7 +269,7 @@ function simple_menu() {
else else
__eecho_ "Entrez le numéro de l'option choisie" 1>&2 __eecho_ "Entrez le numéro de l'option choisie" 1>&2
fi fi
tooenc_ ": " 1>&2 echo_ ": " 1>&2
uread __sm_choice uread __sm_choice
# Valeur par défaut # Valeur par défaut
@ -291,7 +292,7 @@ function simple_menu() {
let __sm_c=$__sm_c+1 let __sm_c=$__sm_c+1
if [ "$__sm_c" -eq 5 ]; then if [ "$__sm_c" -eq 5 ]; then
# sauter une ligne toutes les 4 tentatives # sauter une ligne toutes les 4 tentatives
tooenc "" 1>&2 echo 1>&2
__sm_c=0 __sm_c=0
fi fi
done done
@ -438,7 +439,7 @@ function __void_actions_menu() {
if [ $c -eq 0 ]; then if [ $c -eq 0 ]; then
[ -n "$title" ] && __etitle "$title" 1>&2 [ -n "$title" ] && __etitle "$title" 1>&2
__eecho_ "=== Actions disponibles: " 1>&2 __eecho_ "=== Actions disponibles: " 1>&2
tooenc "$action_title" 1>&2 recho "$action_title" 1>&2
fi fi
if [ -n "$actyc" ]; then if [ -n "$actyc" ]; then
__eecho_ "$actyc" 1>&2 __eecho_ "$actyc" 1>&2
@ -447,7 +448,7 @@ function __void_actions_menu() {
else else
__eecho_ "Entrez l'action à effectuer" 1>&2 __eecho_ "Entrez l'action à effectuer" 1>&2
fi fi
tooenc_ ": " 1>&2 echo_ ": " 1>&2
uread choice uread choice
if [ -z "$choice" -a -n "$default_action" ]; then if [ -z "$choice" -a -n "$default_action" ]; then
select_action="$default_action" select_action="$default_action"
@ -468,7 +469,7 @@ function __void_actions_menu() {
let c=$c+1 let c=$c+1
if [ $c -eq 5 ]; then if [ $c -eq 5 ]; then
# sauter une ligne toutes les 4 tentatives # sauter une ligne toutes les 4 tentatives
tooenc "" 1>&2 echo 1>&2
c=0 c=0
fi fi
done done
@ -484,21 +485,21 @@ function __options_actions_menu() {
i=1 i=1
for option in "${options[@]}"; do for option in "${options[@]}"; do
if [ "$option" == "$select_option" ]; then if [ "$option" == "$select_option" ]; then
tooenc "$i*- $option" 1>&2 echo "$i*- $option" 1>&2
else else
tooenc "$i - $option" 1>&2 echo "$i - $option" 1>&2
fi fi
let i=$i+1 let i=$i+1
done done
__estepn_ "Actions disponibles: " 1>&2 __estepn_ "Actions disponibles: " 1>&2
tooenc "$action_title" 1>&2 recho "$action_title" 1>&2
fi fi
if [ -n "$optyc" ]; then if [ -n "$optyc" ]; then
__eecho_ "$optyc" 1>&2 __eecho_ "$optyc" 1>&2
else else
__eecho_ "Entrez l'action et le numéro de l'option choisie" 1>&2 __eecho_ "Entrez l'action et le numéro de l'option choisie" 1>&2
fi fi
tooenc_ ": " 1>&2 echo_ ": " 1>&2
uread choice uread choice
# vérifier la saisie # vérifier la saisie
@ -572,7 +573,7 @@ function __options_actions_menu() {
let c=$c+1 let c=$c+1
if [ $c -eq 5 ]; then if [ $c -eq 5 ]; then
# sauter une ligne toutes les 4 tentatives # sauter une ligne toutes les 4 tentatives
tooenc "" 1>&2 echo "" 1>&2
c=0 c=0
fi fi
done 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 # $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 # qui vaut par défaut $NULIB_OUTPUT_ENCODING
local value="$1" to="${2:-$NULIB_OUTPUT_ENCODING}" local value="$1" to="${2:-$NULIB_OUTPUT_ENCODING}"
@ -93,9 +93,8 @@ function tooenc() {
iconv -f -utf-8 -t "$to" <<<"$value" iconv -f -utf-8 -t "$to" <<<"$value"
fi fi
} }
function uecho() { tooenc "$*"; }
function tooenc_() { function uecho_() {
# $1 étant une chaine encodée en utf-8, l'afficher sans passer à la ligne dans # $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 # l'encoding de sortie $2 qui vaut par défaut $NULIB_OUTPUT_ENCODING
local value="$1" to="${2:-$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" recho_ "$value" | iconv -f utf-8 -t "$to"
fi fi
} }
function uecho_() { tooenc_ "$*"; }
export NULIB_QUIETLOG export NULIB_QUIETLOG
export NULIB__TMPLOG export NULIB__TMPLOG
@ -210,7 +208,7 @@ function __eindent() {
# indenter les lignes de $1, sauf la première # indenter les lignes de $1, sauf la première
local -a lines; local line first=1 local -a lines; local line first=1
local indent="$(__eindent0)$2" local indent="$(__eindent0)$2"
setx -a lines=echo "$1" setx -a lines=recho "$1"
for line in "${lines[@]}"; do for line in "${lines[@]}"; do
if [ -n "$first" ]; then if [ -n "$first" ]; then
recho "$line" recho "$line"
@ -232,7 +230,11 @@ function __complete() {
} }
PRETTYOPTS=() PRETTYOPTS=()
function set_verbosity() { :;} function set_verbosity() {
case "$1" in
-D|--debug) NULIB_DEBUG=1;;
esac
}
function check_verbosity() { return 0; } function check_verbosity() { return 0; }
function get_verbosity_option() { :;} function get_verbosity_option() { :;}

View File

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

View File

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

View File

@ -108,11 +108,17 @@ function set_interaction() {
# set_interaction en fonction des arguments de la ligne de commande. A utiliser # set_interaction en fonction des arguments de la ligne de commande. A utiliser
# de cette manière: # de cette manière:
# parse_opts ... "${PRETTYOPTS[@]}" @ args -- ... # 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é" -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" -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" -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_error() { [ "$__verbosity" -ge 1 ]; }
function show_warn() { [ "$__verbosity" -ge 2 ]; } 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' Si \$2 n'est pas spécifié, on assume que \$1 est de la forme '.file.ext'
et \$2 vaudra alors 'file' 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" Ajouter file au tableau userfiles"
function template_copy_replace() { 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' Si \$2 n'est pas spécifié, on assume que \$1 est de la forme '.file.ext'
et \$2 vaudra alors 'file' 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" Ajouter file au tableau userfiles"
function template_copy_missing() { function template_copy_missing() {
@ -205,6 +207,18 @@ function template_generate_scripts() {
#etitle "sedscript" cat "$sedscript" #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() { function template_process_userfiles() {
local awkscript sedscript workfile userfile local awkscript sedscript workfile userfile
ac_set_tmpfile awkscript ac_set_tmpfile awkscript
@ -213,6 +227,7 @@ function template_process_userfiles() {
ac_set_tmpfile workfile ac_set_tmpfile workfile
for userfile in "${userfiles[@]}"; do for userfile in "${userfiles[@]}"; do
_template_can_process "$userfile" || continue
if cat "$userfile" | awk -f "$awkscript" | sed -rf "$sedscript" >"$workfile"; then if cat "$userfile" | awk -f "$awkscript" | sed -rf "$sedscript" >"$workfile"; then
if testdiff "$workfile" "$userfile"; then if testdiff "$workfile" "$userfile"; then
# n'écrire le fichier que s'il a changé # 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= Banner=
args=( args=(
"afficher divers messages avec les fonctions e*" "afficher divers messages avec les fonctions e*"
-D,--debug '$set_debug'
-d,--date NULIB_ELOG_DATE=1 -d,--date NULIB_ELOG_DATE=1
-m,--myname NULIB_ELOG_MYNAME=1 -m,--myname NULIB_ELOG_MYNAME=1
-n,--nc,--no-color '$__set_no_colors 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 #!/bin/bash
# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8 # -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8
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 fi
} }
function git_statuses() {
local cwd="$(pwd)" dir
for dir in "$@"; do
cd "$dir" || die
git_status --porcelain
cd "$cwd"
done
}
chdir= chdir=
all= all=
composer=
args=( args=(
"afficher l'état du dépôt" "afficher l'état du dépôt"
"[-d chdir] [-a patterns...] "[-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" 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" -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" -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[@]}" parse_args "$@"; set -- "${args[@]}"
@ -50,16 +61,23 @@ if [ -n "$all" ]; then
dirs+=("${dir%/.git}") dirs+=("${dir%/.git}")
done done
fi fi
setx cwd=pwd git_statuses "${dirs[@]}"
for dir in "${dirs[@]}"; do
cd "$dir" || die elif [ -n "$composer" ]; then
git_status --porcelain # projets dépendants
cd "$cwd" git_ensure_gitvcs
done 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 else
# répertoire courant uniquement # répertoire courant uniquement
setx toplevel=git_get_toplevel git_ensure_gitvcs
[ -n "$toplevel" ] && Cwd="$toplevel" Cwd="$(git_get_toplevel)"
args=() args=()
isatty || args+=(--porcelain) isatty || args+=(--porcelain)

130
bin/pdev
View File

@ -25,14 +25,18 @@ function ensure_branches() {
} }
function merge_action() { 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 "\ enote "\
Ce script va Ce script va
- fusionner la branche ${COULEUR_BLEUE}$SrcBranch${COULEUR_NORMALE} dans ${COULEUR_ROUGE}$DestBranch${COULEUR_NORMALE}${Push:+ - fusionner la branche ${COULEUR_BLEUE}$SrcBranch${COULEUR_NORMALE} dans ${COULEUR_ROUGE}$DestBranch${COULEUR_NORMALE}${Push:+
- pousser les branches modifiées}" - pousser les branches modifiées}"
ask_yesno "Voulez-vous continuer?" O || die 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 -a push_branches delete_branches
local hook
local comment= local comment=
local or_die=" || exit 1" local or_die=" || exit 1"
@ -42,12 +46,23 @@ Ce script va
# merge # merge
if [ -n "\$merge" ]; then if [ -n "\$merge" ]; then
esection "Fusionner la branche" esection "Fusionner la branche"
EOF
hook="BEFORE_MERGE_${SrcType^^}"; [ -n "${!hook}" ] && _scripta <<EOF
(
${!hook}
)$or_die
EOF EOF
_mscript_merge_branch _mscript_merge_branch
hook="AFTER_MERGE_${SrcType^^}"; [ -n "${!hook}" ] && _scripta <<EOF
(
${!hook}
)$or_die
EOF
_scripta <<EOF _scripta <<EOF
fi fi
EOF EOF
if [ -n "$ShouldDelete" ]; then
_scripta <<EOF _scripta <<EOF
################################################################################ ################################################################################
# delete # delete
@ -55,15 +70,26 @@ if [ -n "\$delete" ]; then
esection "Supprimer la branche" esection "Supprimer la branche"
EOF EOF
_mscript_delete_branch _mscript_delete_branch
hook="AFTER_DELETE_${SrcType^^}"; [ -n "${!hook}" ] && _scripta <<EOF
(
${!hook}
)$or_die
EOF
_scripta <<EOF _scripta <<EOF
fi fi
EOF EOF
fi
_scripta <<EOF _scripta <<EOF
################################################################################ ################################################################################
# push # push
if [ -n "\$push" ]; then if [ -n "\$push" ]; then
esection "Pousser les branches" esection "Pousser les branches"
EOF
hook="BEFORE_PUSH_${DestType^^}"; [ -n "${!hook}" ] && _scripta <<EOF
(
${!hook}
)$or_die
EOF EOF
_script_push_branches _script_push_branches
if [ ${#delete_branches[*]} -gt 0 ]; then if [ ${#delete_branches[*]} -gt 0 ]; then
@ -72,27 +98,42 @@ EOF
_script_push_branches _script_push_branches
_scripta <<<fi _scripta <<<fi
fi fi
hook="AFTER_PUSH_${DestType^^}"; [ -n "${!hook}" ] && _scripta <<EOF
(
${!hook}
)$or_die
EOF
_scripta <<EOF _scripta <<EOF
fi fi
EOF EOF
[ -n "$Delete" -o "$ForbidDelete" ] && Deleted=1 || Deleted= [ -n "$Delete" -o -z "$ShouldDelete" ] && Deleted=1 || Deleted=
[ -n "$Push" -o "$ForbidPush" ] && Pushed=1 || Pushed= [ -n "$ShouldDelete" -a -n "$Delete" ] && ShouldDelete=
if [ -n "$_NoRunScript" ]; then [ -n "$ShouldPush" -a -n "$Push" ] && ShouldPush=
einfo "Veuillez consulter le script $script pour le détail des opérations à effectuer" if [ -n "$_Fake" ]; then
cat "$script"
elif ! "$script" merge ${Delete:+delete} ${Push:+push}; then 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 die
elif [ -n "$Deleted" -a -n "$Pushed" ]; then elif [ -n "$Deleted" -a -n "$Push" ]; then
[ -n "$_KeepScript" ] || rm "$script" [ -n "$_KeepScript" ] || rm "$script"
[ -n "$AfterMerge" ] && eval "$AfterMerge"
else else
local cmd local msg="\
[ -n "$Deleted" ] || cmd="$cmd Le script $script a été lancé avec les arguments 'merge${Delete:+ delete}${Push:+ push}'
./$script delete" Vous pouvez consulter le script et/ou le relancer
[ -n "$Pushed" ] || cmd="$cmd ./$script${ShouldDelete:+ delete}${ShouldPush:+ push}"
./$script push" [ -n "$AfterMerge" ] && msg="$msg
einfo "Le script $script a été lancé avec les arguments 'merge${Delete:+ delete}${Push:+ push}' Il y a aussi les commandes supplémentaires suivantes:
Veuillez le consulter pour le détail des autres opérations à effectuer$cmd" ${AfterMerge//
/
}"
einfo "$msg"
fi fi
} }
@ -104,16 +145,27 @@ chdir=
Origin= Origin=
ConfigBranch= ConfigBranch=
ConfigFile= ConfigFile=
_Fake=
_KeepScript= _KeepScript=
_NoRunScript=
action=merge action=merge
TechMerge= TechMerge=
SquashMsg= SquashMsg=
[ -z "$PMAN_NO_PUSH" ] && Push=1 || Push= [ -z "$PMAN_NO_PUSH" ] && Push=1 || Push=
[ -z "$PMAN_NO_DELETE" ] && Delete=1 || Delete= [ -z "$PMAN_NO_DELETE" ] && Delete=1 || Delete=
AfterMerge=
args=( args=(
"fusionner la branche source dans la branche destination correspondante" "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" -d:,--chdir:BASEDIR chdir= "répertoire dans lequel se placer avant de lancer les opérations"
-O:,--origin Origin= "++\ -O:,--origin Origin= "++\
origine à partir de laquelle les branches distantes sont considérées" 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= "++\ -c:,--config-file:CONFIG ConfigFile= "++\
fichier de configuration des branches. cette option est prioritaire sur --config-branch 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" 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" --keep-script _KeepScript=1 "++option non documentée"
--no-run-script _NoRunScript=1 "++option non documentée"
-w,--show action=show "\ -w,--show action=show "\
lister les modifications qui seraient fusionnées dans la branche destination" lister les modifications qui seraient fusionnées dans la branche destination"
-b,--rebase action=rebase "\ -b,--rebase action=rebase "\
@ -146,6 +198,10 @@ ne pas supprimer la branche après la fusion dans la destination"
--delete Delete=1 "++\ --delete Delete=1 "++\
supprimer la branche après la fusion dans la destination. supprimer la branche après la fusion dans la destination.
c'est l'option par défaut" 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[@]}" parse_args "$@"; set -- "${args[@]}"
@ -155,13 +211,7 @@ load_branches all
load_config "$MYNAME" load_config "$MYNAME"
load_branches current "$1" load_branches current "$1"
ForbidPush= resolve_should_push quiet
[ -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=
# puis faire l'action que l'on nous demande # puis faire l'action que l'on nous demande
case "$action" in case "$action" in
@ -171,24 +221,26 @@ show)
show_action "$@" show_action "$@"
;; ;;
merge) merge)
ForbidDelete= ShouldDelete=1
case "$SrcType" in no_merge_msg="$SrcBranch: cette branche doit être fusionnée dans $DestBranch avec prel"
develop|release|hotfix) if [ "$SrcType" == develop ]; then
die "$SrcBranch: cette branche doit être fusionnée dans $DestBranch avec prel" [ -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 # n'autoriser la suppression que pour feature
[ "$SrcType" == feature ] || ForbidDelete=1 [ "$SrcType" == feature ] || ShouldDelete=
;; [ -z "$ShouldDelete" ] && Delete=
esac [ -z "$_Fake" ] && git_ensure_cleancheckout
[ -n "$ForbidDelete" ] && Delete= if array_contains LocalBranches "$SrcBranch"; then
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
ensure_branches ensure_branches
merge_action "$@" 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 fi
;; ;;
*) *)

189
bin/pman
View File

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

103
bin/prel
View File

@ -34,12 +34,19 @@ function ensure_branches() {
function create_release_action() { function create_release_action() {
if [ -n "$ReleaseBranch" ]; then if [ -n "$ReleaseBranch" ]; then
Version="${ReleaseBranch#$RELEASE}" Version="${ReleaseBranch#$RELEASE}"
Tag="$TAG_PREFIX$Version$TAG_SUFFIX"
merge_release_action "$@"; return $? merge_release_action "$@"; return $?
elif [ -n "$HotfixBranch" ]; then elif [ -n "$HotfixBranch" ]; then
Version="${HotfixBranch#$HOTFIX}" Version="${HotfixBranch#$HOTFIX}"
Tag="$TAG_PREFIX$Version$TAG_SUFFIX"
merge_hotfix_action "$@"; return $? merge_hotfix_action "$@"; return $?
fi 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 if [ -z "$Version" -a -n "$CurrentVersion" -a -f VERSION.txt ]; then
Version="$(<VERSION.txt)" Version="$(<VERSION.txt)"
Tag="$TAG_PREFIX$Version$TAG_SUFFIX" Tag="$TAG_PREFIX$Version$TAG_SUFFIX"
@ -67,7 +74,7 @@ Vous devrez:
fi fi
ask_yesno "Voulez-vous continuer?" O || die 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 -a push_branches push_tags
local comment= local comment=
local or_die=" || exit 1" local or_die=" || exit 1"
@ -78,8 +85,18 @@ Vous devrez:
# create # create
if [ -n "\$create" ]; then if [ -n "\$create" ]; then
esection "Création de la release" esection "Création de la release"
EOF
[ -n "$BEFORE_CREATE_RELEASE" ] && _scripta <<EOF
(
$BEFORE_CREATE_RELEASE
)$or_die
EOF EOF
_rscript_create_release_branch _rscript_create_release_branch
[ -n "$AFTER_CREATE_RELEASE" ] && _scripta <<EOF
(
$AFTER_CREATE_RELEASE
)$or_die
EOF
_scripta <<EOF _scripta <<EOF
fi fi
EOF EOF
@ -89,10 +106,20 @@ EOF
# merge # merge
if [ -n "\$merge" ]; then if [ -n "\$merge" ]; then
esection "Fusionner la release" esection "Fusionner la release"
EOF
[ -n "$BEFORE_MERGE_RELEASE" ] && _scripta <<EOF
(
$BEFORE_MERGE_RELEASE
)$or_die
EOF EOF
_rscript_merge_release_branch "$DestBranch" "$Tag" _rscript_merge_release_branch "$DestBranch" "$Tag"
_rscript_merge_release_branch "$SrcBranch" _rscript_merge_release_branch "$SrcBranch"
_rscript_delete_release_branch _rscript_delete_release_branch
[ -n "$AFTER_MERGE_RELEASE" ] && _scripta <<EOF
(
$AFTER_MERGE_RELEASE
)$or_die
EOF
_scripta <<EOF _scripta <<EOF
fi fi
EOF EOF
@ -102,30 +129,42 @@ EOF
# push # push
if [ -n "\$push" ]; then if [ -n "\$push" ]; then
esection "Pousser branches et tags" esection "Pousser branches et tags"
EOF
[ -n "$BEFORE_PUSH_RELEASE" ] && _scripta <<EOF
(
$BEFORE_PUSH_RELEASE
)$or_die
EOF EOF
_script_push_branches _script_push_branches
_script_push_tags _script_push_tags
[ -n "$AFTER_PUSH_RELEASE" ] && _scripta <<EOF
(
$AFTER_PUSH_RELEASE
)$or_die
EOF
_scripta <<EOF _scripta <<EOF
fi fi
EOF EOF
[ -n "$Merge" ] && Merged=1 || Merged= [ -z "$ManualRelease" -a -n "$Merge" ] && ShouldMerge= || ShouldMerge=1
[ -n "$Push" -o "$ForbidPush" ] && Pushed=1 || Pushed= [ -n "$ShouldPush" -a -n "$Push" ] && ShouldPush=
if [ -n "$_NoRunScript" ]; then if [ -n "$_Fake" ]; then
einfo "Veuillez consulter le script $script pour le détail des opérations à effectuer" cat "$script"
elif ! "$script" create ${Merge:+merge} ${Push:+push}; then 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 die
elif [ -n "$Merged" -a -n "$Pushed" ]; then elif [ -n "$Merge" -a -n "$Push" ]; then
[ -n "$_KeepScript" ] || rm "$script" [ -n "$_KeepScript" ] || rm "$script"
else else
local cmd einfo "\
[ -n "$Merged" ] || cmd="$cmd Le script $script a été lancé avec les arguments 'create${Merge:+ merge}${Push:+ push}'
./$script merge" Vous pouvez consulter le script et/ou le relancer
[ -n "$Pushed" ] || cmd="$cmd ./$script${ShouldMerge:+ merge}${ShouldPush:+ push}"
./$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"
fi fi
} }
@ -152,8 +191,8 @@ chdir=
Origin= Origin=
ConfigBranch= ConfigBranch=
ConfigFile= ConfigFile=
_Fake=
_KeepScript= _KeepScript=
_NoRunScript=
action=release action=release
[ -z "$PMAN_NO_MERGE" ] && Merge=1 || Merge= [ -z "$PMAN_NO_MERGE" ] && Merge=1 || Merge=
[ -z "$PMAN_NO_PUSH" ] && Push=1 || Push= [ -z "$PMAN_NO_PUSH" ] && Push=1 || Push=
@ -162,7 +201,17 @@ CurrentVersion=
ForceCreate= ForceCreate=
args=( args=(
"faire une nouvelle release à partir de la branche source" "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" -d:,--chdir:BASEDIR chdir= "répertoire dans lequel se placer avant de lancer les opérations"
-O:,--origin Origin= "++\ -O:,--origin Origin= "++\
origine à partir de laquelle les branches distantes sont considérées" 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= "++\ -c:,--config-file:CONFIG ConfigFile= "++\
fichier de configuration des branches. cette option est prioritaire sur --config-branch 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" 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" --keep-script _KeepScript=1 "++option non documentée"
--no-run-script _NoRunScript=1 "++option non documentée"
-w,--show action=show "\ -w,--show action=show "\
lister les modifications qui seraient intégrées dans la release" lister les modifications qui seraient intégrées dans la release"
--release action=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 "++\ --push Push=1 "++\
pousser les branches vers leur origine après la création de la release. pousser les branches vers leur origine après la création de la release.
c'est l'option par défaut" c'est l'option par défaut"
-v:,--version Version= "\ -v:,--version:VERSION Version= "\
spécifier la version de la release à créer" spécifier la version de la release à créer"
-C,--current-version CurrentVersion=1 "++\ -C,--current-version CurrentVersion=1 "++\
si aucune version n'est spécifiée, prendre la version présente dans le fichier VERSION.txt" 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à" forcer la création de la release même si le tag correspond à la version existe déjà"
) )
parse_args "$@"; set -- "${args[@]}" parse_args "$@"; set -- "${args[@]}"
@ -203,19 +252,11 @@ load_branches all
load_config "$MYNAME" load_config "$MYNAME"
load_branches current "$1"; shift load_branches current "$1"; shift
if [ -n "$Merge" -a -n "$NOAUTO" ]; then [ -n "$Merge" -a -n "$NOAUTO" ] && ManualRelease=1 || ManualRelease=
ewarn "L'option --no-merge a été forcée puisque ce dépôt ne supporte pas les releases automatiques" [ -n "$ManualRelease" ] && Merge=
Merge=
fi
[ -z "$Merge" ] && Push= [ -z "$Merge" ] && Push=
ForbidPush= resolve_should_push quiet
[ -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=
# puis faire l'action que l'on nous demande # puis faire l'action que l'on nous demande
case "$action" in case "$action" in
@ -225,7 +266,7 @@ show)
show_action "$@" show_action "$@"
;; ;;
release) release)
git_ensure_cleancheckout [ -z "$_Fake" ] && git_ensure_cleancheckout
ensure_branches ensure_branches
case "$SrcType" in case "$SrcType" in
release) merge_release_action "$@";; 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-posix": "*",
"ext-pcntl": "*", "ext-pcntl": "*",
"ext-curl": "*", "ext-curl": "*",
"ext-pdo": "*",
"ext-pgsql": "*",
"ext-sqlite3": "*" "ext-sqlite3": "*"
}, },
"autoload": { "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", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "ab280aa4a5f5c83fa488537530b29759", "content-hash": "a8b9dc80255663640bda855729ef2d47",
"packages": [ "packages": [
{ {
"name": "symfony/deprecation-contracts", "name": "symfony/deprecation-contracts",
@ -301,16 +301,16 @@
}, },
{ {
"name": "myclabs/deep-copy", "name": "myclabs/deep-copy",
"version": "1.12.1", "version": "1.13.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/myclabs/DeepCopy.git", "url": "https://github.com/myclabs/DeepCopy.git",
"reference": "123267b2c49fbf30d78a7b2d333f6be754b94845" "reference": "024473a478be9df5fdaca2c793f2232fe788e414"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/123267b2c49fbf30d78a7b2d333f6be754b94845", "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/024473a478be9df5fdaca2c793f2232fe788e414",
"reference": "123267b2c49fbf30d78a7b2d333f6be754b94845", "reference": "024473a478be9df5fdaca2c793f2232fe788e414",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -349,7 +349,7 @@
], ],
"support": { "support": {
"issues": "https://github.com/myclabs/DeepCopy/issues", "issues": "https://github.com/myclabs/DeepCopy/issues",
"source": "https://github.com/myclabs/DeepCopy/tree/1.12.1" "source": "https://github.com/myclabs/DeepCopy/tree/1.13.0"
}, },
"funding": [ "funding": [
{ {
@ -357,7 +357,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2024-11-08T17:47:46+00:00" "time": "2025-02-12T12:17:51+00:00"
}, },
{ {
"name": "nikic/php-parser", "name": "nikic/php-parser",
@ -423,13 +423,19 @@
"source": { "source": {
"type": "git", "type": "git",
"url": "https://git.univ-reunion.fr/sda-php/nulib-tests.git", "url": "https://git.univ-reunion.fr/sda-php/nulib-tests.git",
"reference": "9b5c9c295c3dee6fc02ccddbd8a70bca797c8045" "reference": "8f641d9a7cf6aba1453cb42ebd15951aa7002e1b"
}, },
"require": { "require": {
"php": ">=7.3", "php": "^7.3 || 8.0.*",
"phpunit/phpunit": "^9" "phpunit/phpunit": "^9"
}, },
"type": "library", "type": "library",
"extra": {
"branch-alias": {
"dev-pu9": "7.3.x-dev",
"dev-pu10": "8.1.x-dev"
}
},
"autoload": { "autoload": {
"psr-4": { "psr-4": {
"nulib\\tests\\": "src" "nulib\\tests\\": "src"
@ -447,7 +453,7 @@
} }
], ],
"description": "fonctions et classes pour les tests", "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", "name": "phar-io/manifest",
@ -2015,7 +2021,10 @@
"platform-dev": { "platform-dev": {
"ext-posix": "*", "ext-posix": "*",
"ext-pcntl": "*", "ext-pcntl": "*",
"ext-curl": "*" "ext-curl": "*",
"ext-pdo": "*",
"ext-pgsql": "*",
"ext-sqlite3": "*"
}, },
"plugin-api-version": "2.2.0" "plugin-api-version": "2.2.0"
} }

View File

@ -25,7 +25,7 @@ COPY --from=builder /src/su-exec/su-exec /g/
RUN /g/build RUN /g/build
COPY --from=php /g/ /g/ 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 EXPOSE 80 443
ENTRYPOINT ["/g/entrypoint"] ENTRYPOINT ["/g/entrypoint"]

View File

@ -34,7 +34,7 @@ COPY --from=legacytools /g/ /g/
RUN /g/build nutools RUN /g/build nutools
COPY --from=php /g/ /g/ 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=instantclient /g/ /g/
COPY --from=builder /opt/oracle/ /opt/oracle/ COPY --from=builder /opt/oracle/ /opt/oracle/

View File

@ -15,9 +15,9 @@ class StateException extends LogicException {
return new static($prefix.$message); 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"; $message = "unexpected state";
if ($prefix) $prefix = "$prefix: "; if ($suffix) $suffix = ": $suffix";
return new static($prefix.$message); return new static($message.$suffix);
} }
} }

View File

@ -2,7 +2,7 @@
namespace nulib; namespace nulib;
use ArrayAccess; use ArrayAccess;
use nulib\php\nur_func; use nulib\php\func;
use Traversable; 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 = []; $result = [];
if ($array !== null) { if ($array !== null) {
$ctx = nur_func::_prepare($callback); $func = func::with($func);
foreach ($array as $key => $value) { foreach ($array as $key => $value) {
$result[$key] = nur_func::_call($ctx, [$value, $key]); $result[$key] = $func->invoke([$value, $key]);
} }
} }
return $result; 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 * retourner le tableau $array en "renommant" les clés selon le tableau
* $mappings qui contient des associations de la forme [$from => $to] * $mappings qui contient des associations de la forme [$from => $to]

View File

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

View File

@ -17,6 +17,8 @@ class CapacitorChannel {
const PRIMARY_KEYS = null; const PRIMARY_KEYS = null;
const MIGRATION = null;
const MANAGE_TRANSACTIONS = true; const MANAGE_TRANSACTIONS = true;
const EACH_COMMIT_THRESHOLD = 100; const EACH_COMMIT_THRESHOLD = 100;
@ -63,15 +65,29 @@ class CapacitorChannel {
$this->created = false; $this->created = false;
$columnDefinitions = cl::withn(static::COLUMN_DEFINITIONS); $columnDefinitions = cl::withn(static::COLUMN_DEFINITIONS);
$primaryKeys = cl::withn(static::PRIMARY_KEYS); $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; $index = 0;
foreach ($columnDefinitions as $col => $def) { foreach ($columnDefinitions as $col => $def) {
if ($col === $index) { if ($col === $index) {
# si définition séquentielle, seules les définitions de clé
# primaires sont supportées
$index++; $index++;
if (preg_match('/\bprimary\s+key\s+\((.+)\)/i', $def, $ms)) { if (preg_match('/\bprimary\s+key\s+\((.+)\)/i', $def, $ms)) {
$primaryKeys = preg_split('/\s*,\s*/', trim($ms[1])); $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 { } 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)) { if (preg_match('/\bprimary\s+key\b/i', $def)) {
$primaryKeys[] = $col; $primaryKeys[] = $col;
} }
@ -80,6 +96,7 @@ class CapacitorChannel {
} }
$this->columnDefinitions = $columnDefinitions; $this->columnDefinitions = $columnDefinitions;
$this->primaryKeys = $primaryKeys; $this->primaryKeys = $primaryKeys;
$this->migration = $migration;
} }
protected string $name; protected string $name;
@ -192,6 +209,12 @@ class CapacitorChannel {
return $this->columnDefinitions; return $this->columnDefinitions;
} }
protected ?array $migration;
function getMigration(): ?array {
return $this->migration;
}
protected ?array $primaryKeys; protected ?array $primaryKeys;
function getPrimaryKeys(): ?array { function getPrimaryKeys(): ?array {
@ -245,9 +268,6 @@ class CapacitorChannel {
return $serial !== null? unserialize($serial): null; return $serial !== null? unserialize($serial): null;
} }
const SERIAL_DEFINITION = "mediumtext";
const SUM_DEFINITION = "varchar(40)";
final function sum(?string $serial, $value=null): ?string { final function sum(?string $serial, $value=null): ?string {
if ($serial === null) $serial = $this->serialize($value); if ($serial === null) $serial = $this->serialize($value);
return $serial !== null? sha1($serial): null; return $serial !== null? sha1($serial): null;

View File

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

View File

@ -15,5 +15,9 @@ interface IDatabase extends ITransactor {
function one($query, ?array $params=null): ?array; 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; function all($query, ?array $params=null, $primaryKeys=null): iterable;
} }

View File

@ -11,12 +11,13 @@ interface ITransactor {
*/ */
function willUpdate(...$transactors): self; function willUpdate(...$transactors): self;
/** Indiquer si une transaction est en cours */
function inTransaction(): bool; function inTransaction(): bool;
/** /**
* démarrer une transaction * 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, * transaction. si une erreur se produit lors de l'appel de la fonction,
* annuler la transaction * annuler la transaction
* *
@ -24,7 +25,9 @@ interface ITransactor {
*/ */
function beginTransaction(?callable $func=null, bool $commit=true): void; function beginTransaction(?callable $func=null, bool $commit=true): void;
/** valider la transaction */
function commit(): void; function commit(): void;
/** annuler la transaction */
function rollback(): void; 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\str;
use nulib\ValueException; use nulib\ValueException;
abstract class _base { abstract class _base extends _common {
protected static function consume(string $pattern, string &$string, ?array &$ms=null): bool { protected static function verifix(&$sql, ?array &$bindings=null, ?array &$meta=null): void {
if (!preg_match("/^$pattern/i", $string, $ms)) return false; if (is_array($sql)) {
$string = substr($string, strlen($ms[0])); $prefix = $sql[0] ?? null;
return true; if ($prefix === null) {
} throw new ValueException("requête invalide");
} elseif (_create::isa($prefix)) {
/** fusionner toutes les parties séquentielles d'une requête */ $sql = _create::parse($sql, $bindings);
protected static function merge_seq(array $query): string { $meta = ["isa" => "create", "type" => "ddl"];
$index = 0; } elseif (_select::isa($prefix)) {
$sql = ""; $sql = _select::parse($sql, $bindings);
foreach ($query as $key => $value) { $meta = ["isa" => "select", "type" => "dql"];
if ($key === $index) { } elseif (_insert::isa($prefix)) {
$index++; $sql = _insert::parse($sql, $bindings);
if ($sql && !str::ends_with(" ", $sql) && !str::starts_with(" ", $value)) { $meta = ["isa" => "insert", "type" => "dml"];
$sql .= " "; } elseif (_update::isa($prefix)) {
} $sql = _update::parse($sql, $bindings);
$sql .= $value; $meta = ["isa" => "update", "type" => "dml"];
} } elseif (_delete::isa($prefix)) {
} $sql = _delete::parse($sql, $bindings);
return $sql; $meta = ["isa" => "delete", "type" => "dml"];
} } elseif (_generic::isa($prefix)) {
$sql = _generic::parse($sql, $bindings);
protected static function is_sep(&$cond): bool { $meta = ["isa" => "generic", "type" => null];
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 { } else {
# condition litérale throw ValueException::invalid_kind($sql, "query");
$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 { } else {
$param = "$param0$i"; if (!is_string($sql)) $sql = strval($sql);
$parts[] = ":$param"; if (_create::isa($sql)) {
$bindings[$param] = $condvalue; $meta = ["isa" => "create", "type" => "ddl"];
if ($i === false) $i = 2; } elseif (_select::isa($sql)) {
else $i++; $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 { static function with($sql, ?array $params=null): array {
if (!$values) return; static::verifix($sql, $params);
$index = 0; return [$sql, $params];
$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");
}
}
abstract protected static function verifix(&$sql, ?array &$bindinds=null, ?array &$meta=null): void;
function __construct($sql, ?array $bindings=null) { function __construct($sql, ?array $bindings=null) {
static::verifix($sql, $bindings, $meta); 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 <?php
namespace nulib\db\pdo; namespace nulib\db\_private;
use nulib\php\nur_func; use nulib\db\IDatabase;
use nulib\php\func;
class _config { class _config {
static function with($configs): self { static function with($configs): self {
@ -23,13 +24,12 @@ class _config {
/** @var array */ /** @var array */
protected $configs; protected $configs;
function configure(Pdo $pdo): void { function configure(IDatabase $db): void {
foreach ($this->configs as $key => $config) { foreach ($this->configs as $key => $config) {
if (is_string($config) && !nur_func::is_method($config)) { if (is_string($config) && !func::is_method($config)) {
$pdo->exec($config); $db->exec($config);
} else { } else {
nur_func::ensure_func($config, $this, $args); func::with($config)->bind($this, true)->invoke([$db, $key]);
nur_func::call($config, $pdo, $key, ...$args);
} }
} }
} }

View File

@ -1,7 +1,7 @@
<?php <?php
namespace nulib\db\_private; namespace nulib\db\_private;
class _create { class _create extends _common {
const SCHEMA = [ const SCHEMA = [
"prefix" => "?string", "prefix" => "?string",
"table" => "string", "table" => "string",
@ -9,4 +9,46 @@ class _create {
"cols" => "?array", "cols" => "?array",
"suffix" => "?string", "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 <?php
namespace nulib\db\_private; namespace nulib\db\_private;
class _delete { class _delete extends _common {
const SCHEMA = [ const SCHEMA = [
"prefix" => "?string", "prefix" => "?string",
"from" => "?string", "from" => "?string",
"where" => "?array", "where" => "?array",
"suffix" => "?string", "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 <?php
namespace nulib\db\_private; namespace nulib\db\_private;
class _generic { use nulib\cl;
use nulib\ValueException;
class _generic extends _common {
const SCHEMA = [ 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 <?php
namespace nulib\db\_private; namespace nulib\db\_private;
class _insert { use nulib\cl;
use nulib\ValueException;
class _insert extends _common {
const SCHEMA = [ const SCHEMA = [
"prefix" => "?string", "prefix" => "?string",
"into" => "?string", "into" => "?string",
@ -10,4 +13,79 @@ class _insert {
"values" => "?array", "values" => "?array",
"suffix" => "?string", "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 <?php
namespace nulib\db\_private; namespace nulib\db\_private;
class _select { use nulib\cl;
use nulib\str;
use nulib\ValueException;
class _select extends _common {
const SCHEMA = [ const SCHEMA = [
"prefix" => "?string", "prefix" => "?string",
"schema" => "?array", "schema" => "?array",
@ -14,4 +18,164 @@ class _select {
"having" => "?array", "having" => "?array",
"suffix" => "?string", "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 <?php
namespace nulib\db\_private; namespace nulib\db\_private;
class _update { class _update extends _common {
const SCHEMA = [ const SCHEMA = [
"prefix" => "?string", "prefix" => "?string",
"table" => "?string", "table" => "?string",
@ -11,4 +11,43 @@ class _update {
"where" => "?array", "where" => "?array",
"suffix" => "?string", "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 <?php
namespace nulib\db\mysql; namespace nulib\db\mysql;
use nulib\cl;
use nulib\db\CapacitorChannel; use nulib\db\CapacitorChannel;
use nulib\db\CapacitorStorage; use nulib\db\CapacitorStorage;
@ -12,8 +13,7 @@ class MysqlStorage extends CapacitorStorage {
$this->db = Mysql::with($mysql); $this->db = Mysql::with($mysql);
} }
/** @var Mysql */ protected Mysql $db;
protected $db;
function db(): Mysql { function db(): Mysql {
return $this->db; return $this->db;
@ -23,17 +23,35 @@ class MysqlStorage extends CapacitorStorage {
"id_" => "integer primary key auto_increment", "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 { 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()); 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 { function _exists(CapacitorChannel $channel): bool {
$db = $this->db; $mysql = $this->db;
$tableName = $db->get([ $tableName = $mysql->get([
"select table_name from information_schema.tables", "select table_name from information_schema.tables",
"where" => [ "where" => [
"table_schema" => $db->getDbname(), "table_schema" => $mysql->getDbname(),
"table_name" => $channel->getTableName(), "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 Generator;
use nulib\cl; use nulib\cl;
use nulib\db\_private\_config;
use nulib\db\_private\Tvalues; use nulib\db\_private\Tvalues;
use nulib\db\IDatabase; use nulib\db\IDatabase;
use nulib\db\ITransactor; use nulib\db\ITransactor;
use nulib\php\nur_func; use nulib\php\func;
use nulib\ValueException; use nulib\ValueException;
class Pdo implements IDatabase { class Pdo implements IDatabase {
@ -21,7 +22,7 @@ class Pdo implements IDatabase {
"dbconn" => $pdo->dbconn, "dbconn" => $pdo->dbconn,
"options" => $pdo->options, "options" => $pdo->options,
"config" => $pdo->config, "config" => $pdo->config,
"migrate" => $pdo->migration, "migration" => $pdo->migration,
], $params)); ], $params));
} else { } else {
return new static($pdo, $params); return new static($pdo, $params);
@ -49,7 +50,7 @@ class Pdo implements IDatabase {
protected const CONFIG = null; protected const CONFIG = null;
protected const MIGRATE = null; protected const MIGRATION = null;
const dbconn_SCHEMA = [ const dbconn_SCHEMA = [
"name" => "string", "name" => "string",
@ -62,7 +63,7 @@ class Pdo implements IDatabase {
"options" => ["?array|callable"], "options" => ["?array|callable"],
"replace_config" => ["?array|callable"], "replace_config" => ["?array|callable"],
"config" => ["?array|callable"], "config" => ["?array|callable"],
"migrate" => ["?array|string|callable"], "migration" => ["?array|string|callable"],
"auto_open" => ["bool", true], "auto_open" => ["bool", true],
]; ];
@ -93,7 +94,7 @@ class Pdo implements IDatabase {
} }
$this->config = $config; $this->config = $config;
# migrations # migrations
$this->migration = $params["migrate"] ?? static::MIGRATE; $this->migration = $params["migration"] ?? static::MIGRATION;
# #
$defaultAutoOpen = self::params_SCHEMA["auto_open"][1]; $defaultAutoOpen = self::params_SCHEMA["auto_open"][1];
if ($params["auto_open"] ?? $defaultAutoOpen) { if ($params["auto_open"] ?? $defaultAutoOpen) {
@ -104,7 +105,7 @@ class Pdo implements IDatabase {
protected ?array $dbconn; protected ?array $dbconn;
/** @var array|callable */ /** @var array|callable */
protected array $options; protected $options;
/** @var array|string|callable */ /** @var array|string|callable */
protected $config; protected $config;
@ -119,8 +120,7 @@ class Pdo implements IDatabase {
$dbconn = $this->dbconn; $dbconn = $this->dbconn;
$options = $this->options; $options = $this->options;
if (is_callable($options)) { if (is_callable($options)) {
nur_func::ensure_func($options, $this, $args); $options = func::with($options)->bind($this, true)->invoke();
$options = nur_func::call($options, ...$args);
} }
$this->db = new \PDO($dbconn["name"], $dbconn["user"], $dbconn["pass"], $options); $this->db = new \PDO($dbconn["name"], $dbconn["user"], $dbconn["pass"], $options);
_config::with($this->config)->configure($this); _config::with($this->config)->configure($this);
@ -143,21 +143,16 @@ class Pdo implements IDatabase {
return $this->db()->exec($query); 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) { function exec($query, ?array $params=null) {
$db = $this->db(); $db = $this->db();
$query = new _query_base($query, $params); $query = new _pdoQuery($query, $params);
if ($query->useStmt($db, $stmt, $sql)) { if ($query->_use_stmt($db, $stmt, $sql)) {
if ($stmt->execute() === false) return false; if ($stmt->execute() === false) return false;
if ($query->isInsert()) return $db->lastInsertId(); if ($query->isInsert()) return $db->lastInsertId();
else return $stmt->rowCount(); else return $stmt->rowCount();
} else { } else {
$rowCount = $db->exec($sql); $rowCount = $db->exec($sql);
if (self::is_insert($sql)) return $db->lastInsertId(); if ($query->isInsert()) return $db->lastInsertId();
else return $rowCount; else return $rowCount;
} }
} }
@ -191,7 +186,7 @@ class Pdo implements IDatabase {
if ($func !== null) { if ($func !== null) {
$commited = false; $commited = false;
try { try {
nur_func::call($func, $this); func::call($func, $this);
if ($commit) { if ($commit) {
$this->commit(); $this->commit();
$commited = true; $commited = true;
@ -222,11 +217,11 @@ class Pdo implements IDatabase {
function get($query, ?array $params=null, bool $entireRow=false) { function get($query, ?array $params=null, bool $entireRow=false) {
$db = $this->db(); $db = $this->db();
$query = new _query_base($query, $params); $query = new _pdoQuery($query, $params);
$stmt = null; $stmt = null;
try { try {
/** @var \PDOStatement $stmt */ /** @var \PDOStatement $stmt */
if ($query->useStmt($db, $stmt, $sql)) { if ($query->_use_stmt($db, $stmt, $sql)) {
if ($stmt->execute() === false) return null; if ($stmt->execute() === false) return null;
} else { } else {
$stmt = $db->query($sql); $stmt = $db->query($sql);
@ -245,22 +240,18 @@ class Pdo implements IDatabase {
return $this->get($query, $params, true); return $this->get($query, $params, true);
} }
/** function all($query, ?array $params=null, $primaryKeys=null): iterable {
* 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 {
$db = $this->db(); $db = $this->db();
$query = new _query_base($query, $params); $query = new _pdoQuery($query, $params);
$stmt = null; $stmt = null;
try { try {
/** @var \PDOStatement $stmt */ /** @var \PDOStatement $stmt */
if ($query->useStmt($db, $stmt, $sql)) { if ($query->_use_stmt($db, $stmt, $sql)) {
if ($stmt->execute() === false) return; if ($stmt->execute() === false) return;
} else { } else {
$stmt = $db->query($sql); $stmt = $db->query($sql);
} }
if ($primaryKeys !== null) $primaryKeys = cl::with($primaryKeys); $primaryKeys = cl::withn($primaryKeys);
while (($row = $stmt->fetch(\PDO::FETCH_ASSOC)) !== false) { while (($row = $stmt->fetch(\PDO::FETCH_ASSOC)) !== false) {
$this->verifixRow($row); $this->verifixRow($row);
if ($primaryKeys !== null) { 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 Generator;
use nulib\cl; use nulib\cl;
use nulib\db\_private\_config;
use nulib\db\_private\Tvalues; use nulib\db\_private\Tvalues;
use nulib\db\IDatabase; use nulib\db\IDatabase;
use nulib\db\ITransactor; use nulib\db\ITransactor;
use nulib\php\nur_func; use nulib\php\func;
use nulib\ValueException; use nulib\ValueException;
use SQLite3; use SQLite3;
use SQLite3Result; use SQLite3Result;
@ -29,7 +30,7 @@ class Sqlite implements IDatabase {
"encryption_key" => $sqlite->encryptionKey, "encryption_key" => $sqlite->encryptionKey,
"allow_wal" => $sqlite->allowWal, "allow_wal" => $sqlite->allowWal,
"config" => $sqlite->config, "config" => $sqlite->config,
"migrate" => $sqlite->migration, "migration" => $sqlite->migration,
], $params)); ], $params));
} elseif (is_array($sqlite)) { } elseif (is_array($sqlite)) {
return new static(null, cl::merge($sqlite, $params)); return new static(null, cl::merge($sqlite, $params));
@ -71,7 +72,7 @@ class Sqlite implements IDatabase {
const CONFIG = null; const CONFIG = null;
const MIGRATE = null; const MIGRATION = null;
const params_SCHEMA = [ const params_SCHEMA = [
"file" => ["string", ""], "file" => ["string", ""],
@ -80,7 +81,7 @@ class Sqlite implements IDatabase {
"allow_wal" => ["?bool"], "allow_wal" => ["?bool"],
"replace_config" => ["?array|callable"], "replace_config" => ["?array|callable"],
"config" => ["?array|callable"], "config" => ["?array|callable"],
"migrate" => ["?array|string|callable"], "migration" => ["?array|string|callable"],
"auto_open" => ["bool", true], "auto_open" => ["bool", true],
]; ];
@ -108,7 +109,7 @@ class Sqlite implements IDatabase {
} }
$this->config = $config; $this->config = $config;
# migrations # migrations
$this->migration = $params["migrate"] ?? static::MIGRATE; $this->migration = $params["migration"] ?? static::MIGRATION;
# #
$defaultAutoOpen = self::params_SCHEMA["auto_open"][1]; $defaultAutoOpen = self::params_SCHEMA["auto_open"][1];
$this->inTransaction = false; $this->inTransaction = false;
@ -149,7 +150,7 @@ class Sqlite implements IDatabase {
if ($this->db === null) { if ($this->db === null) {
$this->db = new SQLite3($this->file, $this->flags, $this->encryptionKey); $this->db = new SQLite3($this->file, $this->flags, $this->encryptionKey);
_config::with($this->config)->configure($this); _config::with($this->config)->configure($this);
_migration::with($this->migration)->migrate($this); _sqliteMigration::with($this->migration)->migrate($this);
$this->inTransaction = false; $this->inTransaction = false;
} }
return $this; return $this;
@ -180,15 +181,10 @@ class Sqlite implements IDatabase {
return $this->db()->exec($query); 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) { function exec($query, ?array $params=null) {
$db = $this->db(); $db = $this->db();
$query = new _query_base($query, $params); $query = new _sqliteQuery($query, $params);
if ($query->useStmt($db, $stmt, $sql)) { if ($query->_use_stmt($db, $stmt, $sql)) {
try { try {
$result = $stmt->execute(); $result = $stmt->execute();
if ($result === false) return false; if ($result === false) return false;
@ -201,7 +197,7 @@ class Sqlite implements IDatabase {
} else { } else {
$result = $db->exec($sql); $result = $db->exec($sql);
if ($result === false) return false; if ($result === false) return false;
if (self::is_insert($sql)) return $db->lastInsertRowID(); if ($query->isInsert()) return $db->lastInsertRowID();
else return $db->changes(); else return $db->changes();
} }
} }
@ -237,7 +233,7 @@ class Sqlite implements IDatabase {
if ($func !== null) { if ($func !== null) {
$commited = false; $commited = false;
try { try {
nur_func::call($func, $this); func::call($func, $this);
if ($commit) { if ($commit) {
$this->commit(); $this->commit();
$commited = true; $commited = true;
@ -274,8 +270,8 @@ class Sqlite implements IDatabase {
function get($query, ?array $params=null, bool $entireRow=false) { function get($query, ?array $params=null, bool $entireRow=false) {
$db = $this->db(); $db = $this->db();
$query = new _query_base($query, $params); $query = new _sqliteQuery($query, $params);
if ($query->useStmt($db, $stmt, $sql)) { if ($query->_use_stmt($db, $stmt, $sql)) {
try { try {
$result = $this->checkResult($stmt->execute()); $result = $this->checkResult($stmt->execute());
try { try {
@ -300,7 +296,7 @@ class Sqlite implements IDatabase {
} }
protected function _fetchResult(SQLite3Result $result, ?SQLite3Stmt $stmt=null, $primaryKeys=null): Generator { protected function _fetchResult(SQLite3Result $result, ?SQLite3Stmt $stmt=null, $primaryKeys=null): Generator {
if ($primaryKeys !== null) $primaryKeys = cl::with($primaryKeys); $primaryKeys = cl::withn($primaryKeys);
try { try {
while (($row = $result->fetchArray(SQLITE3_ASSOC)) !== false) { while (($row = $result->fetchArray(SQLITE3_ASSOC)) !== false) {
$this->verifixRow($row); $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 { function all($query, ?array $params=null, $primaryKeys=null): iterable {
$db = $this->db(); $db = $this->db();
$query = new _query_base($query, $params); $query = new _sqliteQuery($query, $params);
if ($query->useStmt($db, $stmt, $sql)) { if ($query->_use_stmt($db, $stmt, $sql)) {
$result = $this->checkResult($stmt->execute()); $result = $this->checkResult($stmt->execute());
return $this->_fetchResult($result, $stmt, $primaryKeys); return $this->_fetchResult($result, $stmt, $primaryKeys);
} else { } else {

View File

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

View File

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

View File

@ -2,7 +2,7 @@
namespace nulib\output; namespace nulib\output;
use nulib\output\std\ProxyMessenger; 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 * 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 !== null && $log !== false) {
if ($log instanceof IMessenger) log::set_messenger($log); if ($log instanceof IMessenger) log::set_messenger($log);
elseif (is_string($log)) log::set_messenger_class($log); elseif (is_string($log)) log::set_messenger_class($log);
elseif (is_array($log)) { else $log = func::call($log);
nur_func::ensure_class($log, $args);
$log = nur_func::cons($log, $args);
}
log::set_messenger($log); log::set_messenger($log);
$msgs[] = $log; $msgs[] = $log;
} }
if ($console !== null && $console !== false) { if ($console !== null && $console !== false) {
if ($console instanceof IMessenger) console::set_messenger($console); if ($console instanceof IMessenger) console::set_messenger($console);
elseif (is_string($console)) console::set_messenger_class($console); elseif (is_string($console)) console::set_messenger_class($console);
elseif (is_array($console)) { else $console = func::call($console);
nur_func::ensure_class($console, $args);
$console = nur_func::cons($console, $args);
}
console::set_messenger($console); console::set_messenger($console);
$msgs[] = $console; $msgs[] = $console;
} }
if ($say !== null && $say !== false) { if ($say !== null && $say !== false) {
if ($say instanceof IMessenger) say::set_messenger($say); if ($say instanceof IMessenger) say::set_messenger($say);
elseif (is_string($say)) say::set_messenger_class($say); elseif (is_string($say)) say::set_messenger_class($say);
elseif (is_array($say)) { else $say = func::call($say);
nur_func::ensure_class($say, $args);
$say = nur_func::cons($say, $args);
}
say::set_messenger($say); say::set_messenger($say);
$msgs[] = $say; $msgs[] = $say;
} }

View File

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

View File

@ -3,13 +3,16 @@ namespace nulib\php;
use Closure; use Closure;
use Exception; use Exception;
use Generator;
use nulib\A; use nulib\A;
use nulib\cl;
use nulib\cv; use nulib\cv;
use nulib\StateException; use nulib\StateException;
use nulib\ValueException; use nulib\ValueException;
use ReflectionClass; use ReflectionClass;
use ReflectionFunction; use ReflectionFunction;
use ReflectionMethod; use ReflectionMethod;
use Traversable;
/** /**
* Class func: outils pour appeler fonctions et méthodes dynamiquement * 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) * la fonction (ne pas uniquement faire une vérification syntaxique)
*/ */
static function verifix_function(&$func, bool $strict=true, ?string &$reason=null): bool { static function verifix_function(&$func, bool $strict=true, ?string &$reason=null): bool {
if ($strict) { if ($strict) $reason = null;
$msg = var_export($func, true);
$reason = null;
}
if ($func instanceof ReflectionFunction) return true; if ($func instanceof ReflectionFunction) return true;
if (is_string($func)) { if (is_string($func)) {
$c = false; $c = false;
@ -82,11 +82,11 @@ class func {
if ($strict) { if ($strict) {
$reason = null; $reason = null;
if (class_exists($f)) { if (class_exists($f)) {
$reason = "$msg: is a class"; $reason = "$f: is a class";
return false; return false;
} }
if (!function_exists($f)) { if (!function_exists($f)) {
$reason = "$msg: function not found"; $reason = "$f: function not found";
return false; return false;
} }
} }
@ -117,10 +117,7 @@ class func {
* faire une vérification syntaxique) * faire une vérification syntaxique)
*/ */
static function verifix_class(&$func, bool $strict=true, ?string &$reason=null): bool { static function verifix_class(&$func, bool $strict=true, ?string &$reason=null): bool {
if ($strict) { if ($strict) $reason = null;
$msg = var_export($func, true);
$reason = null;
}
if ($func instanceof ReflectionClass) return true; if ($func instanceof ReflectionClass) return true;
if (is_string($func)) { if (is_string($func)) {
$c = $func; $c = $func;
@ -138,12 +135,10 @@ class func {
if (self::_parse_static($c)) return false; if (self::_parse_static($c)) return false;
if (self::_parse_method($c)) return false; if (self::_parse_method($c)) return false;
if ($f !== false) return false; if ($f !== false) return false;
if ($strict) { if ($strict && !class_exists($c)) {
if (!class_exists($c)) { $reason = "$c: class not found";
$reason = "$msg: class not found";
return false; return false;
} }
}
$func = [$c, false]; $func = [$c, false];
return true; return true;
} }
@ -207,10 +202,7 @@ class func {
* la méthode est liée (ne pas uniquement faire une vérification syntaxique) * 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 { static function verifix_static(&$func, bool $strict=true, ?bool &$bound=null, ?string &$reason=null): bool {
if ($strict) { if ($strict) $reason = null;
$msg = var_export($func, true);
$reason = null;
}
if ($func instanceof ReflectionMethod) { if ($func instanceof ReflectionMethod) {
$bound = false; $bound = false;
return true; return true;
@ -265,18 +257,19 @@ class func {
return false; return false;
} }
if ($strict) { if ($strict) {
[$c, $f] = $cf;
$reason = null; $reason = null;
if ($bound) { if ($bound) {
if (!class_exists($c)) { if (!class_exists($c)) {
$reason = "$msg: class not found"; $reason = "$c: class not found";
return false; return false;
} }
if (!method_exists($c, $f)) { if (!method_exists($c, $f)) {
$reason = "$msg: method not found"; $reason = "$c::$f: method not found";
return false; return false;
} }
} else { } else {
$reason = "$msg: not bound"; $reason = "$c::$f: not bound";
} }
} }
$func = $cf; $func = $cf;
@ -342,10 +335,7 @@ class func {
* la méthode est liée (ne pas uniquement faire une vérification syntaxique) * 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 { static function verifix_method(&$func, bool $strict=true, ?bool &$bound=null, ?string &$reason=null): bool {
if ($strict) { if ($strict) $reason = null;
$msg = var_export($func, true);
$reason = null;
}
if ($func instanceof ReflectionMethod) { if ($func instanceof ReflectionMethod) {
$bound = false; $bound = false;
return true; return true;
@ -401,18 +391,19 @@ class func {
return false; return false;
} }
if ($strict) { if ($strict) {
[$c, $f] = $cf;
$reason = null; $reason = null;
if ($bound) { if ($bound) {
if (!is_object($c) && !class_exists($c)) { if (!is_object($c) && !class_exists($c)) {
$reason = "$msg: class not found"; $reason = "$c: class not found";
return false; return false;
} }
if (!method_exists($c, $f)) { if (!method_exists($c, $f)) {
$reason = "$msg: method not found"; $reason = "$c::$f: method not found";
return false; return false;
} }
} else { } else {
$reason = "$msg: not bound"; $reason = "$c::$f: not bound";
} }
} }
$func = $cf; $func = $cf;
@ -446,7 +437,7 @@ class func {
return new ValueException($reason); 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 (!is_array($func)) {
if ($func instanceof Closure) { if ($func instanceof Closure) {
return new self(self::TYPE_CLOSURE, $func, $args); return new self(self::TYPE_CLOSURE, $func, $args);
@ -467,6 +458,12 @@ class func {
} elseif (self::verifix_static($func, $strict, $bound, $reason)) { } elseif (self::verifix_static($func, $strict, $bound, $reason)) {
return new self(self::TYPE_STATIC, $func, $args, $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); 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) { static function call($func, ...$args) {
return self::with($func)->invoke($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) { protected function __construct(int $type, $func, ?array $args=null, bool $bound=false, ?string $reason=null) {
@ -561,6 +593,27 @@ class func {
protected int $maxArgs; 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 { protected function updateReflection($reflection): void {
$variadic = false; $variadic = false;
$minArgs = $maxArgs = 0; $minArgs = $maxArgs = 0;
@ -596,11 +649,16 @@ class func {
else return $this->bound && $this->object !== null; 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->type !== self::TYPE_METHOD) return $this;
if ($this->bound && $unlessAlreadyBound) return $this;
[$c, $f] = $this->func; [$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->func[0] = $c = $object;
$this->updateReflection(new ReflectionMethod($c, $f)); $this->updateReflection(new ReflectionMethod($c, $f));
} }

View File

@ -44,7 +44,7 @@ class mprop {
} catch (ReflectionException $e) { } catch (ReflectionException $e) {
return oprop::get($object, $property, $default); 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é */ /** spécifier la valeur d'une propriété */
@ -60,7 +60,7 @@ class mprop {
} catch (ReflectionException $e) { } catch (ReflectionException $e) {
return oprop::_set($c, $object, $property, $value); return oprop::_set($c, $object, $property, $value);
} }
nur_func::call([$object, $m], $value); func::call([$object, $m], $value);
return $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 { class ref_schema {
/** @var array schéma des natures de schéma */ /** @var array schéma des natures de schéma */
const NATURE_METASCHEMA = [ const NATURE_METASCHEMA = [
"nature" => ["string", null, "nature du schéma", 0 => ["string", null, "nature du schéma",
"pkey" => 0, "allowed_values" => ["scalar", "assoc", "list"],
"allowed_values" => ["assoc", "list", "scalar"],
], ],
"title" => ["?string", null, "libellé de la valeur"], "compute_func" => ["?callable", null, "fonction qui calcule les valeurs des champs computed"],
"required" => ["bool", false, "la valeur est-elle requise?"], "validate_func" => ["?callable", null, "fonction qui vérifie la conformité de l'objet dans son ensemble"],
"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"],
]; ];
/** @var array meta-schema d'un schéma de nature scalaire */ /** @var array meta-schéma d'une valeur */
const SCALAR_METASCHEMA = [ const VALUE_METASCHEMA = [
"type" => ["array", null, "types possibles de la valeur", "required" => true], "type" => ["array", null, "types possibles de la valeur", "required" => true],
"default" => [null, null, "valeur par défaut si la valeur n'existe pas"], "default" => [null, null, "valeur par défaut si la valeur n'existe pas"],
"title" => ["?string", null, "libellé de la valeur"], "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"], "messages" => ["?array", null, "messages à afficher en cas d'erreur d'analyse"],
"formatter_func" => ["?callable", null, "fonction qui formatte la valeur pour affichage"], "formatter_func" => ["?callable", null, "fonction qui formatte la valeur pour affichage"],
"format" => [null, null, "format à utiliser pour l'affichage"], "format" => [null, null, "format à utiliser pour l'affichage"],
"" => ["array", "scalar", "nature du schéma", "size" => ["?int", null, "nom de caractères ou de chiffres de la valeur"],
"" => ["assoc", "schema" => self::NATURE_METASCHEMA], "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"], "name" => ["?string", null, "identifiant de la valeur"],
"pkey" => ["?pkey", null, "chemin de clé de la valeur dans un tableau associatif"], "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"], "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 = [ const MESSAGES = [
"missing" => "{key}: Vous devez spécifier cette valeur", "missing" => "vous devez spécifier cette valeur",
"unavailable" => "{key}: Vous devez spécifier cette valeur", "unavailable" => "vous devez spécifier cette valeur",
"null" => "{key}: cette valeur ne doit pas être nulle", "null" => "cette valeur ne doit pas être nulle",
"empty" => "{key}: cette valeur ne doit pas être vide", "empty" => "cette valeur ne doit pas être vide",
"invalid" => "{key}: {orig}: cette valeur est invalide", "invalid" => "cette valeur est invalide",
]; ];
/** @var array meta-schema d'un schéma de nature associative */ const PARAMS_SCHEMA = [
const ASSOC_METASCHEMA = [ "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 */ /** @var array clés supplémentaires de schéma de la nature scalaire */
const LIST_METASCHEMA = [ 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", "boolean" => "bool",
"integer" => "int", "integer" => "int",
"flt" => "float", "double" => "float", "dbl" => "float", "flt" => "float", "double" => "float", "dbl" => "float",
"func" => "callable", "function" => "callable",
]; ];
} }

View File

@ -242,6 +242,21 @@ class str {
return true; 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 * 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