Compare commits

..

44 Commits

Author SHA1 Message Date
8e347bc23e modifs.mineures sans commentaires 2025-10-20 12:15:39 +04:00
e99716735c modifs.mineures sans commentaires 2025-10-19 20:34:58 +04:00
39abe09f11 modifs.mineures sans commentaires 2025-10-19 08:42:44 +04:00
e4e6a98be2 suite support feature branches 2025-10-17 20:21:43 +04:00
0b01946090 suite pwip 2025-10-17 17:20:15 +04:00
bda3cff3d3 la signature de file::writer est cohérente avec les autres méthodes 2025-10-16 15:43:30 +04:00
2f3a21aad4 support délai infini 2025-10-16 08:03:35 +04:00
24efdddb68 support de la session 2025-10-16 06:33:29 +04:00
27eb08ecff modifs.mineures sans commentaires 2025-10-16 06:16:05 +04:00
5eb376257f renommer data en key 2025-10-16 06:07:16 +04:00
7d332552ab modifs.mineures sans commentaires 2025-10-15 17:09:19 +04:00
cf5ef38a0f modifs.mineures sans commentaires 2025-10-15 16:06:25 +04:00
a0c6fb21e6 modifs.mineures sans commentaires 2025-10-15 15:33:13 +04:00
48d5f84bbd modifs.mineures sans commentaires 2025-10-15 12:15:04 +04:00
55728059cf début réorganisation log 2025-10-15 12:13:49 +04:00
51215b42eb ne pas changer le répertoire courant 2025-10-15 09:48:28 +04:00
20e64b8ffb modifs.mineures sans commentaires 2025-10-14 16:55:50 +04:00
c62de542c3 ajout cachedir 2025-10-14 14:40:02 +04:00
394edb782e maj todo 2025-10-14 14:36:46 +04:00
5da09a039b modifs.mineures sans commentaires 2025-10-13 16:57:12 +04:00
59e2abee61 modifs.mineures sans commentaires 2025-10-13 14:35:15 +04:00
5035d5522a mise à jour de la gestion des logs 2025-10-13 11:52:41 +04:00
8974cf09a1 modifs.mineures sans commentaires 2025-10-08 15:48:46 +04:00
c37748b6f4 modifs.mineures sans commentaires 2025-10-08 15:44:34 +04:00
d52ab4f5a1 modifs.mineures sans commentaires 2025-10-08 14:10:13 +04:00
23fe2859b5 modifs.mineures sans commentaires 2025-10-08 12:45:04 +04:00
42992c84d4 réorganiser les exceptions 2025-10-08 12:42:51 +04:00
cf30ff6386 exceptions normalisées 2025-10-08 09:06:03 +04:00
75c06e7038 ajouter le support des articles 2025-10-08 09:04:31 +04:00
ee058e00cd modifs.mineures sans commentaires 2025-10-07 08:20:59 +04:00
c748fed388 Date et DateTime sont immutable 2025-10-07 05:34:58 +04:00
810ead58d6 modifs.mineures sans commentaires 2025-10-07 02:00:52 +04:00
95b0263969 modifs.mineures sans commentaires 2025-10-06 21:05:57 +04:00
c1c369f554 maj chemin pour phpwrappers 2025-10-06 13:09:02 +04:00
90ca62984d maj mounts 2025-10-06 11:46:07 +04:00
f55c66e1f3 support --no-arg 2025-10-06 08:59:29 +04:00
526a693ead changer la configuration par défaut pour mysql 2025-10-06 08:04:16 +04:00
ff4ef34037 par défaut, vérifier la connexion avant chaque transaction 2025-10-05 20:52:32 +04:00
f52da16f44 mailer prend la configuration dans config aussi 2025-10-05 20:07:30 +04:00
d8b70d7ee0 optimiser la destination du cache 2025-10-05 17:27:12 +04:00
291db941b9 modifs.mineures sans commentaires 2025-10-05 16:56:03 +04:00
651ba8c553 intégration de nulib/cache 2025-10-05 16:48:18 +04:00
6cedfe9493 modifs.mineures sans commentaires 2025-10-04 10:12:45 +04:00
014825f09d modifs.mineures sans commentaires 2025-10-04 09:59:02 +04:00
160 changed files with 8314 additions and 3199 deletions

View File

@ -17,6 +17,21 @@
</DockerContainerSettings> </DockerContainerSettings>
</value> </value>
</entry> </entry>
<entry key="385aa179-8c50-45e2-9ad7-4d9bfba298a3">
<value>
<DockerContainerSettings>
<option name="version" value="1" />
<option name="volumeBindings">
<list>
<DockerVolumeBindingImpl>
<option name="containerPath" value="/opt/project" />
<option name="hostPath" value="$PROJECT_DIR$" />
</DockerVolumeBindingImpl>
</list>
</option>
</DockerContainerSettings>
</value>
</entry>
<entry key="38915385-b3ff-4f4b-8a9a-d5f3ecae559e"> <entry key="38915385-b3ff-4f4b-8a9a-d5f3ecae559e">
<value> <value>
<DockerContainerSettings> <DockerContainerSettings>

4
.idea/php.xml generated
View File

@ -2,7 +2,7 @@
<project version="4"> <project version="4">
<component name="MessDetector"> <component name="MessDetector">
<phpmd_settings> <phpmd_settings>
<phpmd_by_interpreter asDefaultInterpreter="true" interpreter_id="846389f7-9fb5-4173-a868-1dc6b8fbb3fa" timeout="30000" /> <phpmd_by_interpreter asDefaultInterpreter="true" interpreter_id="38915385-b3ff-4f4b-8a9a-d5f3ecae559e" timeout="30000" />
</phpmd_settings> </phpmd_settings>
</component> </component>
<component name="MessDetectorOptionsConfiguration"> <component name="MessDetectorOptionsConfiguration">
@ -17,7 +17,7 @@
</component> </component>
<component name="PhpCodeSniffer"> <component name="PhpCodeSniffer">
<phpcs_settings> <phpcs_settings>
<phpcs_by_interpreter asDefaultInterpreter="true" interpreter_id="846389f7-9fb5-4173-a868-1dc6b8fbb3fa" timeout="30000" /> <phpcs_by_interpreter asDefaultInterpreter="true" interpreter_id="38915385-b3ff-4f4b-8a9a-d5f3ecae559e" timeout="30000" />
</phpcs_settings> </phpcs_settings>
</component> </component>
<component name="PhpIncludePathManager"> <component name="PhpIncludePathManager">

View File

@ -1,7 +1,5 @@
# -*- 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
## configuration par défaut
UPSTREAM= UPSTREAM=
DEVELOP=develop DEVELOP=develop
FEATURE=wip/ FEATURE=wip/

View File

@ -28,6 +28,146 @@ CONFIG_VARS=(
UPSTREAM DEVELOP FEATURE RELEASE MAIN TAG_PREFIX TAG_SUFFIX HOTFIX DIST NOAUTO UPSTREAM DEVELOP FEATURE RELEASE MAIN TAG_PREFIX TAG_SUFFIX HOTFIX DIST NOAUTO
) )
################################################################################
PMAN_TOOL_PUPS=UPSTREAM
PMAN_TOOL_PDEV=DEVELOP
PMAN_TOOL_PWIP=FEATURE
PMAN_TOOL_PMAIN=MAIN
PMAN_TOOL_PDIST=DIST
UPSTREAM_BASE= ; UPSTREAM_MERGE_FROM= ; UPSTREAM_MERGE_TO=DEVELOP ; UPSTREAM_PREL= ; UPSTREAM_DELETE=
DEVELOP_BASE=MAIN ; DEVELOP_MERGE_FROM=FEATURE ; DEVELOP_MERGE_TO=MAIN ; DEVELOP_PREL=to ; DEVELOP_DELETE=from
MAIN_BASE= ; MAIN_MERGE_FROM=DEVELOP ; MAIN_MERGE_TO=DIST ; MAIN_PREL=from ; MAIN_DELETE=
DIST_BASE=MAIN ; DIST_MERGE_FROM=MAIN ; DIST_MERGE_TO= ; DIST_PREL= ; DIST_DELETE=
FEATURE_BASE=DEVELOP ; FEATURE_MERGE_FROM= ; FEATURE_MERGE_TO=DEVELOP ; FEATURE_PREL= ; FEATURE_DELETE=to
function get_base_branch() {
# afficher la branche depuis laquelle créer la branche $1
# retourner 1 en cas d'erreur (pas de branche source)
local branch="$1" infos
[ -n "$branch" ] || return 1
infos="${branch^^}_BASE"; branch="${!infos}"
[ -n "$branch" ] && echo "$branch" || return 1
}
function get_merge_from_branch() {
# afficher la branche depuis laquelle la branche $1 doit merger
# retourner 1 en cas d'erreur (pas de branche source)
local branch="$1" infos
[ -n "$branch" ] || return 1
infos="${branch^^}_MERGE_FROM"; branch="${!infos}"
[ -n "$branch" ] && echo "$branch" || return 1
}
function get_merge_to_branch() {
# afficher la branche dans laquelle la branche $1 doit merger
# retourner 1 en cas d'erreur (pas de branche destination)
local branch="$1" infos
[ -n "$branch" ] || return 1
infos="${branch^^}_MERGE_TO"; branch="${!infos}"
[ -n "$branch" ] && echo "$branch" || return 1
}
function should_prel_merge() {
# tester si la branche $1 doit être mergée avec prel dans la direction
# $2(=to)
local branch="$1" dir="${2:-to}" infos
[ -n "$branch" ] || return 1
infos="${branch^^}_PREL"
[ "${!infos}" == "$dir" ]
}
function should_delete_merged() {
# tester si la branche $1 doit être supprimée après avoir été mergée dans la
# direction $2(=to)
local branch="$1" dir="${2:-to}" infos
[ -n "$branch" ] || return 1
infos="${branch^^}_DELETE"
[ "${!infos}" == "$dir" ]
}
: "
# description des variables #
* PMAN_TOOL -- nom de l'outil, e.g pdev, pmain, pdist
* PMAN_REF_BRANCH -- code de la branche de référence basé sur le nom de l'outil
* PmanRefBranch -- nom effectif de la branche si elle est définie dans
.pman.conf, vide sinon
* IfRefBranch -- nom effectif de la branche *si elle existe*, vide sinon
* PMAN_UNIQUE -- si la branche de référence est unique. est vide pour les
codes de branches multiples, telle que FEATURE
* PMAN_BASE_BRANCH -- branche de base à partir de laquelle créer la branche
de référence
* PmanBaseBranch -- nom effectif de la branche de base si elle est définie
dans .pman.conf, vide sinon
* IfBaseBranch -- nom effectif de la branche de base *si elle existe*, vide
sinon
* PMAN_MERGE_FROM -- code de la branche source à partir de laquelle la fusion
est faite dans PMAN_REF_BRANCH. vide si la branche n'a pas de source
* PMAN_MERGE_TO -- code de la branche destination dans laquelle la fusion est
faite depuis PMAN_REF_BRANCH. vide si la branche n'a pas de destination
* PMAN_DIR -- direction de la fusion:
'from' si on fait PMAN_MERGE_FROM --> PMAN_REF_BRANCH
'to' si on fait PMAN_REF_BRANCH --> PMAN_MERGE_TO
* PMAN_PREL_MERGE -- si la fusion devrait se faire avec prel
* PMAN_DELETE_MERGED -- s'il faut supprimer la branche source après la fusion
* PMAN_MERGE_SRC -- code de la branche source pour la fusion, ou vide si la
fusion n'est pas possible
* PmanMergeSrc -- nom effectif de la branche source si elle est définie
dans .pman.conf
* IfMergeSrc -- nom effectif de la branche source *si elle existe*, vide
sinon
* PMAN_MERGE_DEST -- code de la branche destination pour la fusion, ou vide si
la fusion n'est pas possible
* PmanMergeDest -- nom effectif de la branche destination si elle est
définie dans .pman.conf
* IfMergeDest -- nom effectif de la branche source *si elle existe*, vide
sinon"
[ -n "$PMAN_TOOL" ] || PMAN_TOOL="$MYNAME"
PMAN_REF_BRANCH="PMAN_TOOL_${PMAN_TOOL^^}"; PMAN_REF_BRANCH="${!PMAN_REF_BRANCH}"
function set_pman_vars() {
PmanRefBranch="${!PMAN_REF_BRANCH}"
case "$PMAN_REF_BRANCH" in
FEATURE|RELEASE|HOTFIX) PMAN_UNIQUE=;;
*) PMAN_UNIQUE=1;;
esac
PMAN_BASE_BRANCH=$(get_base_branch "$PMAN_REF_BRANCH")
[ -n "$PMAN_BASE_BRANCH" ] && PmanBaseBranch="${!PMAN_BASE_BRANCH}" || PmanBaseBranch=
PMAN_MERGE_FROM=$(get_merge_from_branch "$PMAN_REF_BRANCH")
PMAN_MERGE_TO=$(get_merge_to_branch "$PMAN_REF_BRANCH")
if [ -n "$1" ]; then PMAN_DIR="$1"
else PMAN_DIR=to
#elif [ -n "$PMAN_MERGE_TO" ]; then PMAN_DIR=to
#else PMAN_DIR=from
fi
PMAN_PREL_MERGE=$(should_prel_merge "$PMAN_REF_BRANCH" "$PMAN_DIR" && echo 1)
PMAN_DELETE_MERGED=$(should_delete_merged "$PMAN_REF_BRANCH" "$PMAN_DIR" && echo 1)
case "$PMAN_DIR" in
to)
PMAN_MERGE_SRC="$PMAN_REF_BRANCH"
PMAN_MERGE_DEST="$PMAN_MERGE_TO"
;;
from)
PMAN_MERGE_SRC="$PMAN_MERGE_FROM"
PMAN_MERGE_DEST="$PMAN_REF_BRANCH"
;;
esac
[ -n "$PMAN_MERGE_SRC" ] && PmanMergeSrc="${!PMAN_MERGE_SRC}" || PmanMergeSrc=
[ -n "$PMAN_MERGE_DEST" ] && PmanMergeDest="${!PMAN_MERGE_DEST}" || PmanMergeDest=
}
################################################################################
function _init_changelog() { function _init_changelog() {
setx date=date +%d/%m/%Y-%H:%M setx date=date +%d/%m/%Y-%H:%M
ac_set_tmpfile changelog ac_set_tmpfile changelog
@ -147,18 +287,23 @@ EOF
################################################################################ ################################################################################
# Config # Config
function ensure_gitdir() { function check_gitdir() {
# commencer dans le répertoire indiqué # commencer dans le répertoire indiqué
local chdir="$1" local chdir="$1"
if [ -n "$chdir" ]; then if [ -n "$chdir" ]; then
cd "$chdir" || die || return cd "$chdir" || return 1
fi fi
# se mettre à la racine du dépôt git # se mettre à la racine du dépôt git
local gitdir local gitdir
git_ensure_gitvcs git_check_gitvcs || return 1
setx gitdir=git_get_toplevel setx gitdir=git_get_toplevel
cd "$gitdir" || die || return cd "$gitdir" || return 1
}
function ensure_gitdir() {
# commencer dans le répertoire indiqué
check_gitdir "$@" || die || return 1
} }
function load_branches() { function load_branches() {
@ -203,23 +348,32 @@ function load_branches() {
HotfixBranch= HotfixBranch=
MainBranch= MainBranch=
DistBranch= DistBranch=
IfRefBranch=
IfBaseBranch=
IfMergeSrc=
IfMergeDest=
for branch in "${LocalBranches[@]}"; do for branch in "${LocalBranches[@]}"; do
if [ "$branch" == "$UPSTREAM" ]; then if [ "$branch" == "$UPSTREAM" ]; then
UpstreamBranch="$branch" UpstreamBranch="$branch"
elif [[ "$branch" == "$FEATURE"* ]]; then elif [ -n "$FEATURE" ] && [[ "$branch" == "$FEATURE"* ]]; then
FeatureBranches+=("$branch") FeatureBranches+=("$branch")
elif [ "$branch" == "$DEVELOP" ]; then elif [ -n "$DEVELOP" -a "$branch" == "$DEVELOP" ]; then
DevelopBranch="$branch" DevelopBranch="$branch"
elif [[ "$branch" == "$RELEASE"* ]]; then elif [ -n "$RELEASE" ] && [[ "$branch" == "$RELEASE"* ]]; then
ReleaseBranch="$branch" ReleaseBranch="$branch"
elif [[ "$branch" == "$HOTFIX"* ]]; then elif [ -n "$HOTFIX" ] && [[ "$branch" == "$HOTFIX"* ]]; then
HotfixBranch="$branch" HotfixBranch="$branch"
elif [ "$branch" == "$MAIN" ]; then elif [ -n "$MAIN" -a "$branch" == "$MAIN" ]; then
MainBranch="$branch" MainBranch="$branch"
elif [ "$branch" == "$DIST" ]; then elif [ -n "$DIST" -a "$branch" == "$DIST" ]; then
DistBranch="$branch" DistBranch="$branch"
fi fi
[ -n "$PmanRefBranch" -a "$branch" == "$PmanRefBranch" ] && IfRefBranch="$branch"
[ -n "$PmanBaseBranch" -a "$branch" == "$PmanBaseBranch" ] && IfBaseBranch="$branch"
[ -n "$PmanMergeSrc" -a "$branch" == "$PmanMergeSrc" ] && IfMergeSrc="$branch"
[ -n "$PmanMergeDest" -a "$branch" == "$PmanMergeDest" ] && IfMergeDest="$branch"
done done
[ -n "$IfMergeSrc" -a "$IfMergeDest" ] && IfCanMerge=1 || IfCanMerge=
;; ;;
esac esac
} }
@ -244,9 +398,6 @@ function load_config() {
elif [ -f .pman.conf ]; then elif [ -f .pman.conf ]; then
ConfigFile="$(pwd)/.pman.conf" ConfigFile="$(pwd)/.pman.conf"
source "$ConfigFile" source "$ConfigFile"
elif [ -n "$1" -a -n "${MYNAME#$1}" ]; then
ConfigFile="$NULIBDIR/bash/src/pman${MYNAME#$1}.conf.sh"
source "$ConfigFile"
else else
ConfigFile="$NULIBDIR/bash/src/pman.conf.sh" ConfigFile="$NULIBDIR/bash/src/pman.conf.sh"
fi fi

289
bash/src/pman.tool.sh Normal file
View File

@ -0,0 +1,289 @@
# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8
git_cleancheckout_DIRTY="\
Vous avez des modifications locales.
Enregistrez ces modifications avant de fusionner la branche"
function dump_action() {
enote "Valeurs des variables:
PMAN_TOOL=$PMAN_TOOL
PMAN_REF_BRANCH=$PMAN_REF_BRANCH${PmanRefBranch:+ PmanRefBranch=$PmanRefBranch IfRefBranch=$IfRefBranch}
PMAN_BASE_BRANCH=$PMAN_BASE_BRANCH${PmanBaseBranch:+ PmanBaseBranch=$PmanBaseBranch IfBaseBranch=$IfBaseBranch}
PMAN_MERGE_FROM=$PMAN_MERGE_FROM
PMAN_MERGE_TO=$PMAN_MERGE_TO
PMAN_DIR=$PMAN_DIR
PMAN_PREL_MERGE=$PMAN_PREL_MERGE
PMAN_DELETE_MERGED=$PMAN_DELETE_MERGED
PMAN_MERGE_SRC=$PMAN_MERGE_SRC${PmanMergeSrc:+ PmanMergeSrc=$PmanMergeSrc IfMergeSrc=$IfMergeSrc}
PMAN_MERGE_DEST=$PMAN_MERGE_DEST${PmanMergeDest:+ PmanMergeDest=$PmanMergeDest IfMergeDest=$IfMergeDest}
CurrentBranch=$CurrentBranch
LocalBranches=${LocalBranches[*]}
RemoteBranches=${RemoteBranches[*]}
AllBranches=${AllBranches[*]}
UpstreamBranch=$UpstreamBranch
FeatureBranches=${FeatureBranches[*]}
DevelopBranch=$DevelopBranch
ReleaseBranch=$ReleaseBranch
HotfixBranch=$HotfixBranch
MainBranch=$MainBranch
DistBranch=$DistBranch
"
}
function resolve_unique_branch() {
if [ "$PMAN_REF_BRANCH" == FEATURE ]; then
if [ $# -gt 0 ]; then
PmanRefBranch="$FEATURE${1#$FEATURE}"
elif [[ "$CurrentBranch" == "$FEATURE"* ]]; then
PmanRefBranch="$CurrentBranch"
elif [ ${#FeatureBranches[*]} -eq 0 ]; then
die "Vous devez spécifier la branche de feature"
elif [ ${#FeatureBranches[*]} -eq 1 ]; then
PmanRefBranch="${FeatureBranches[0]}"
else
simple_menu \
PmanRefBranch FeatureBranches \
-t "Branches de feature" \
-m "Veuillez choisir la branche de feature" \
-d "${FeatureBranches[0]}"
fi
else
die "resolve_unique_branch: $PMAN_REF_BRANCH: non implémenté"
fi
if [ "$PMAN_DIR" == to ]; then
PmanMergeSrc="$PmanRefBranch"
elif [ "$PMAN_DIR" == from ]; then
PmanMergeDest="$PmanRefBranch"
fi
}
function _ensure_ref_branch() {
[ -n "$PmanRefBranch" ] || die "\
La branche $PMAN_REF_BRANCH n'a pas été définie.
Veuillez éditer le fichier .pman.conf"
[ "$1" == init -o -n "$IfRefBranch" ] || die "$PmanRefBranch: cette branche n'existe pas (le dépôt a-t-il été initialisé?)"
}
function _ensure_base_branch() {
[ -n "$PmanBaseBranch" ] || die "\
La branche $PMAN_BASE_BRANCH n'a pas été définie.
Veuillez éditer le fichier .pman.conf"
[ "$1" == init -o -n "$IfBaseBranch" ] || die "$PmanBaseBranch: cette branche n'existe pas (le dépôt a-t-il été initialisé?)"
}
function checkout_action() {
local -a push_branches
[ -n "$PMAN_UNIQUE" ] || resolve_unique_branch "$@"
_ensure_ref_branch init
#if [ -n "$IfRefBranch" ]; then
# git checkout "$IfRefBranch"
#el
if array_contains LocalBranches "$PmanRefBranch"; then
git checkout "$PmanRefBranch"
elif array_contains AllBranches "$PmanRefBranch"; then
enote "$PmanRefBranch: une branche du même nom existe dans l'origine"
ask_yesno "Voulez-vous basculer sur cette branche?" O || die
git checkout "$PmanRefBranch"
else
_ensure_base_branch
resolve_should_push
enote "Vous allez créer la branche ${COULEUR_BLEUE}$PmanRefBranch${COULEUR_NORMALE} <-- ${COULEUR_ROUGE}$PmanBaseBranch${COULEUR_NORMALE}"
ask_yesno "Voulez-vous continuer?" O || die
einfo "Création de la branche $PmanRefBranch"
git checkout -b "$PmanRefBranch" "$PmanBaseBranch" || die
push_branches+=("$PmanRefBranch")
_push_branches
fi
}
function ensure_merge_branches() {
[ -n "$PmanMergeSrc" ] || die "\
$PmanRefBranch: configuration de fusion non trouvée: la branche $PMAN_MERGE_SRC n'a pas été définie.
Veuillez éditer le fichier .pman.conf"
[ -n "$PmanMergeDest" ] || die "\
$PmanRefBranch: configuration de fusion non trouvée: la branche $PMAN_MERGE_DEST n'a pas été définie.
Veuillez éditer le fichier .pman.conf"
local branches
[ "$1" == -a ] && branches=AllBranches || branches=LocalBranches
array_contains "$branches" "$PmanMergeSrc" || die "$PmanMergeSrc: branche source introuvable"
array_contains "$branches" "$PmanMergeDest" || die "$PmanMergeDest: branche destination introuvable"
}
function _show_action() {
local commits
setx commits=_list_commits "$PmanMergeSrc" "$PmanMergeDest"
if [ -n "$commits" ]; then
if [ $ShowLevel -ge 2 ]; then
{
echo "\
# Commits à fusionner $PmanMergeSrc --> $PmanMergeDest
$commits
"
_sd_COLOR=always _show_diff
} | less -eRF
else
einfo "Commits à fusionner $PmanMergeSrc --> $PmanMergeDest"
eecho "$commits"
fi
fi
}
function show_action() {
git_check_cleancheckout || ewarn "$git_cleancheckout_DIRTY"
[ -n "$PMAN_UNIQUE" ] || resolve_unique_branch "$@"
ensure_merge_branches
_show_action "$@"
}
function _merge_action() {
enote "\
Ce script va
- fusionner la branche ${COULEUR_BLEUE}$PmanMergeSrc${COULEUR_NORMALE} dans ${COULEUR_ROUGE}$PmanMergeDest${COULEUR_NORMALE}${Push:+
- pousser les branches modifiées}"
ask_yesno "Voulez-vous continuer?" O || die
local script=".git/pman-merge.sh"
local -a push_branches delete_branches
local hook
local comment=
local or_die=" || exit 1"
_mscript_start
_scripta <<EOF
################################################################################
# merge
if [ -n "\$merge" ]; then
esection "Fusionner la branche"
EOF
hook="BEFORE_MERGE_$PMAN_MERGE_SRC"; [ -n "${!hook}" ] && _scripta <<EOF
(
${!hook}
)$or_die
EOF
_mscript_merge_branch
hook="AFTER_MERGE_$PMAN_MERGE_SRC"; [ -n "${!hook}" ] && _scripta <<EOF
(
${!hook}
)$or_die
EOF
_scripta <<EOF
fi
EOF
if [ -n "$ShouldDelete" ]; then
_scripta <<EOF
################################################################################
# delete
if [ -n "\$delete" ]; then
esection "Supprimer la branche"
EOF
_mscript_delete_branch
hook="AFTER_DELETE_$PMAN_MERGE_SRC"; [ -n "${!hook}" ] && _scripta <<EOF
(
${!hook}
)$or_die
EOF
_scripta <<EOF
fi
EOF
fi
_scripta <<EOF
################################################################################
# push
if [ -n "\$push" ]; then
esection "Pousser les branches"
EOF
hook="BEFORE_PUSH_$PMAN_MERGE_DEST"; [ -n "${!hook}" ] && _scripta <<EOF
(
${!hook}
)$or_die
EOF
_script_push_branches
if [ ${#delete_branches[*]} -gt 0 ]; then
_scripta <<<"if [ -n \"\$delete\" ]; then"
push_branches=("${delete_branches[@]}")
_script_push_branches
_scripta <<<fi
fi
hook="AFTER_PUSH_$PMAN_MERGE_DEST"; [ -n "${!hook}" ] && _scripta <<EOF
(
${!hook}
)$or_die
EOF
_scripta <<EOF
fi
EOF
[ -n "$Delete" -o -z "$ShouldDelete" ] && Deleted=1 || Deleted=
[ -n "$ShouldDelete" -a -n "$Delete" ] && ShouldDelete=
[ -n "$ShouldPush" -a -n "$Push" ] && ShouldPush=
if [ -n "$_Fake" ]; then
cat "$script"
elif ! "$script" merge ${Delete:+delete} ${Push:+push}; then
eimportant "\
Le script $script a été lancé avec les arguments 'merge${Delete:+ delete}${Push:+ push}'
En cas d'erreur de merge, veuillez corriger les erreurs puis continuer avec
git merge --continue
Sinon, veuillez consulter le script et/ou le relancer
./$script${Delete:+ delete}${Push:+ push}"
die
elif [ -n "$Deleted" -a -n "$Push" ]; then
[ -n "$_KeepScript" ] || rm "$script"
[ -n "$AfterMerge" ] && eval "$AfterMerge"
else
local msg="\
Le script $script a été lancé avec les arguments 'merge${Delete:+ delete}${Push:+ push}'
Vous pouvez consulter le script et/ou le relancer
./$script${ShouldDelete:+ delete}${ShouldPush:+ push}"
[ -n "$AfterMerge" ] && msg="$msg
Il y a aussi les commandes supplémentaires suivantes:
${AfterMerge//
/
}"
einfo "$msg"
fi
}
function merge_action() {
[ -n "$PMAN_UNIQUE" ] || resolve_unique_branch "$@"
ensure_merge_branches -a
if [ -n "$PMAN_PREL_MERGE" ]; then
[ -n "$ForceMerge" ] || die "$PmanMergeSrc: cette branche doit être fusionnée dans $PmanMergeDest avec prel"
fi
if [ -n "$PMAN_DELETE_MERGED" ]; then
ShouldDelete=1
[ -n "$AfterMerge" ] || setx AfterMerge=qvals git checkout -q "$PmanMergeDest"
else
ShouldDelete=
Delete=
[ -n "$AfterMerge" ] || setx AfterMerge=qvals git checkout -q "$PmanMergeSrc"
fi
[ -z "$_Fake" ] && git_ensure_cleancheckout
if ! array_contains LocalBranches "$PmanMergeSrc" && array_contains AllBranches "$PmanMergeSrc"; then
enote "$PmanMergeSrc: une branche du même nom existe dans l'origine"
fi
if ! array_contains LocalBranches "$PmanMergeDest" && array_contains AllBranches "$PmanMergeDest"; then
enote "$PmanMergeDest: une branche du même nom existe dans l'origine"
fi
array_contains LocalBranches "$PmanMergeSrc" || die "$PmanMergeSrc: branche locale introuvable"
array_contains LocalBranches "$PmanMergeDest" || die "$PmanMergeDest: branche locale introuvable"
resolve_should_push
_merge_action "$@"
}
function rebase_action() {
die "non implémenté"
}

View File

@ -1,9 +1,5 @@
# -*- 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
## configuration de la branche 7.4 d'un projet PHP multiversion
# il s'agit d'un projet avec deux branches parallèles: 7.4 et 8.2, les
# modifications de la 7.4 étant incluses dans la branche 8.2
UPSTREAM= UPSTREAM=
DEVELOP=dev74 DEVELOP=dev74
FEATURE=wip74/ FEATURE=wip74/

View File

@ -1,9 +1,5 @@
# -*- 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
## configuration de la branche 8.2 d'un projet PHP multiversion
# il s'agit d'un projet avec deux branches parallèles: 7.4 et 8.2, les
# modifications de la 7.4 étant incluses dans la branche 8.2
UPSTREAM=dev74 UPSTREAM=dev74
DEVELOP=dev82 DEVELOP=dev82
FEATURE=wip82/ FEATURE=wip82/

1
bin/.cachectl.php Symbolic link
View File

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

1
bin/cachectl.php Symbolic link
View File

@ -0,0 +1 @@
runphp

View File

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

View File

@ -19,6 +19,7 @@ while true; do
fi fi
cd .. cd ..
done done
cd "$owd"
export RUNPHP_MOUNT= export RUNPHP_MOUNT=
if [ "$MYNAME" == composer ]; then if [ "$MYNAME" == composer ]; then

View File

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

7
php/bin/cachectl.php Executable file
View File

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

132
php/cli/CachectlApp.php Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,8 +1,5 @@
# nulib\app # nulib\app
* [ ] ajouter des méthodes normalisées `app::get_cachedir()` et
`app::get_cachefile($name)` avec la valeur par défaut
`cachedir = $vardir/cache`
* [ ] `app::action()` et `app::step()` appellent automatiquement * [ ] `app::action()` et `app::step()` appellent automatiquement
`app::_dispatch_signals()` `app::_dispatch_signals()`

View File

@ -5,12 +5,13 @@ use nulib\A;
use nulib\app\cli\Application; use nulib\app\cli\Application;
use nulib\app\config\ProfileManager; use nulib\app\config\ProfileManager;
use nulib\cl; use nulib\cl;
use nulib\exceptions;
use nulib\ExitError; use nulib\ExitError;
use nulib\os\path; use nulib\os\path;
use nulib\os\sh; use nulib\os\sh;
use nulib\php\func; use nulib\php\func;
use nulib\ref\ref_profiles;
use nulib\str; use nulib\str;
use nulib\ValueException;
class app { class app {
private static function isa_Application($app): bool { private static function isa_Application($app): bool {
@ -35,6 +36,7 @@ class app {
"datadir" => $app::DATADIR, "datadir" => $app::DATADIR,
"etcdir" => $app::ETCDIR, "etcdir" => $app::ETCDIR,
"vardir" => $app::VARDIR, "vardir" => $app::VARDIR,
"cachedir" => $app::CACHEDIR,
"logdir" => $app::LOGDIR, "logdir" => $app::LOGDIR,
"appgroup" => $app::APPGROUP, "appgroup" => $app::APPGROUP,
"name" => $app::NAME, "name" => $app::NAME,
@ -50,6 +52,7 @@ class app {
"datadir" => constant("$app::DATADIR"), "datadir" => constant("$app::DATADIR"),
"etcdir" => constant("$app::ETCDIR"), "etcdir" => constant("$app::ETCDIR"),
"vardir" => constant("$app::VARDIR"), "vardir" => constant("$app::VARDIR"),
"cachedir" => constant("$app::CACHEDIR"),
"logdir" => constant("$app::LOGDIR"), "logdir" => constant("$app::LOGDIR"),
"appgroup" => constant("$app::APPGROUP"), "appgroup" => constant("$app::APPGROUP"),
"name" => constant("$app::NAME"), "name" => constant("$app::NAME"),
@ -58,7 +61,7 @@ class app {
} elseif (is_array($app)) { } elseif (is_array($app)) {
$params = $app; $params = $app;
} else { } else {
throw ValueException::invalid_type($app, Application::class); throw exceptions::invalid_type($app, "app", Application::class);
} }
return $params; return $params;
} }
@ -83,6 +86,7 @@ class app {
"datadir", "datadir",
"etcdir", "etcdir",
"vardir", "vardir",
"cachedir",
"logdir", "logdir",
"profile", "profile",
"facts", "facts",
@ -115,12 +119,20 @@ class app {
return self::get()->getProfile($productionMode); return self::get()->getProfile($productionMode);
} }
static function is_production_mode(): bool {
return self::get()->isProductionMode();
}
static function is_prod(): bool { static function is_prod(): bool {
return self::get_profile() === "prod"; return self::get_profile() === ref_profiles::PROD;
}
static function is_test(): bool {
return self::get_profile() === ref_profiles::TEST;
} }
static function is_devel(): bool { static function is_devel(): bool {
return self::get_profile() === "devel"; return self::get_profile() === ref_profiles::DEVEL;
} }
static function set_profile(?string $profile=null, ?bool $productionMode=null): void { static function set_profile(?string $profile=null, ?bool $productionMode=null): void {
@ -163,6 +175,7 @@ class app {
"datadir" => $datadir, "datadir" => $datadir,
"etcdir" => $etcdir, "etcdir" => $etcdir,
"vardir" => $vardir, "vardir" => $vardir,
"cachedir" => $cachedir,
"logdir" => $logdir, "logdir" => $logdir,
] = $params; ] = $params;
$cwd = $params["cwd"] ?? null; $cwd = $params["cwd"] ?? null;
@ -214,6 +227,11 @@ class app {
if ($vardir === false) $vardir = $params["vardir"] ?? null; if ($vardir === false) $vardir = $params["vardir"] ?? null;
if ($vardir === null) $vardir = "var"; if ($vardir === null) $vardir = "var";
$vardir = path::reljoin($datadir, $vardir); $vardir = path::reljoin($datadir, $vardir);
# cachedir
$cachedir = getenv("${PROJCODE}_CACHEDIR");
if ($cachedir === false) $cachedir = $params["cachedir"] ?? null;
if ($cachedir === null) $cachedir = "cache";
$cachedir = path::reljoin($vardir, $cachedir);
# logdir # logdir
$logdir = getenv("${PROJCODE}_LOGDIR"); $logdir = getenv("${PROJCODE}_LOGDIR");
if ($logdir === false) $logdir = $params["logdir"] ?? null; if ($logdir === false) $logdir = $params["logdir"] ?? null;
@ -241,6 +259,7 @@ class app {
$this->datadir = $datadir; $this->datadir = $datadir;
$this->etcdir = $etcdir; $this->etcdir = $etcdir;
$this->vardir = $vardir; $this->vardir = $vardir;
$this->cachedir = $cachedir;
$this->logdir = $logdir; $this->logdir = $logdir;
# name, title # name, title
@ -310,6 +329,12 @@ class app {
return $this->vardir; return $this->vardir;
} }
protected string $cachedir;
function getCachedir(): string {
return $this->cachedir;
}
protected string $logdir; protected string $logdir;
function getLogdir(): string { function getLogdir(): string {
@ -401,7 +426,7 @@ class app {
function fencedJoin(string $basedir, ?string ...$paths): string { function fencedJoin(string $basedir, ?string ...$paths): string {
$path = path::reljoin($basedir, ...$paths); $path = path::reljoin($basedir, ...$paths);
if (!path::is_within($path, $basedir)) { if (!path::is_within($path, $basedir)) {
throw ValueException::invalid_value($path, "path"); throw exceptions::invalid_value($path, "path");
} }
return $path; return $path;
} }
@ -440,6 +465,7 @@ class app {
"datadir" => $this->datadir, "datadir" => $this->datadir,
"etcdir" => $this->etcdir, "etcdir" => $this->etcdir,
"vardir" => $this->vardir, "vardir" => $this->vardir,
"cachedir" => $this->cachedir,
"logdir" => $this->logdir, "logdir" => $this->logdir,
"profile" => $this->getProfile(), "profile" => $this->getProfile(),
"facts" => $this->facts, "facts" => $this->facts,
@ -455,7 +481,7 @@ class app {
* une valeur de la forme "$ETCDIR/$name[.$profile].conf" * une valeur de la forme "$ETCDIR/$name[.$profile].conf"
*/ */
function getEtcfile(?string $name=null, $profile=null): string { function getEtcfile(?string $name=null, $profile=null): string {
if ($name === null) $name = "{$this->name}.conf"; $name ??= "{$this->name}.conf";
return $this->findFile([$this->etcdir], [$name], $profile); return $this->findFile([$this->etcdir], [$name], $profile);
} }
@ -464,13 +490,25 @@ class app {
* valeur de la forme "$VARDIR/$appgroup/$name[.$profile].tmp" * valeur de la forme "$VARDIR/$appgroup/$name[.$profile].tmp"
*/ */
function getVarfile(?string $name=null, $profile=null): string { function getVarfile(?string $name=null, $profile=null): string {
if ($name === null) $name = "{$this->name}.tmp"; $name ??= "{$this->name}.tmp";
$file = $this->fencedJoin($this->vardir, $this->appgroup, $name); $file = $this->fencedJoin($this->vardir, $this->appgroup, $name);
$file = $this->withProfile($file, $profile); $file = $this->withProfile($file, $profile);
sh::mkdirof($file); sh::mkdirof($file);
return $file; return $file;
} }
/**
* obtenir le chemin vers le fichier de cache. par défaut, retourner une
* valeur de la forme "$CACHEDIR/$appgroup/$name[.$profile].cache"
*/
function getCachefile(?string $name=null, $profile=null): string {
$name ??= "{$this->name}.cache";
$file = $this->fencedJoin($this->cachedir, $this->appgroup, $name);
$file = $this->withProfile($file, $profile);
sh::mkdirof($file);
return $file;
}
/** /**
* obtenir le chemin vers le fichier de log. par défaut, retourner une * obtenir le chemin vers le fichier de log. par défaut, retourner une
* valeur de la forme "$LOGDIR/$appgroup/$name.log" (sans le profil, parce * valeur de la forme "$LOGDIR/$appgroup/$name.log" (sans le profil, parce
@ -484,10 +522,10 @@ class app {
$name = "{$this->name}.log"; $name = "{$this->name}.log";
$profile ??= false; $profile ??= false;
} }
$file = $this->fencedJoin($this->logdir, $this->appgroup, $name); $logfile = $this->fencedJoin($this->logdir, $this->appgroup, $name);
$file = $this->withProfile($file, $profile); $logfile = $this->withProfile($logfile, $profile);
sh::mkdirof($file); sh::mkdirof($logfile);
return $file; return $logfile;
} }
/** /**

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -10,10 +10,14 @@ use nulib\app\config;
use nulib\app\RunFile; use nulib\app\RunFile;
use nulib\ExitError; use nulib\ExitError;
use nulib\ext\yaml; use nulib\ext\yaml;
use nulib\output\console; use nulib\output\con;
use nulib\output\log; use nulib\output\log;
use nulib\output\msg; use nulib\output\msg;
use nulib\output\std\StdMessenger; use nulib\output\say;
use nulib\output\std\ConsoleMessenger;
use nulib\output\std\LogMessenger;
use nulib\output\std\ProxyMessenger;
use nulib\ref\ref_profiles;
/** /**
* Class Application: application de base * Class Application: application de base
@ -63,6 +67,7 @@ abstract class Application {
const DATADIR = null; const DATADIR = null;
const ETCDIR = null; const ETCDIR = null;
const VARDIR = null; const VARDIR = null;
const CACHEDIR = null;
const LOGDIR = null; const LOGDIR = null;
/** @var bool faut-il activer automatiquement l'écriture dans les logs */ /** @var bool faut-il activer automatiquement l'écriture dans les logs */
@ -191,26 +196,30 @@ EOT);
protected static function _initialize_app(): void { protected static function _initialize_app(): void {
app::init(static::class); app::init(static::class);
app::set_fact(app::FACT_CLI_APP); app::set_fact(app::FACT_CLI_APP);
msg::set_messenger(new StdMessenger([ $con = new ConsoleMessenger([
"min_level" => msg::DEBUG, "min_level" => msg::DEBUG,
])); ]);
say::set_messenger($con);
msg::set_messenger($con);
} }
protected static function _configure_app(Application $app): void { protected static function _configure_app(Application $app): void {
config::configure(config::CONFIGURE_INITIAL_ONLY); config::configure(config::CONFIGURE_INITIAL_ONLY);
$msgs = null; $con = con::set_messenger(new ConsoleMessenger([
$msgs["console"] = new StdMessenger([ "min_level" => con::NORMAL,
"min_level" => msg::NORMAL, ]));
]); say::set_messenger($con, true);
msg::set_messenger($con, true);
if (static::USE_LOGFILE) { if (static::USE_LOGFILE) {
$msgs["log"] = new StdMessenger([ $log = log::set_messenger(new LogMessenger([
"output" => app::get()->getLogfile(), "output" => app::get()->getLogfile(),
"min_level" => msg::MINOR, "min_level" => msg::MINOR,
"add_date" => true, ]));
]); } else {
$log = log::set_messenger(new ProxyMessenger());
} }
msg::init($msgs); msg::set_messenger($log);
$app->parseArgs(); $app->parseArgs();
config::configure(); config::configure();
@ -257,9 +266,9 @@ EOT);
"action" => [app::class, "set_profile"], "action" => [app::class, "set_profile"],
"help" => "spécifier le profil d'exécution", "help" => "spécifier le profil d'exécution",
], ],
["-P", "--prod", "action" => [app::class, "set_profile", "prod"]], ["-P", "--prod", "action" => [app::class, "set_profile", ref_profiles::PROD]],
["-T", "--test", "action" => [app::class, "set_profile", "test"]], ["-T", "--test", "action" => [app::class, "set_profile", ref_profiles::TEST]],
["--devel", "action" => [app::class, "set_profile", "devel"]], ["--devel", "action" => [app::class, "set_profile", ref_profiles::DEVEL]],
], ],
]; ];
@ -267,26 +276,36 @@ EOT);
"title" => "NIVEAU D'INFORMATION", "title" => "NIVEAU D'INFORMATION",
"show" => false, "show" => false,
["group", ["group",
["--verbosity", ["-V", "--verbosity",
"args" => "verbosity", "argsdesc" => "silent|quiet|verbose|debug", "args" => "verbosity", "argsdesc" => "silent|quiet|verbose|debug",
"action" => [console::class, "set_verbosity"], "action" => [con::class, "set_verbosity"],
"help" => "spécifier le niveau d'informations affiché", "help" => "Spécifier le niveau d'informations affiché sur la console",
], ],
["-q", "--quiet", "action" => [console::class, "set_verbosity", "quiet"]], ["-q", "--quiet", "action" => [con::class, "set_verbosity", "quiet"]],
["-v", "--verbose", "action" => [console::class, "set_verbosity", "verbose"]], ["-v", "--verbose", "action" => [con::class, "set_verbosity", "verbose"]],
["-D", "--debug", "action" => [console::class, "set_verbosity", "debug"]], ["-D", "--debug", "action" => [con::class, "set_verbosity", "debug"]],
],
["-L", "--logfile",
"args" => "output",
"action" => [log::class, "set_output"],
"help" => "Logger les messages de l'application dans le fichier spécifié",
], ],
["group", ["group",
["--color", ["--color",
"action" => [console::class, "set_color", true], "action" => [con::class, "set_color", true],
"help" => "Afficher (resp. ne pas afficher) la sortie en couleur par défaut", "help" => "Afficher (resp. ne pas afficher) la sortie en couleur par défaut",
], ],
["--no-color", "action" => [console::class, "set_color", false]], ["--no-color", "action" => [con::class, "set_color", false]],
],
["group",
["-L", "--logfile",
"args" => "output",
"action" => [log::class, "set_output"],
"help" => "Logger les messages de l'application dans le fichier spécifié",
],
["--lV", "--lverbosity",
"args" => "verbosity", "argsdesc" => "silent|quiet|verbose|debug",
"action" => [log::class, "set_verbosity"],
"help" => "Spécifier le niveau des informations ajoutées dans les logs",
],
["--lq", "--lquiet", "action" => [log::class, "set_verbosity", "quiet"]],
["--lv", "--lverbose", "action" => [log::class, "set_verbosity", "verbose"]],
["--lD", "--ldebug", "action" => [log::class, "set_verbosity", "debug"]],
], ],
]; ];
@ -307,9 +326,9 @@ EOT);
} }
const PROFILE_COLORS = [ const PROFILE_COLORS = [
"prod" => "@r", ref_profiles::PROD => "@r",
"test" => "@g", ref_profiles::TEST => "@g",
"devel" => "@w", ref_profiles::DEVEL => "@w",
]; ];
const DEFAULT_PROFILE_COLOR = "y"; const DEFAULT_PROFILE_COLOR = "y";

View File

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

View File

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

View File

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

50
php/src/cache/CacheData.php vendored Normal file
View File

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

356
php/src/cache/CacheFile.php vendored Normal file
View File

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

68
php/src/cache/CacheManager.php vendored Normal file
View File

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

40
php/src/cache/CursorCacheData.php vendored Normal file
View File

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

127
php/src/cache/CursorChannel.php vendored Normal file
View File

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

62
php/src/cache/DataCacheData.php vendored Normal file
View File

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

6
php/src/cache/TODO.md vendored Normal file
View File

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

93
php/src/cache/cache.php vendored Normal file
View File

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

View File

@ -166,6 +166,12 @@ class cv {
############################################################################# #############################################################################
/** retourner $value si elle est non nulle, lancer une exception sinon */
static final function not_null($value, ?string $kind=null) {
if ($value !== null) return $value;
throw exceptions::null_value($kind);
}
/** vérifier si $value est un booléen, sinon retourner null */ /** vérifier si $value est un booléen, sinon retourner null */
static final function check_bool($value): ?bool { static final function check_bool($value): ?bool {
return is_bool($value)? $value: null; return is_bool($value)? $value: null;
@ -192,11 +198,11 @@ class cv {
* *
* lever une exception si $value n'est d'aucun de ces types * lever une exception si $value n'est d'aucun de ces types
*/ */
static final function check_key($value, ?string $prefix=null, bool $throw_exception=true): array { static final function check_key($value, ?string $kind=null, bool $throwException=true): array {
$index = is_int($value)? $value : null; $index = is_int($value)? $value : null;
$key = is_string($value)? $value : null; $key = is_string($value)? $value : null;
if ($index === null && $key === null && $throw_exception) { if ($index === null && $key === null && $throwException) {
throw ValueException::invalid_kind($value, "key", $prefix); throw exceptions::invalid_type($value, $kind, "key");
} else { } else {
return [$index, $key]; return [$index, $key];
} }
@ -208,12 +214,12 @@ class cv {
* *
* @throws ValueException si $value n'est d'aucun de ces types * @throws ValueException si $value n'est d'aucun de ces types
*/ */
static final function check_bsa($value, ?string $prefix=null, bool $throw_exception=true): array { static final function check_bsa($value, ?string $kind=null, bool $throwException=true): array {
$bool = is_bool($value)? $value : null; $bool = is_bool($value)? $value : null;
$scalar = !is_bool($value) && is_scalar($value)? $value : null; $scalar = !is_bool($value) && is_scalar($value)? $value : null;
$array = is_array($value)? $value : null; $array = is_array($value)? $value : null;
if ($bool === null && $scalar === null && $array === null && $throw_exception) { if ($bool === null && $scalar === null && $array === null && $throwException) {
throw ValueException::invalid_kind($value, "value", $prefix); throw exceptions::invalid_type($value, $kind, ["bool", "scalar", "array"]);
} else { } else {
return [$bool, $scalar, $array]; return [$bool, $scalar, $array];
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -3,21 +3,13 @@ namespace nulib\db\sqlite;
use nulib\cl; use nulib\cl;
use nulib\db\CapacitorChannel; use nulib\db\CapacitorChannel;
use nulib\db\Capacitor; use nulib\db\CapacitorStorage;
/** /**
* Class SqliteStorage * Class SqliteStorage
*/ */
class SqliteCapacitor extends Capacitor { class SqliteStorage extends CapacitorStorage {
const CDATA_DEFINITION = "mediumtext"; const GENSERIAL_DEFINITION = "integer primary key autoincrement";
const CSUM_DEFINITION = "varchar(40)";
const CTIMESTAMP_DEFINITION = "datetime";
const GSERIAL_DEFINITION = "integer primary key autoincrement";
const GLIC_DEFINITION = "varchar(80)";
const GLIB_DEFINITION = "varchar(255)";
const GTEXT_DEFINITION = "mediumtext";
const GBOOL_DEFINITION = "integer(1) default 0";
const GUUID_DEFINITION = "varchar(36)";
function __construct($sqlite) { function __construct($sqlite) {
$this->db = Sqlite::with($sqlite); $this->db = Sqlite::with($sqlite);
@ -39,30 +31,30 @@ class SqliteCapacitor extends Capacitor {
return $found !== null; return $found !== null;
} }
function getMigration(CapacitorChannel $channel): _sqliteMigration { function _getMigration(CapacitorChannel $channel): _sqliteMigration {
$migrations = cl::merge([ $migrations = cl::merge([
"0init" => [$this->getCreateChannelSql($channel)], "0init" => [$this->_createSql($channel)],
], $channel->getMigration($this->db->getPrefix())); ], $channel->getMigration($this->db->getPrefix()));
return new _sqliteMigration($migrations, $channel->getName()); return new _sqliteMigration($migrations, $channel->getName());
} }
protected function addToCatalogSql(CapacitorChannel $channel): array { protected function _addToChannelsSql(CapacitorChannel $channel): array {
$sql = parent::addToCatalogSql($channel); $sql = parent::_addToChannelsSql($channel);
$sql[0] = "insert or ignore"; $sql[0] = "insert or ignore";
return $sql; return $sql;
} }
protected function afterCreate(CapacitorChannel $channel): void { protected function _afterCreate(CapacitorChannel $channel): void {
$db = $this->db; $db = $this->db;
if (!$this->tableExists(static::CATALOG_TABLE)) { 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($this->getCreateCatalogSql()); $db->exec($this->_createChannelsSql());
} }
if (!$this->isInCatalog(["name" => $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($this->addToCatalogSql($channel)); $db->exec($this->_addToChannelsSql($channel));
} }
} }

253
php/src/exceptions.php Normal file
View File

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

View File

@ -58,8 +58,8 @@ class file {
return $file; return $file;
} }
static function writer($output, ?string $mode="w+b", ?callable $func=null): FileWriter { static function writer($output, ?callable $func=null): FileWriter {
$file = new FileWriter(self::fix_dash($output), $mode); $file = new FileWriter(self::fix_dash($output), "w+b");
if ($func !== null) { if ($func !== null) {
try { try {
$func($file); $func($file);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,7 +1,9 @@
<?php <?php
namespace nulib\mail; namespace nulib\mail;
use nulib\app\config;
use nulib\cl; use nulib\cl;
use nulib\web\session;
class MailTemplateHelper { class MailTemplateHelper {
function __construct(?array $data) { function __construct(?array $data) {

View File

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

View File

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

View File

@ -1,8 +1,13 @@
# nulib\output # nulib\output
* dans msg::action($m, function() {}), *bloquer* la marque pour empêcher d'aller * log:: permet d'ajouter autant d'instance de LogMessenger que nécessaire
plus bas que prévu. comme ça s'il y a plusieurs success ou failure dans la * on pourrait qualifier un logger avec par exemple la classe qui l'appelle
fonction, c'est affiché correctement. ou le nom d'un sous-système.
* pour un log structuré, un attribut donnerai le qualificatif, ce qui ne
serait pris en compte que par le logger approprié (e.g un logger qui est
responsable de nulib/io logguera les message de nulib/io/ClassA mais pas
les messages de nulib/args/ClassB
* un trait permet d'ajouter un logger à une classe
* [ ] possibilité de paramétrer le nom du fichier destination pour faire une * [ ] possibilité de paramétrer le nom du fichier destination pour faire une
rotation des logs rotation des logs
@ -13,11 +18,6 @@
* [ ] dans `StdMessenger::resetParams()`, `[output]` peut être une instance de * [ ] dans `StdMessenger::resetParams()`, `[output]` peut être une instance de
StdOutput pour mettre à jour $out ET $err, ou un tableau de deux éléments pour StdOutput pour mettre à jour $out ET $err, ou un tableau de deux éléments pour
mettre à jour séparément $out et $err mettre à jour séparément $out et $err
* [ ] vérifier que la date affichée pour un TITLE est celle à laquelle l'appel
a été fait, même si le premier événement en dessous arrive bien plus tard
* [ ] pareil pour action: sauf si c'est une seule ligne, la date de action est
la date du premier appel, alors que la date de $result est celui du result si
c'est affiché sur une autre ligne
* réorganiser pour que msg:: attaque un proxy dans lequel est configuré un * réorganiser pour que msg:: attaque un proxy dans lequel est configuré un
ensemble standard de sorties: say, log, debuglog ensemble standard de sorties: say, log, debuglog
* `--aD, --av, --aq, --asilent` pour les valeurs d'ajustement qui sont un * `--aD, --av, --aq, --asilent` pour les valeurs d'ajustement qui sont un

View File

@ -0,0 +1,67 @@
<?php
namespace nulib\output;
use nulib\app\app;
use nulib\exceptions;
use nulib\output\std\NullMessenger;
use nulib\output\std\ProxyMessenger;
trait _TMessenger {
protected static ?IMessenger $msg = null;
static function set_messenger(IMessenger $msg, bool $replace=false): IMessenger {
if (self::$msg instanceof NullMessenger) self::$msg = null;
if ($replace || self::$msg === null) {
return self::$msg = $msg;
} elseif (self::$msg instanceof ProxyMessenger) {
self::$msg->addMessenger($msg);
} else {
self::$msg = new ProxyMessenger(self::$msg);
self::$msg->addMessenger($msg);
}
return $msg;
}
static function get(): IMessenger {
return self::$msg ??= new NullMessenger();
}
static function set_verbosity(string $verbosity): void {
$msg = self::get();
switch ($verbosity) {
case "Q":
case "silent":
$msg->resetParams([
"min_level" => self::NONE,
]);
break;
case "q":
case "quiet":
$msg->resetParams([
"min_level" => self::MAJOR,
]);
break;
case "n":
case "normal":
$msg->resetParams([
"min_level" => self::NORMAL,
]);
break;
case "v":
case "verbose":
$msg->resetParams([
"min_level" => self::MINOR,
]);
break;
case "D":
case "debug":
app::set_debug();
$msg->resetParams([
"min_level" => self::DEBUG,
]);
break;
default:
throw exceptions::forbidden_value($verbosity, "verbosity", ["silent", "quiet", "normal", "verbose", "debug"]);
}
}
}

View File

@ -1,44 +1,16 @@
<?php <?php
namespace nulib\output; namespace nulib\output;
use nulib\cl;
use nulib\str;
use nulib\ValueException;
/**
* Class _messenger: classe de base pour say, log et msg
*/
abstract class _messenger { abstract class _messenger {
abstract static function is_setup(): bool; abstract static function set_messenger(IMessenger $msg, bool $replace=false): IMessenger;
abstract static function set_messenger(IMessenger $msg);
abstract static function get(): IMessenger; abstract static function get(): IMessenger;
static function set_messenger_class(string $msg_class, ?array $params=null) {
if (!is_subclass_of($msg_class, IMessenger::class)) {
throw ValueException::invalid_class($msg_class, IMessenger::class);
}
static::set_messenger(new $msg_class($params));
}
static function create_or_reset_params(?array $params=null, string $log_class=null, ?array $create_params=null) {
if (static::is_setup()) {
self::reset_params($params);
} else {
$params = cl::merge($params, $create_params);
self::set_messenger_class($log_class, $params);
}
}
/** obtenir une nouvelle instance, avec un nouveau paramétrage */ /** obtenir une nouvelle instance, avec un nouveau paramétrage */
static function new(?array $params=null): IMessenger { static function new(?array $params=null): IMessenger {
return static::get()->clone($params); return static::get()->clone($params);
} }
static final function __callStatic($name, $args) {
$name = str::us2camel($name);
call_user_func_array([static::get(), $name], $args);
}
############################################################################# #############################################################################
const DEBUG = IMessenger::DEBUG; const DEBUG = IMessenger::DEBUG;

25
php/src/output/con.php Normal file
View File

@ -0,0 +1,25 @@
<?php
namespace nulib\output;
use nulib\output\std\ConsoleMessenger;
/**
* Class con: afficher un message à l'utilisateur sur la console (c'est à dire
* la sortie standard pour {@link con::print()}, et la sortie d'erreur pour
* toutes les autres méthodes)
*
* Cette classe est utilisable sans initialisation préalable
*/
class con extends _messenger {
use _TMessenger;
static function get(): IMessenger {
return static::$msg ??= new ConsoleMessenger();
}
static function set_color(bool $color=true): void {
self::get()->resetParams([
"color" => $color,
]);
}
}

View File

@ -1,75 +0,0 @@
<?php
namespace nulib\output;
use nulib\app\app;
use nulib\output\std\ProxyMessenger;
use nulib\ValueException;
/**
* Class console: afficher un message sur la console
*
* Cette classe DOIT être initialisée avant d'être utilisée
*/
class console extends _messenger {
private static ?IMessenger $msg = null;
private static bool $setup = false;
static function is_setup(): bool {
return self::$setup;
}
static function set_messenger(IMessenger $msg) {
self::$msg = $msg;
self::$setup = true;
}
static function get(): IMessenger {
return self::$msg ??= new ProxyMessenger();
}
static function set_verbosity(string $verbosity): void {
$console = self::get();
switch ($verbosity) {
case "Q":
case "silent":
$console->resetParams([
"min_level" => msg::NONE,
]);
break;
case "q":
case "quiet":
$console->resetParams([
"min_level" => msg::MAJOR,
]);
break;
case "n":
case "normal":
$console->resetParams([
"min_level" => msg::NORMAL,
]);
break;
case "v":
case "verbose":
$console->resetParams([
"min_level" => msg::MINOR,
]);
break;
case "D":
case "debug":
app::set_debug();
$console->resetParams([
"min_level" => msg::DEBUG,
]);
break;
default:
throw ValueException::invalid_value($verbosity, "verbosity");
}
}
static function set_color(bool $color=true): void {
console::reset_params([
"color" => $color,
]);
}
}

View File

@ -1,38 +1,40 @@
<?php <?php
namespace nulib\output; namespace nulib\output;
use nulib\output\std\LogMessenger;
use nulib\output\std\ProxyMessenger; use nulib\output\std\ProxyMessenger;
use nulib\output\std\StdMessenger;
/** /**
* Class log: inscrire un message dans les logs uniquement * Class log: ajouter un message dans les logs
* *
* Cette classe DOIT être initialisée avant d'être utilisée * Cette classe DEVRAIT être initialisée avant utilisation. Sinon, elle envoie
* ses messages vers /dev/null
*
* mode | con | web | log
* ------|-----|-----|-----
* cli | | | x
* web | | | x
*/ */
class log extends _messenger { class log extends _messenger {
private static ?IMessenger $msg = null; use _TMessenger;
private static bool $setup = false;
static function is_setup(): bool {
return self::$setup;
}
static function set_messenger(IMessenger $msg) {
self::$msg = $msg;
self::$setup = true;
}
static function get(): IMessenger { static function get(): IMessenger {
return self::$msg ??= new ProxyMessenger(); return self::$msg ??= static::$msg = new ProxyMessenger();
}
protected static function ensure_log(): IMessenger {
$msg = self::$msg;
if ($msg instanceof ProxyMessenger && $msg->isEmpty()) {
$msg->addMessenger(new LogMessenger([
"min_level" => msg::MINOR,
]));
}
return $msg;
} }
static function set_output(string $logfile): void { static function set_output(string $logfile): void {
self::create_or_reset_params([ self::ensure_log()->resetParams([
"output" => $logfile, "output" => $logfile,
], StdMessenger::class, [
"add_date" => true,
"min_level" => self::MINOR,
]); ]);
} }
} }

View File

@ -1,67 +1,18 @@
<?php <?php
namespace nulib\output; namespace nulib\output;
use nulib\output\std\ProxyMessenger;
use nulib\php\func;
/** /**
* Class msg: inscrire un message dans les logs ET l'afficher à l'utilisateur * Class msg: afficher un message à l'utilisateur et l'ajouter aussi dans les
* logs
* *
* Cette classe DOIT être initialisée avec {@link set_messenger()} ou * Cette classe DEVRAIT être initialisée avant utilisation. Sinon, elle envoie
* {@link set_messenger_class()} avant d'être utilisée. * ses messages vers /dev/null
*
* mode | con | web | log
* ------|-----|-----|-----
* cli | x | | x
* web | | x | x
*/ */
class msg extends _messenger { class msg extends _messenger {
private static ?IMessenger $msg = null; use _TMessenger;
private static bool $setup = false;
static function is_setup(): bool {
return self::$setup;
}
static function set_messenger(IMessenger $msg) {
self::$msg = $msg;
self::$setup = true;
}
static function get(): IMessenger {
return self::$msg ??= new ProxyMessenger();
}
/**
* initialiser les instances say, console, log.
*/
static function init(array $msgs) {
$say = $msgs["say"] ?? null;
$console = $msgs["console"] ?? null;
$log = $msgs["log"] ?? null;
$msgs = [];
if ($log !== null && $log !== false) {
if ($log instanceof IMessenger) log::set_messenger($log);
elseif (is_string($log)) log::set_messenger_class($log);
else $log = func::call($log);
log::set_messenger($log);
$msgs[] = $log;
}
if ($console !== null && $console !== false) {
if ($console instanceof IMessenger) console::set_messenger($console);
elseif (is_string($console)) console::set_messenger_class($console);
else $console = func::call($console);
console::set_messenger($console);
$msgs[] = $console;
}
if ($say !== null && $say !== false) {
if ($say instanceof IMessenger) say::set_messenger($say);
elseif (is_string($say)) say::set_messenger_class($say);
else $say = func::call($say);
say::set_messenger($say);
$msgs[] = $say;
}
if ($say === null && $console !== null) {
say::set_messenger($console);
} elseif ($console === null && $say !== null) {
console::set_messenger($say);
}
self::set_messenger(new ProxyMessenger(...$msgs));
}
} }

View File

@ -1,34 +0,0 @@
<?php
namespace nulib\output;
use nulib\output\std\StdOutput;
/**
* Class out: affichage sur la sortie standard
*/
class out {
/** @var StdOutput */
private static $out;
static function get(): StdOutput {
return self::$out;
}
protected static function set(StdOutput $out): StdOutput {
return self::$out = $out;
}
/** reparamétrer l'instance */
static function reset($output=null, ?array $params=null): StdOutput {
if (self::$out === null) return self::set(new StdOutput($output, $params));
if ($output !== null) $params["output"] = $output;
self::$out->resetParams($params);
return self::$out;
}
static function write(...$values): void { self::$out->write(...$values); }
static function print(...$values): void { self::$out->print(...$values); }
static function iwrite(int $indentLevel, ...$values): void { self::$out->iwrite($indentLevel, ...$values); }
static function iprint(int $indentLevel, ...$values): void { self::$out->iprint($indentLevel, ...$values); }
}
out::reset();

View File

@ -1,28 +1,17 @@
<?php <?php
namespace nulib\output; namespace nulib\output;
use nulib\output\std\ProxyMessenger;
/** /**
* Class say: afficher un message pour l'utilisateur * Class say: afficher un message à l'utilisateur
* *
* Cette classe DOIT être initialisée avant d'être utilisée * Cette classe DEVRAIT être initialisée avant utilisation. Sinon, elle envoie
* ses messages vers /dev/null
*
* mode | con | web | log
* ------|-----|-----|-----
* cli | x | |
* web | | x |
*/ */
class say extends _messenger { class say extends _messenger {
private static ?IMessenger $msg = null; use _TMessenger;
private static bool $setup = false;
static function is_setup(): bool {
return self::$setup;
}
static function set_messenger(IMessenger $msg) {
self::$msg = $msg;
self::$setup = true;
}
static function get(): IMessenger {
return self::$msg ??= new ProxyMessenger();
}
} }

View File

@ -0,0 +1,297 @@
<?php
namespace nulib\output\std;
use Exception;
use nulib\A;
use nulib\cl;
use nulib\exceptions;
use nulib\ExceptionShadow;
use Throwable;
abstract class AbstractMessenger implements _IMessenger {
const ADD_DATE = false;
const SHOW_IDS = false;
protected static function verifix_level($level, int $max_level=self::MAX_LEVEL): int {
if (!in_array($level, self::VALID_LEVELS, true)) {
$level = cl::get(self::LEVEL_MAP, $level, $level);
}
if (!in_array($level, self::VALID_LEVELS, true)) {
throw new Exception("$level: invalid level");
}
if ($level > $max_level) {
throw new Exception("$level: level not allowed here");
}
return $level;
}
/** @var StdOutput la sortie standard */
protected StdOutput $out;
/** @var int level par défaut dans lequel les messages sont affichés */
protected int $defaultLevel;
/** @var int level minimum que doivent avoir les messages pour être affichés */
protected int $minLevel;
/** @var bool faut-il ajouter la date à chaque ligne? */
protected bool $addDate;
/** @var string format de la date */
protected string $dateFormat;
/** @var bool faut-il afficher les ids (p=id t=id a=id) */
protected bool $showIds;
/** @var ?string identifiant de ce messenger, à ajouter à chaque ligne */
protected ?string $id;
protected int $lastTitleId = 1;
protected abstract function title__getId(): ?int;
protected int $lastActionId = 1;
protected abstract function action__getId(): ?int;
protected function getLinePrefix(): ?string {
$linePrefix = null;
if ($this->addDate) {
$date = date_create()->format($this->dateFormat);
$linePrefix .= "$date ";
}
if ($this->showIds) {
if ($this->id !== null) $linePrefix .= "p=$this->id ";
$titleId = $this->title__getId();
if ($titleId !== null) $linePrefix .= "t=$titleId ";
$actionId = $this->action__getId();
if ($actionId !== null) $linePrefix .= "a=$actionId ";
}
return $linePrefix;
}
protected function decrLevel(int $level, int $amount=-1): int {
$level += $amount;
if ($level < self::MIN_LEVEL) $level = self::MIN_LEVEL;
return $level;
}
protected function checkLevel(?int &$level): bool {
if ($level === null) $level = $this->defaultLevel;
elseif ($level < 0) $level = $this->decrLevel($this->defaultLevel, $level);
return $level >= $this->minLevel;
}
protected function _printTitle(
int $level, string $type, ?string $linePrefix, int $indentLevel,
StdOutput $out, $content
): void {
$prefixes = self::GENERIC_PREFIXES[$level][$type];
if ($prefixes[0]) $out->print();
$content = cl::with($content);
if ($out->isColor()) {
$before = $prefixes[2];
$prefix = $prefixes[3];
$prefix2 = $prefix !== null? "$prefix ": null;
$suffix = $prefixes[4];
$suffix2 = $suffix !== null? " $suffix": null;
$after = $prefixes[5];
$lines = $out->getLines(false, ...$content);
$maxlen = 0;
foreach ($lines as &$content) {
$line = $out->filterColors($content);
$len = mb_strlen($line);
if ($len > $maxlen) $maxlen = $len;
$content = [$content, $len];
}; unset($content);
if ($before !== null) {
if ($linePrefix !== null) $out->write($linePrefix);
$out->iprint($indentLevel, $prefix, substr($before, 1), str_repeat($before[0], $maxlen), $suffix);
}
foreach ($lines as [$content, $len]) {
if ($linePrefix !== null) $out->write($linePrefix);
$padding = $len < $maxlen? str_repeat(" ", $maxlen - $len): null;
$out->iprint($indentLevel, $prefix2, $content, $padding, $suffix2);
}
if ($after !== null) {
if ($linePrefix !== null) $out->write($linePrefix);
$out->iprint($indentLevel, $prefix, substr($after, 1), str_repeat($after[0], $maxlen), $suffix);
}
} else {
$prefix = $prefixes[1];
if ($prefix !== null) $prefix .= " ";
$prefix2 = str_repeat(" ", mb_strlen($prefix));
$lines = $out->getLines(false, ...$content);
foreach ($lines as $content) {
if ($linePrefix !== null) $out->write($linePrefix);
$out->iprint($indentLevel, $prefix, $content);
$prefix = $prefix2;
}
}
}
protected abstract function action__flush(bool $endAction=false, ?int $overrideLevel=null): void;
protected function _printAction(
int $level, ?string $linePrefix, int $indentLevel,
StdOutput $out,
bool $printContent, $content,
bool $printResult, ?bool $rsuccess, $rcontent
): void {
$color = $out->isColor();
if ($rsuccess === true) $type = "success";
elseif ($rsuccess === false) $type = "failure";
else $type = "done";
$rprefixes = self::RESULT_PREFIXES[$type];
if ($color) {
$rprefix = $rprefixes[1];
$rprefix2 = null;
if ($rprefix !== null) {
$rprefix .= " ";
$rprefix2 = $out->filterColors($out->filterContent($rprefix));
$rprefix2 = str_repeat(" ", mb_strlen($rprefix2));
}
} else {
$rprefix = $rprefixes[0];
if ($rprefix !== null) $rprefix .= " ";
$rprefix2 = str_repeat(" ", mb_strlen($rprefix));
}
if ($printContent && $printResult) {
A::ensure_array($content);
if ($rcontent) {
$content[] = ": ";
$content[] = $rcontent;
}
$lines = $out->getLines(false, ...$content);
foreach ($lines as $content) {
if ($linePrefix !== null) $out->write($linePrefix);
$out->iprint($indentLevel, $rprefix, $content);
$rprefix = $rprefix2;
}
} elseif ($printContent) {
$prefixes = self::GENERIC_PREFIXES[$level]["step"];
if ($color) {
$prefix = $prefixes[1];
if ($prefix !== null) $prefix .= " ";
$prefix2 = $out->filterColors($out->filterContent($prefix));
$prefix2 = str_repeat(" ", mb_strlen($prefix2));
$suffix = $prefixes[2];
} else {
$prefix = $prefixes[0];
if ($prefix !== null) $prefix .= " ";
$prefix2 = str_repeat(" ", mb_strlen($prefix));
$suffix = null;
}
A::ensure_array($content);
$content[] = ":";
$lines = $out->getLines(false, ...$content);
foreach ($lines as $content) {
if ($linePrefix !== null) $out->write($linePrefix);
$out->iprint($indentLevel, $prefix, $content, $suffix);
$prefix = $prefix2;
}
} elseif ($printResult) {
if (!$rcontent) {
if ($type === "success") $rcontent = $color? "succès": "";
elseif ($type === "failure") $rcontent = $color? "échec": "";
elseif ($type === "done") $rcontent = "fait";
}
$rprefix = " $rprefix";
$rprefix2 = " $rprefix2";
$lines = $out->getLines(false, $rcontent);
foreach ($lines as $rcontent) {
if ($linePrefix !== null) $out->write($linePrefix);
$out->iprint($indentLevel, $rprefix, $rcontent);
$rprefix = $rprefix2;
}
}
}
protected function _printGeneric(
int $level, string $type, ?string $linePrefix, int $indentLevel,
StdOutput $out, $content
): void {
$prefixes = self::GENERIC_PREFIXES[$level][$type];
$content = cl::with($content);
if ($out->isColor()) {
$prefix = $prefixes[1];
$prefix2 = null;
if ($prefix !== null) {
$prefix .= " ";
$prefix2 = $out->filterColors($out->filterContent($prefix));
$prefix2 = str_repeat(" ", mb_strlen($prefix2));
}
$suffix = $prefixes[2];
$lines = $out->getLines(false, ...$content);
foreach ($lines as $content) {
if ($linePrefix !== null) $out->write($linePrefix);
$out->iprint($indentLevel, $prefix, $content, $suffix);
$prefix = $prefix2;
}
} else {
$prefix = $prefixes[0];
if ($prefix !== null) $prefix .= " ";
$prefix2 = str_repeat(" ", mb_strlen($prefix));
$lines = $out->getLines(false, ...$content);
foreach ($lines as $content) {
if ($linePrefix !== null) $out->write($linePrefix);
$out->iprint($indentLevel, $prefix, $content);
$prefix = $prefix2;
}
}
}
protected function _printGenericOrException(
?int $level, string $type, int $indentLevel,
StdOutput $out, $content
): void {
$linePrefix = $this->getLinePrefix();
# si $content contient des exceptions, les afficher avec un level moindre
$exceptions = null;
if (is_array($content)) {
$valueContent = null;
foreach ($content as $value) {
if ($value instanceof Throwable || $value instanceof ExceptionShadow) {
$exceptions[] = $value;
} else {
$valueContent[] = $value;
}
}
if ($valueContent === null) $content = null;
elseif (count($valueContent) == 1) $content = $valueContent[0];
else $content = $valueContent;
} elseif ($content instanceof Throwable || $content instanceof ExceptionShadow) {
$exceptions[] = $content;
$content = null;
}
$flushActions = true;
$showContent = $this->checkLevel($level);
if ($content !== null && $showContent) {
$this->action__flush(); $flushActions = false;
$this->_printGeneric($level, $type, $linePrefix, $indentLevel, $out, $content);
}
if ($exceptions !== null) {
$level1 = $this->decrLevel($level);
$showTraceback = $this->checkLevel($level1);
foreach ($exceptions as $exception) {
# tout d'abord message
$message = exceptions::get_message($exception);
if ($showContent) {
if ($flushActions) { $this->action__flush(); $flushActions = false; }
$this->_printGeneric($level, $type, $linePrefix, $indentLevel, $out, $message);
}
# puis summary et traceback
if ($showTraceback) {
if ($flushActions) { $this->action__flush(); $flushActions = false; }
$summary = exceptions::get_summary($exception, false);
$this->_printGeneric($level1, $type, $linePrefix, $indentLevel, $out, $summary);
$traceback = exceptions::get_traceback($exception);
$this->_printGeneric($level1, $type, $linePrefix, $indentLevel, $out, $traceback);
}
}
}
}
}

View File

@ -0,0 +1,473 @@
<?php
namespace nulib\output\std;
use Exception;
use nulib\A;
use nulib\cl;
use nulib\output\IMessenger;
class ConsoleMessenger extends AbstractMessenger {
function __construct(?array $params=null) {
$output = $params["output"] ?? null;
$color = $params["color"] ?? null;
$indent = $params["indent"] ?? static::INDENT;
$defaultLevel = $params["default_level"] ?? null;
$defaultLevel = self::verifix_level($defaultLevel ?? self::NORMAL);
$debug = boolval($params["debug"] ?? null);
$minLevel = $params["min_level"] ?? null;
if ($debug) $minLevel ??= self::DEBUG;
$minLevel ??= $params["verbosity"] ?? null; # alias
$minLevel = self::verifix_level($minLevel ?? self::NORMAL, self::NONE);
$addDate = boolval($params["add_date"] ?? static::ADD_DATE);
$dateFormat = cl::get($params, "date_format", static::DATE_FORMAT);
$id = $params["id"] ?? null;
$showIds = $params["show_ids"] ?? static::SHOW_IDS;
$params = [
"color" => $color,
"indent" => $indent,
];
if ($output !== null) {
$this->err = $this->out = new StdOutput($output, $params);
} else {
$this->out = new StdOutput(STDOUT, $params);
$this->err = new StdOutput(STDERR, $params);
}
$this->defaultLevel = $defaultLevel;
$this->minLevel = $minLevel;
$this->addDate = $addDate;
$this->dateFormat = $dateFormat;
$this->id = $id;
$this->showIds = $showIds;
$this->inSection = false;
$this->section = null;
$this->titles = [];
$this->actions = [];
}
function resetParams(?array $params=null): void {
$output = $params["output"] ?? null;
$color = $params["color"] ?? null;
$indent = $params["indent"] ?? null;
$defaultLevel = $params["default_level"] ?? null;
if ($defaultLevel !== null) $defaultLevel = self::verifix_level($defaultLevel);
$debug = $params["debug"] ?? null;
$minLevel = $params["min_level"] ?? null;
if ($debug !== null) $minLevel ??= self::DEBUG;
$minLevel ??= $params["verbosity"] ?? null; # alias
if ($minLevel !== null) $minLevel = self::verifix_level($minLevel, self::NONE);
$addDate = $params["add_date"] ?? null;
$dateFormat = $params["date_format"] ?? null;
$id = $params["id"] ?? null;
$params = [
"output" => $output,
"color" => $color,
"indent" => $indent,
];
if ($this->out === $this->err) {
$this->out->resetParams($params);
} else {
# NB: si initialement [output] était null, et qu'on spécifie une valeur
# [output], alors les deux instances $out et $err sont mis à jour
# séparément avec la même valeur de output
# de plus, on ne peut plus revenir à la situation initiale avec une
# destination différente pour $out et $err
$this->out->resetParams($params);
$this->err->resetParams($params);
}
if ($defaultLevel !== null) $this->defaultLevel = $defaultLevel;
if ($minLevel !== null) $this->minLevel = $minLevel;
if ($addDate !== null) $this->addDate = boolval($addDate);
if ($dateFormat !== null) $this->dateFormat = $dateFormat;
if ($id !== null) $this->id = $id;
}
function clone(?array $params=null): IMessenger {
$clone = clone $this;
if ($params !== null) $clone->resetParams($params);
#XXX faut-il marquer la section et les titres du clone à "print" => false?
# ou en faire des références au parent?
# dans tous les cas, on considère qu'il n'y a pas d'actions en cours, et on
# ne doit pas dépiler avec end() plus que l'état que l'on a eu lors du clone
return $clone;
}
/** @var StdOutput la sortie d'erreur */
protected StdOutput $err;
/** @var bool est-on dans une section? */
protected bool $inSection;
/** @var array section qui est en attente d'affichage */
protected ?array $section;
protected function section__end(): void {
while ($this->actions) $this->adone();
while ($this->titles) $this->title__end();
$this->inSection = false;
$this->section = null;
}
function section__afterFunc(): void {
$this->section__end();
}
function section($content, ?callable $func=null, ?int $level=null): void {
$this->section__end();
$this->inSection = true;
if (!$this->checkLevel($level)) return;
$this->section = [
"msg_level" => $level,
"line_prefix" => $this->getLinePrefix(),
"content" => $content,
"print_content" => true,
];
if ($func !== null) {
try {
$func($this);
} finally {
$this->section__afterFunc();
}
}
}
protected function printSection() {
$section =& $this->section;
if ($section !== null && $section["print_content"]) {
$this->_printTitle(
$section["msg_level"], "section", $section["line_prefix"], 0,
$this->err, $section["content"]);
$section["print_content"] = false;
}
}
protected function getIndentLevel(bool $withActions=true): int {
$indentLevel = count($this->titles) - 1;
if ($indentLevel < 0) $indentLevel = 0;
if ($withActions) {
foreach ($this->actions as $action) {
if ($action["msg_level"] < $this->minLevel) continue;
$indentLevel++;
}
}
return $indentLevel;
}
protected array $titles;
protected function title__last(): ?array {
$last = end($this->titles);
return $last !== false? $last: null;
}
function title__getMarks(): array {
return [count($this->titles)];
}
protected function title__getId(): ?int {
return $this->title__last()["id"] ?? null;
}
protected function title__end(?int $until=null): void {
$title = $this->title__last();
if ($title !== null) {
$until ??= $title["max_title_level"];
$until ??= $this->title__getMarks()[0] - 1;
while (count($this->titles) > $until) {
array_pop($this->titles);
}
}
}
protected function title__flush(): void {
$this->printSection();
$err = $this->err;
$indentLevel = 0;
foreach ($this->titles as &$title) {
if ($title["print_content"]) {
$this->_printTitle(
$title["msg_level"], "title", $title["line_prefix"], $indentLevel,
$err, $title["content"]);
$title["print_content"] = false;
}
if ($title["print_descs"]) {
foreach ($title["descs"] as $desc) {
$this->_printGeneric(
$desc["msg_level"], "desc", $desc["line_prefix"], $indentLevel,
$err, $desc["content"]);
}
$title["descs"] = [];
$title["print_descs"] = false;
}
$indentLevel++;
}; unset($title);
}
protected function &title__ref(): ?array {
return $this->titles[array_key_last($this->titles)];
}
function title__beforeFunc(array $marks): void {
$title =& $this->title__ref();
$title["max_title_level"] = $marks[0] + 1;
}
function title__afterFunc(array $marks): void {
$title =& $this->title__ref();
$title["max_title_level"] = null;
$this->title__end($marks[0]);
}
function title($content, ?callable $func=null, ?int $level=null): void {
if (!$this->checkLevel($level)) return;
$marks = $this->title__getMarks();
// faire en deux temps pour linePrefix soit à jour
$this->titles[] = ["id" => $this->lastTitleId++];
A::merge($this->title__ref(), [
"title_level" => $marks[0],
"max_title_level" => null,
"msg_level" => $level,
"line_prefix" => $this->getLinePrefix(),
"content" => $content,
"print_content" => true,
"descs" => [],
"print_descs" => false,
]);
if ($func !== null) {
try {
$this->title__beforeFunc($marks);
$func($this);
} finally {
$this->title__afterFunc($marks);
}
}
}
function desc($content, ?int $level=null): void {
if (!$this->checkLevel($level)) return;
$desc = [
"msg_level" => $level,
"line_prefix" => $this->getLinePrefix(),
"content" => $content,
];
$title = $this->title__last();
if ($title !== null) {
$title =& $this->title__ref();
$title["descs"][] = $desc;
$title["print_descs"] = true;
} else {
# pas de titre en cours
$this->_printGeneric(
$desc["msg_level"], "desc", $desc["line_prefix"], 0,
$this->err, $desc["content"]);
}
}
protected array $actions;
protected function action__last(): ?array {
$last = end($this->actions);
return $last !== false? $last: null;
}
function action__getMarks(): array {
return [count($this->actions)];
}
protected function action__getId(): ?int {
return $this->action__last()["id"] ?? null;
}
protected function action__end(?int $until=null): void {
$action = $this->action__last();
if ($action !== null) {
$until ??= $action["max_action_level"];
$until ??= $this->action__getMarks()[0] - 1;
while (count($this->actions) > $until) {
array_pop($this->actions);
}
}
}
protected function action__flush(bool $endAction=false, ?int $overrideLevel=null): void {
$this->title__flush();
$err = $this->err;
$indentLevel = $this->getIndentLevel(false);
$lastIndex = array_key_last($this->actions);
$index = 0;
foreach ($this->actions as &$action) {
$mergeResult = $index++ == $lastIndex && $endAction;
$level = $overrideLevel?? $action["msg_level"];
$linePrefix = $action["line_prefix"];
$content = $action["content"];
$printContent = $action["print_content"];
$rsuccess = $action["result_success"];
$rcontent = $action["result_content"];
if ($level < $this->minLevel) continue;
if ($mergeResult) {
if (time() - $action["timestamp"] <= 2) {
$this->_printAction(
$level, $linePrefix, $indentLevel,
$err,
$printContent, $content,
true, $rsuccess, $rcontent);
} else {
# si l'action a pris plus de 2 secondes, ne pas fusionner pour que
# l'on voit le temps que ça a pris
$this->_printAction(
$level, $linePrefix, $indentLevel,
$err,
$printContent, $content,
false, null, null);
# recalculer une nouvelle ligne de préfixe pour le résultat
$linePrefix = $this->getLinePrefix();
$this->_printAction(
$level, $linePrefix, $indentLevel,
$err,
false, null,
true, $rsuccess, $rcontent);
}
$action["action_aresult"] = true;
} elseif ($printContent) {
$this->_printAction(
$level, $linePrefix, $indentLevel,
$err,
$printContent, $content,
false, $rsuccess, $rcontent);
$action["print_content"] = false;
}
$indentLevel++;
}; unset($action);
if ($endAction) $this->action__end();
}
protected function &action__ref(): ?array {
return $this->actions[array_key_last($this->actions)];
}
function action__beforeFunc(array $marks): void {
$action =& $this->action__ref();
$action["max_action_level"] = $marks[0] + 1;
}
function action__afterFunc(array $marks, $result): void {
$action =& $this->action__ref();
$aresult = $action["action_aresult"] ?? false;
if (!$aresult) $this->aresult($result);
$action["max_action_level"] = null;
$this->action__end($marks[0]);
}
function action($content, ?callable $func=null, ?int $level=null): void {
$this->checkLevel($level);
$marks = $this->action__getMarks();
// faire en deux temps pour linePrefix soit à jour
$this->actions[] = ["id" => $this->lastActionId++];
A::merge($this->action__ref(), [
"action_level" => $marks[0],
"max_action_level" => null,
"action_aresult" => false,
"timestamp" => time(),
"msg_level" => $level,
"line_prefix" => $this->getLinePrefix(),
"content" => $content,
"print_content" => true,
"result_success" => null,
"result_content" => null,
]);
if ($func !== null) {
try {
$result = null;
$this->action__beforeFunc($marks);
$result = $func($this);
} catch (Exception $e) {
$this->afailure($e);
throw $e;
} finally {
$this->action__afterFunc($marks, $result);
}
}
}
function step($content, ?int $level=null): void {
$this->_printGenericOrException(
$level, "step", $this->getIndentLevel(),
$this->err, $content);
}
function asuccess($content=null, ?int $overrideLevel=null): void {
if (!$this->actions) $this->action(null);
$action =& $this->action__ref();
$action["result_success"] = true;
$action["result_content"] = $content;
$this->action__flush(true, $overrideLevel);
}
function afailure($content=null, ?int $overrideLevel=null): void {
if (!$this->actions) $this->action(null);
$action =& $this->action__ref();
$action["result_success"] = false;
$action["result_content"] = $content;
$this->action__flush(true, $overrideLevel);
}
function adone($content=null, ?int $overrideLevel=null): void {
if (!$this->actions) $this->action(null);
$action =& $this->action__ref();
$action["result_success"] = null;
$action["result_content"] = $content;
$this->action__flush(true, $overrideLevel);
}
function aresult($result=null, ?int $overrideLevel=null): void {
if (!$this->actions) $this->action(null);
if ($result === true) $this->asuccess(null, $overrideLevel);
elseif ($result === false) $this->afailure(null, $overrideLevel);
elseif ($result instanceof Exception) $this->afailure($result, $overrideLevel);
else $this->adone($result, $overrideLevel);
}
function print($content, ?int $level=null): void {
$this->_printGenericOrException(
$level, "print", $this->getIndentLevel(),
$this->out, $content);
}
function info($content, ?int $level=null): void {
$this->_printGenericOrException(
$level, "info", $this->getIndentLevel(),
$this->err, $content);
}
function note($content, ?int $level=null): void {
$this->_printGenericOrException(
$level, "note", $this->getIndentLevel(),
$this->err, $content);
}
function warning($content, ?int $level=null): void {
$this->_printGenericOrException(
$level, "warning", $this->getIndentLevel(),
$this->err, $content);
}
function error($content, ?int $level=null): void {
$this->_printGenericOrException(
$level, "error", $this->getIndentLevel(),
$this->err, $content);
}
function end(bool $all=false): void {
if ($all) $this->section__afterFunc();
elseif ($this->actions) $this->action__end();
elseif ($this->titles) $this->title__end();
else $this->section__afterFunc();
}
}

View File

@ -0,0 +1,350 @@
<?php
namespace nulib\output\std;
use Exception;
use nulib\cl;
use nulib\output\IMessenger;
class LogMessenger extends AbstractMessenger {
const ADD_DATE = true;
const SHOW_IDS = true;
function __construct(?array $params=null) {
$output = $params["output"] ?? null;
$color = $params["color"] ?? false;
$indent = $params["indent"] ?? static::INDENT;
$defaultLevel = $params["default_level"] ?? null;
$defaultLevel = self::verifix_level($defaultLevel ?? self::NORMAL);
$debug = boolval($params["debug"] ?? null);
$minLevel = $params["min_level"] ?? null;
if ($debug) $minLevel ??= self::DEBUG;
$minLevel ??= $params["verbosity"] ?? null; # alias
$minLevel = self::verifix_level($minLevel ?? self::NORMAL, self::NONE);
$addDate = boolval($params["add_date"] ?? static::ADD_DATE);
$dateFormat = cl::get($params, "date_format", static::DATE_FORMAT);
$id = $params["id"] ?? null;
$showIds = $params["show_ids"] ?? static::SHOW_IDS;
$this->out = new StdOutput($output ?? STDERR, [
"color" => $color,
"indent" => $indent,
]);
$this->defaultLevel = $defaultLevel;
$this->minLevel = $minLevel;
$this->addDate = $addDate;
$this->dateFormat = $dateFormat;
$this->id = $id;
$this->showIds = $showIds;
$this->titles = [];
$this->actions = [];
}
function resetParams(?array $params=null): void {
$output = $params["output"] ?? null;
$color = $params["color"] ?? null;
$indent = $params["indent"] ?? null;
$defaultLevel = $params["default_level"] ?? null;
if ($defaultLevel !== null) $defaultLevel = self::verifix_level($defaultLevel);
$debug = $params["debug"] ?? null;
$minLevel = $params["min_level"] ?? null;
if ($debug !== null) $minLevel ??= self::DEBUG;
$minLevel ??= $params["verbosity"] ?? null; # alias
if ($minLevel !== null) $minLevel = self::verifix_level($minLevel, self::NONE);
$addDate = $params["add_date"] ?? null;
$dateFormat = $params["date_format"] ?? null;
$id = $params["id"] ?? null;
$this->out->resetParams([
"output" => $output,
"color" => $color,
"indent" => $indent,
]);
if ($defaultLevel !== null) $this->defaultLevel = $defaultLevel;
if ($minLevel !== null) $this->minLevel = $minLevel;
if ($addDate !== null) $this->addDate = boolval($addDate);
if ($dateFormat !== null) $this->dateFormat = $dateFormat;
if ($id !== null) $this->id = $id;
}
function clone(?array $params=null): IMessenger {
$clone = clone $this;
if ($params !== null) $clone->resetParams($params);
return $clone;
}
protected function section__end(): void {
$this->end(true);
}
function section__afterFunc(): void {
$this->section__end();
}
function section($content, ?callable $func=null, ?int $level=null): void {
$this->section__end();
if (!$this->checkLevel($level)) return;
$this->_printTitle(
$level, "section", $this->getLinePrefix(), 0,
$this->out, $content);
if ($func !== null) {
try {
$func($this);
} finally {
$this->section__afterFunc();
}
}
}
protected array $titles;
protected function title__last(): ?array {
$last = end($this->titles);
return $last !== false? $last: null;
}
function title__getMarks(): array {
return [count($this->titles)];
}
protected function title__getId(): ?int {
return $this->title__last()["id"] ?? null;
}
protected function title__end(?int $until=null): void {
$title = $this->title__last();
if ($title !== null) {
$until ??= $title["max_title_level"];
$until ??= $this->title__getMarks()[0] - 1;
while (count($this->titles) > $until) {
array_pop($this->titles);
}
}
}
protected function &title__ref(): ?array {
return $this->titles[array_key_last($this->titles)];
}
function title__beforeFunc(array $marks): void {
$title =& $this->title__ref();
$title["max_title_level"] = $marks[0] + 1;
}
function title__afterFunc(array $marks): void {
$title =& $this->title__ref();
$title["max_title_level"] = null;
$this->title__end($marks[0]);
}
function title($content, ?callable $func=null, ?int $level=null): void {
if (!$this->checkLevel($level)) return;
$marks = $this->title__getMarks();
$this->titles[] = [
"id" => $this->lastTitleId++,
"title_level" => $marks[0],
"max_title_level" => null,
];
$this->_printTitle(
$level, "title", $this->getLinePrefix(), $marks[0],
$this->out, $content);
if ($func !== null) {
try {
$this->title__beforeFunc($marks);
$func($this);
} finally {
$this->title__afterFunc($marks);
}
}
}
function desc($content, ?int $level=null): void {
if (!$this->checkLevel($level)) return;
$titleLevel = $this->title__last()["title_level"] ?? 0;
$this->_printGeneric(
$level, "desc", $this->getLinePrefix(), $titleLevel,
$this->out, $content);
}
protected array $actions;
protected function action__last(): ?array {
$last = end($this->actions);
return $last !== false? $last: null;
}
function action__getMarks(): array {
return [count($this->actions)];
}
protected function action__getId(): ?int {
return $this->action__last()["id"] ?? null;
}
protected function action__end(?int $until=null): void {
$action = $this->action__last();
if ($action !== null) {
$until ??= $action["max_action_level"];
$until ??= $this->action__getMarks()[0] - 1;
while (count($this->actions) > $until) {
array_pop($this->actions);
}
}
}
protected function action__flush(bool $endAction=false, ?int $overrideLevel=null): void {
}
protected function &action__ref(): ?array {
return $this->actions[array_key_last($this->actions)];
}
function action__beforeFunc(array $marks): void {
$action =& $this->action__ref();
$action["max_action_level"] = $marks[0] + 1;
}
function action__afterFunc(array $marks, $result): void {
$action =& $this->action__ref();
$aresult = $action["action_aresult"] ?? false;
if (!$aresult) $this->aresult($result);
$action["max_action_level"] = null;
$this->action__end($marks[0]);
}
function action($content, ?callable $func=null, ?int $level=null): void {
$this->checkLevel($level);
$marks = $this->action__getMarks();
$this->actions[] = [
"id" => $this->lastActionId++,
"action_level" => $marks[0],
"max_action_level" => null,
"action_aresult" => false,
"msg_level" => $level
];
$this->_printAction(
$level, $this->getLinePrefix(), $marks[0],
$this->out,
true, $content,
false, null, null);
if ($func !== null) {
try {
$result = null;
$this->action__beforeFunc($marks);
$result = $func($this);
} catch (Exception $e) {
$this->afailure($e);
throw $e;
} finally {
$this->action__afterFunc($marks, $result);
}
}
}
function step($content, ?int $level=null): void {
$this->_printGenericOrException(
$level, "step", $this->getIndentLevel(),
$this->out, $content);
}
function asuccess($content=null, ?int $overrideLevel=null): void {
if ($this->action__getMarks()[0] == 0) $this->action(null);
$action =& $this->action__ref();
$level = $overrideLevel ?? $action["msg_level"];
$this->_printAction(
$level, $this->getLinePrefix(), $action["action_level"],
$this->out,
false, null,
true, true, $content);
$action["action_aresult"] = true;
$this->action__end();
}
function afailure($content=null, ?int $overrideLevel=null): void {
if ($this->action__getMarks()[0] == 0) $this->action(null);
$action =& $this->action__ref();
$level = $overrideLevel ?? $action["msg_level"];
$this->_printAction(
$level, $this->getLinePrefix(), $action["action_level"],
$this->out,
false, null,
true, false, $content);
$action["action_aresult"] = true;
$this->action__end();
}
function adone($content=null, ?int $overrideLevel=null): void {
if ($this->action__getMarks()[0] == 0) $this->action(null);
$action =& $this->action__ref();
$level = $overrideLevel ?? $action["msg_level"];
$this->_printAction(
$level, $this->getLinePrefix(), $action["action_level"],
$this->out,
false, null,
true, null, $content);
$action["action_aresult"] = true;
$this->action__end();
}
function aresult($result=null, ?int $overrideLevel=null): void {
if ($this->action__getMarks()[0] == 0) $this->action(null);
if ($result === true) $this->asuccess(null, $overrideLevel);
elseif ($result === false) $this->afailure(null, $overrideLevel);
elseif ($result instanceof Exception) $this->afailure($result, $overrideLevel);
else $this->adone($result, $overrideLevel);
}
protected function getIndentLevel(bool $withActions=true): int {
$indentLevel = count($this->titles) - 1;
if ($indentLevel < 0) $indentLevel = 0;
if ($withActions) $indentLevel += count($this->actions);
return $indentLevel;
}
function print($content, ?int $level=null): void {
$this->_printGenericOrException(
$level, "print", $this->getIndentLevel(),
$this->out, $content);
}
function info($content, ?int $level=null): void {
$this->_printGenericOrException(
$level, "info", $this->getIndentLevel(),
$this->out, $content);
}
function note($content, ?int $level=null): void {
$this->_printGenericOrException(
$level, "note", $this->getIndentLevel(),
$this->out, $content);
}
function warning($content, ?int $level=null): void {
$this->_printGenericOrException(
$level, "warning", $this->getIndentLevel(),
$this->out, $content);
}
function error($content, ?int $level=null): void {
$this->_printGenericOrException(
$level, "error", $this->getIndentLevel(),
$this->out, $content);
}
function end(bool $all=false): void {
if ($all) {
while ($this->actions) $this->adone();
while ($this->titles) $this->title__end();
} elseif ($this->actions) {
$this->action__end();
} elseif ($this->titles) {
$this->title__end();
}
}
}

View File

@ -0,0 +1,61 @@
<?php
namespace nulib\output\std;
use nulib\output\IMessenger;
class NullMessenger implements IMessenger {
function resetParams(?array $params=null): void {
}
function clone(?array $params=null): self {
return clone $this;
}
function section($content, ?callable $func=null, ?int $level=null): void {
if ($func !== null) $func($this);
}
function title($content, ?callable $func=null, ?int $level=null): void {
if ($func !== null) $func($this);
}
function desc($content, ?int $level=null): void {
}
function action($content, ?callable $func=null, ?int $level=null): void {
if ($func !== null) $func($this);
}
function step($content, ?int $level=null): void {
}
function asuccess($content=null, ?int $overrideLevel=null): void {
}
function afailure($content=null, ?int $overrideLevel=null): void {
}
function adone($content=null, ?int $overrideLevel=null): void {
}
function aresult($result=null, ?int $overrideLevel=null): void {
}
function print($content, ?int $level=null): void {
}
function info($content, ?int $level=null): void {
}
function note($content, ?int $level=null): void {
}
function warning($content, ?int $level=null): void {
}
function error($content, ?int $level=null): void {
}
function end(bool $all=false): void {
}
}

View File

@ -1,28 +1,39 @@
<?php <?php
namespace nulib\output\std; namespace nulib\output\std;
use Exception;
use nulib\output\IMessenger; use nulib\output\IMessenger;
/** /**
* Class ProxyMessenger: un proxy vers ou un plusieurs instances de IMessenger * Class ProxyMessenger: un proxy vers ou un plusieurs instances de IMessenger
* *
* NB: si cette classe est instanciée sans argument, elle agit comme un * NB: si cette classe est instanciée sans argument, elle agit comme
* "NullMessenger", c'est à dire une instance qui envoie tous les messages vers * {@link NullMessenger}: elle envoie tous les messages vers /dev/null
* /dev/null
*/ */
class ProxyMessenger implements IMessenger { class ProxyMessenger implements _IMessenger {
function __construct(?IMessenger ...$msgs) { function __construct(?IMessenger ...$msgs) {
$this->msgs = [];
foreach ($msgs as $msg) { foreach ($msgs as $msg) {
if ($msg !== null) $this->msgs[] = $msg; if ($msg !== null) $this->msgs[] = $msg;
} }
} }
/** @var IMessenger[] */ /** @var _IMessenger[] */
protected $msgs; protected ?array $msgs = [];
function isEmpty(): bool {
return !$this->msgs;
}
function addMessenger(IMessenger $msg): self {
$this->msgs[] = $msg;
return $this;
}
function resetParams(?array $params=null): void {
foreach ($this->msgs as $msg) {
$msg->resetParams($params);
}
}
function resetParams(?array $params=null): void { foreach ($this->msgs as $msg) { $msg->resetParams($params); } }
function clone(?array $params=null): self { function clone(?array $params=null): self {
$clone = clone $this; $clone = clone $this;
foreach ($clone->msgs as &$msg) { foreach ($clone->msgs as &$msg) {
@ -30,92 +41,180 @@ class ProxyMessenger implements IMessenger {
}; unset($msg); }; unset($msg);
return $clone; return $clone;
} }
function section__afterFunc(): void {
foreach ($this->msgs as $msg) {
if ($msg instanceof _IMessenger) {
$msg->section__afterFunc();
}
}
}
function section($content, ?callable $func=null, ?int $level=null): void { function section($content, ?callable $func=null, ?int $level=null): void {
$useFunc = false;
foreach ($this->msgs as $msg) { foreach ($this->msgs as $msg) {
$msg->section($content, null, $level); $msg->section($content, null, $level);
if ($msg instanceof _IMessenger) $useFunc = true;
} }
if ($useFunc && $func !== null) { if ($func !== null) {
try { try {
$func($this); $func($this);
} finally { } finally {
/** @var _IMessenger $msg */ $this->section__afterFunc();
foreach ($this->msgs as $msg) {
$msg->_endSection();
}
} }
} }
} }
function title($content, ?callable $func=null, ?int $level=null): void {
$useFunc = false; function title__getMarks(): array {
$untils = []; $marks = [];
foreach ($this->msgs as $msg) { foreach ($this->msgs as $key => $msg) {
if ($msg instanceof _IMessenger) { if ($msg instanceof _IMessenger) {
$useFunc = true; $marks[$key] = $msg->title__getMarks();
$untils[] = $msg->_getTitleMark();
} }
}
return $marks;
}
function title__beforeFunc(array $marks): void {
foreach ($this->msgs as $key => $msg) {
if ($msg instanceof _IMessenger) {
$msg->title__beforeFunc($marks[$key]);
}
}
}
function title__afterFunc(array $marks): void {
foreach ($this->msgs as $key => $msg) {
if ($msg instanceof _IMessenger) {
$msg->title__afterFunc($marks[$key]);
}
}
}
function title($content, ?callable $func=null, ?int $level=null): void {
$marks = $this->title__getMarks();
foreach ($this->msgs as $msg) {
$msg->title($content, null, $level); $msg->title($content, null, $level);
} }
if ($useFunc && $func !== null) { if ($func !== null) {
try { try {
$this->title__beforeFunc($marks);
$func($this); $func($this);
} finally { } finally {
/** @var _IMessenger $msg */ $this->title__afterFunc($marks);
$index = 0;
foreach ($this->msgs as $msg) {
if ($msg instanceof _IMessenger) {
$msg->_endTitle($untils[$index++]);
}
}
} }
} }
} }
function desc($content, ?int $level=null): void { foreach ($this->msgs as $msg) { $msg->desc($content, $level); } }
function action($content, ?callable $func=null, ?int $level=null): void { function desc($content, ?int $level=null): void {
$useFunc = false;
$untils = [];
foreach ($this->msgs as $msg) { foreach ($this->msgs as $msg) {
$msg->desc($content, $level);
}
}
function action__getMarks(): array {
$marks = [];
foreach ($this->msgs as $key => $msg) {
if ($msg instanceof _IMessenger) { if ($msg instanceof _IMessenger) {
$useFunc = true; $marks[$key] = $msg->action__getMarks();
$untils[] = $msg->_getActionMark();
} }
}
return $marks;
}
function action__beforeFunc(array $marks): void {
foreach ($this->msgs as $key => $msg) {
if ($msg instanceof _IMessenger) {
$msg->action__beforeFunc($marks[$key]);
}
}
}
function action__afterFunc(array $marks, $result): void {
foreach ($this->msgs as $key => $msg) {
if ($msg instanceof _IMessenger) {
$msg->action__afterFunc($marks[$key], $result);
}
}
}
function action($content, ?callable $func=null, ?int $level=null): void {
$marks = $this->action__getMarks();
foreach ($this->msgs as $msg) {
$msg->action($content, null, $level); $msg->action($content, null, $level);
} }
if ($useFunc && $func !== null) { if ($func !== null) {
try { try {
$result = null;
$this->action__beforeFunc($marks);
$result = $func($this); $result = $func($this);
/** @var _IMessenger $msg */
$index = 0;
foreach ($this->msgs as $msg) {
if ($msg->_getActionMark() > $untils[$index++]) {
$msg->aresult($result);
}
}
} catch (Exception $e) {
/** @var _IMessenger $msg */
foreach ($this->msgs as $msg) {
$msg->afailure($e);
}
throw $e;
} finally { } finally {
/** @var _IMessenger $msg */ $this->action__afterFunc($marks, $result);
$index = 0;
foreach ($this->msgs as $msg) {
$msg->_endAction($untils[$index++]);
}
} }
} }
} }
function step($content, ?int $level=null): void { foreach ($this->msgs as $msg) { $msg->step($content, $level); } }
function asuccess($content=null, ?int $overrideLevel=null): void { foreach ($this->msgs as $msg) { $msg->asuccess($content, $overrideLevel); } } function step($content, ?int $level=null): void {
function afailure($content=null, ?int $overrideLevel=null): void { foreach ($this->msgs as $msg) { $msg->afailure($content, $overrideLevel); } } foreach ($this->msgs as $msg) {
function adone($content=null, ?int $overrideLevel=null): void { foreach ($this->msgs as $msg) { $msg->adone($content, $overrideLevel); } } $msg->step($content, $level);
function aresult($result=null, ?int $overrideLevel=null): void { foreach ($this->msgs as $msg) { $msg->aresult($result, $overrideLevel); } } }
function print($content, ?int $level=null): void { foreach ($this->msgs as $msg) { $msg->print($content, $level); } } }
function info($content, ?int $level=null): void { foreach ($this->msgs as $msg) { $msg->info($content, $level); } }
function note($content, ?int $level=null): void { foreach ($this->msgs as $msg) { $msg->note($content, $level); } } function asuccess($content=null, ?int $overrideLevel=null): void {
function warning($content, ?int $level=null): void { foreach ($this->msgs as $msg) { $msg->warning($content, $level); } } foreach ($this->msgs as $msg) {
function error($content, ?int $level=null): void { foreach ($this->msgs as $msg) { $msg->error($content, $level); } } $msg->asuccess($content, $overrideLevel);
function end(bool $all=false): void { foreach ($this->msgs as $msg) { $msg->end($all); } } }
}
function afailure($content=null, ?int $overrideLevel=null): void {
foreach ($this->msgs as $msg) {
$msg->afailure($content, $overrideLevel);
}
}
function adone($content=null, ?int $overrideLevel=null): void {
foreach ($this->msgs as $msg) {
$msg->adone($content, $overrideLevel);
}
}
function aresult($result=null, ?int $overrideLevel=null): void {
foreach ($this->msgs as $msg) {
$msg->aresult($result, $overrideLevel);
}
}
function print($content, ?int $level=null): void {
foreach ($this->msgs as $msg) {
$msg->print($content, $level);
}
}
function info($content, ?int $level=null): void {
foreach ($this->msgs as $msg) {
$msg->info($content, $level);
}
}
function note($content, ?int $level=null): void {
foreach ($this->msgs as $msg) {
$msg->note($content, $level);
}
}
function warning($content, ?int $level=null): void {
foreach ($this->msgs as $msg) {
$msg->warning($content, $level);
}
}
function error($content, ?int $level=null): void {
foreach ($this->msgs as $msg) {
$msg->error($content, $level);
}
}
function end(bool $all=false): void {
foreach ($this->msgs as $msg) {
$msg->end($all);
}
}
} }

View File

@ -1,723 +0,0 @@
<?php
namespace nulib\output\std;
use Exception;
use nulib\A;
use nulib\cl;
use nulib\ExceptionShadow;
use nulib\output\IMessenger;
use nulib\UserException;
use Throwable;
class StdMessenger implements _IMessenger {
const INDENT = " ";
const DATE_FORMAT = 'Y-m-d\TH:i:s.u';
const VALID_LEVELS = [self::DEBUG, self::MINOR, self::NORMAL, self::MAJOR, self::NONE];
const LEVEL_MAP = [
"debug" => self::DEBUG,
"minor" => self::MINOR, "verbose" => self::MINOR,
"normal" => self::NORMAL,
"major" => self::MAJOR, "quiet" => self::MAJOR,
"none" => self::NONE, "silent" => self::NONE,
];
protected static function verifix_level($level, int $max_level=self::MAX_LEVEL): int {
if (!in_array($level, self::VALID_LEVELS, true)) {
$level = cl::get(self::LEVEL_MAP, $level, $level);
}
if (!in_array($level, self::VALID_LEVELS, true)) {
throw new Exception("$level: invalid level");
}
if ($level > $max_level) {
throw new Exception("$level: level not allowed here");
}
return $level;
}
const GENERIC_PREFIXES = [
self::MAJOR => [
"section" => [true, "SECTION!", "===", "<color @b>=", "=</color>", "==="],
"title" => [false, "TITLE!", null, "<color @b>T", "</color>", "==="],
"desc" => ["DESC!", "<color @b>></color>", ""],
"error" => ["CRIT.ERROR!", "<color @r>E!", "</color>"],
"warning" => ["CRIT.WARNING!", "<color @y>W!", "</color>"],
"note" => ["ATTENTION!", "<color @g>N!", "</color>"],
"info" => ["IMPORTANT!", "<color @b>N!", "</color>"],
"step" => ["*", "<color @w>.</color>", ""],
"print" => [null, null, null],
],
self::NORMAL => [
"section" => [true, "SECTION:", "---", "<color @b>-", "-</color>", "---"],
"title" => [false, "TITLE:", null, "<color @b>T</color><color b>", "</color>", "---"],
"desc" => ["DESC:", "<color @b>></color>", ""],
"error" => ["ERROR:", "<color @r>E</color><color r>", "</color>"],
"warning" => ["WARNING:", "<color @y>W</color><color y>", "</color>"],
"note" => ["NOTE:", "<color @g>N</color>", ""],
"info" => ["INFO:", "<color @b>I</color>", ""],
"step" => ["*", "<color @w>.</color>", ""],
"print" => [null, null, null],
],
self::MINOR => [
"section" => [true, "section", null, "<color @w>>>", "<<</color>", null],
"title" => [false, "title", null, "<color b>t", "</color>", null],
"desc" => ["desc", "<color b>></color>", ""],
"error" => ["error", "<color r>E</color><color -r>", "</color>"],
"warning" => ["warning", "<color y>W</color><color -y>", "</color>"],
"note" => ["note", "<color g>N</color>", ""],
"info" => ["info", "<color b>I</color><color w>", "</color>"],
"step" => ["*", "<color w>.</color>", ""],
"print" => [null, null, null],
],
self::DEBUG => [
"section" => [true, "section", null, "<color @w>>>", "<<</color>", null],
"title" => [false, "title", null, "<color b>t", "</color>", null],
"desc" => ["desc", "<color b>></color>", ""],
"error" => ["debugE", "<color r>e</color><color -r>", "</color>"],
"warning" => ["debugW", "<color y>w</color><color -y>", "</color>"],
"note" => ["debugN", "<color b>i</color>", ""],
"info" => ["debug", "<color @w>D</color><color w>", "</color>"],
"step" => ["*", "<color w>.</color>", ""],
"print" => [null, null, null],
],
];
const RESULT_PREFIXES = [
"failure" => ["(FAILURE)", "<color r>✘</color>"],
"success" => ["(SUCCESS)", "<color @g>✔</color>"],
"done" => [null, null],
];
function __construct(?array $params=null) {
$output = cl::get($params, "output");
$color = cl::get($params, "color");
$indent = cl::get($params, "indent", static::INDENT);
$defaultLevel = cl::get($params, "default_level");
if ($defaultLevel === null) $defaultLevel = self::NORMAL;
$defaultLevel = self::verifix_level($defaultLevel);
$debug = boolval(cl::get($params, "debug"));
$minLevel = cl::get($params, "min_level");
if ($minLevel === null && $debug) $minLevel = self::DEBUG;
if ($minLevel === null) $minLevel = cl::get($params, "verbosity"); # alias
if ($minLevel === null) $minLevel = self::NORMAL;
$minLevel = self::verifix_level($minLevel, self::NONE);
$addDate = boolval(cl::get($params, "add_date"));
$dateFormat = cl::get($params, "date_format", static::DATE_FORMAT);
$id = cl::get($params, "id");
$params = [
"color" => $color,
"indent" => $indent,
];
if ($output !== null) {
$this->err = $this->out = new StdOutput($output, $params);
} else {
$this->out = new StdOutput(STDOUT, $params);
$this->err = new StdOutput(STDERR, $params);
}
$this->defaultLevel = $defaultLevel;
$this->minLevel = $minLevel;
$this->addDate = $addDate;
$this->dateFormat = $dateFormat;
$this->id = $id;
$this->inSection = false;
$this->titles = [];
$this->actions = [];
}
function resetParams(?array $params=null): void {
$output = cl::get($params, "output");
$color = cl::get($params, "color");
$indent = cl::get($params, "indent");
$defaultLevel = cl::get($params, "default_level");
if ($defaultLevel !== null) $defaultLevel = self::verifix_level($defaultLevel);
$debug = cl::get($params, "debug");
$minLevel = cl::get($params, "min_level");
if ($minLevel === null && $debug !== null) $minLevel = $debug? self::DEBUG: self::NORMAL;
if ($minLevel === null) $minLevel = cl::get($params, "verbosity"); # alias
if ($minLevel !== null) $minLevel = self::verifix_level($minLevel, self::NONE);
$addDate = cl::get($params, "add_date");
$dateFormat = cl::get($params, "date_format");
$id = cl::get($params, "id");
$params = [
"output" => $output,
"color" => $color,
"indent" => $indent,
];
if ($this->out === $this->err) {
$this->out->resetParams($params);
} else {
# NB: si initialement [output] était null, et qu'on spécifie une valeur
# [output], alors les deux instances $out et $err sont mis à jour
# séparément avec la même valeur de output
# de plus, on ne peut plus revenir à la situation initiale avec une
# destination différente pour $out et $err
$this->out->resetParams($params);
$this->err->resetParams($params);
}
if ($defaultLevel !== null) $this->defaultLevel = $defaultLevel;
if ($minLevel !== null) $this->minLevel = $minLevel;
if ($addDate !== null) $this->addDate = boolval($addDate);
if ($dateFormat !== null) $this->dateFormat = $dateFormat;
if ($id !== null) $this->id = $id;
}
function clone(?array $params=null): IMessenger {
$clone = clone $this;
if ($params !== null) $clone->resetParams($params);
#XXX faut-il marquer la section et les titres du clone à "print" => false?
# ou en faire des références au parent?
# dans tous les cas, on considère qu'il n'y a pas d'actions en cours, et on
# ne doit pas dépiler avec end() plus que l'état que l'on a eu lors du clone
return $clone;
}
/** @var StdOutput la sortie standard */
protected $out;
/** @var StdOutput la sortie d'erreur */
protected $err;
/** @var int level par défaut dans lequel les messages sont affichés */
protected $defaultLevel;
/** @var int level minimum que doivent avoir les messages pour être affichés */
protected $minLevel;
/** @var bool faut-il ajouter la date à chaque ligne? */
protected $addDate;
/** @var string format de la date */
protected $dateFormat;
/** @var ?string identifiant de ce messenger, à ajouter à chaque ligne */
protected $id;
protected function getLinePrefix(): ?string {
$linePrefix = null;
if ($this->addDate) {
$date = date_create()->format($this->dateFormat);
$linePrefix .= "$date ";
}
if ($this->id !== null) {
$linePrefix .= "$this->id ";
}
return $linePrefix;
}
protected function decrLevel(int $level, int $amount=-1): int {
$level += $amount;
if ($level < self::MIN_LEVEL) $level = self::MIN_LEVEL;
return $level;
}
protected function checkLevel(?int &$level): bool {
if ($level === null) $level = $this->defaultLevel;
elseif ($level < 0) $level = $this->decrLevel($this->defaultLevel, $level);
return $level >= $this->minLevel;
}
protected function getIndentLevel(bool $withActions=true): int {
$indentLevel = count($this->titles) - 1;
if ($indentLevel < 0) $indentLevel = 0;
if ($withActions) {
foreach ($this->actions as $action) {
if ($action["level"] < $this->minLevel) continue;
$indentLevel++;
}
}
return $indentLevel;
}
protected function _printTitle(?string $linePrefix, int $level,
string $type, $content,
int $indentLevel, StdOutput $out): void {
$prefixes = self::GENERIC_PREFIXES[$level][$type];
if ($prefixes[0]) $out->print();
$content = cl::with($content);
if ($out->isColor()) {
$before = $prefixes[2];
$prefix = $prefixes[3];
$prefix2 = $prefix !== null? "$prefix ": null;
$suffix = $prefixes[4];
$suffix2 = $suffix !== null? " $suffix": null;
$after = $prefixes[5];
$lines = $out->getLines(false, ...$content);
$maxlen = 0;
foreach ($lines as &$content) {
$line = $out->filterColors($content);
$len = mb_strlen($line);
if ($len > $maxlen) $maxlen = $len;
$content = [$content, $len];
}; unset($content);
if ($before !== null) {
if ($linePrefix !== null) $out->write($linePrefix);
$out->iprint($indentLevel, $prefix, substr($before, 1), str_repeat($before[0], $maxlen), $suffix);
}
foreach ($lines as [$content, $len]) {
if ($linePrefix !== null) $out->write($linePrefix);
$padding = $len < $maxlen? str_repeat(" ", $maxlen - $len): null;
$out->iprint($indentLevel, $prefix2, $content, $padding, $suffix2);
}
if ($after !== null) {
if ($linePrefix !== null) $out->write($linePrefix);
$out->iprint($indentLevel, $prefix, substr($after, 1), str_repeat($after[0], $maxlen), $suffix);
}
} else {
$prefix = $prefixes[1];
if ($prefix !== null) $prefix .= " ";
$prefix2 = str_repeat(" ", mb_strlen($prefix));
$lines = $out->getLines(false, ...$content);
foreach ($lines as $content) {
if ($linePrefix !== null) $out->write($linePrefix);
$out->iprint($indentLevel, $prefix, $content);
$prefix = $prefix2;
}
}
}
protected function _printAction(?string $linePrefix, int $level,
bool $printContent, $content,
bool $printResult, ?bool $rsuccess, $rcontent,
int $indentLevel, StdOutput $out): void {
$color = $out->isColor();
if ($rsuccess === true) $type = "success";
elseif ($rsuccess === false) $type = "failure";
else $type = "done";
$rprefixes = self::RESULT_PREFIXES[$type];
if ($color) {
$rprefix = $rprefixes[1];
$rprefix2 = null;
if ($rprefix !== null) {
$rprefix .= " ";
$rprefix2 = $out->filterColors($out->filterContent($rprefix));
$rprefix2 = str_repeat(" ", mb_strlen($rprefix2));
}
} else {
$rprefix = $rprefixes[0];
if ($rprefix !== null) $rprefix .= " ";
$rprefix2 = str_repeat(" ", mb_strlen($rprefix));
}
if ($printContent && $printResult) {
A::ensure_array($content);
if ($rcontent) {
$content[] = ": ";
$content[] = $rcontent;
}
$lines = $out->getLines(false, ...$content);
foreach ($lines as $content) {
if ($linePrefix !== null) $out->write($linePrefix);
$out->iprint($indentLevel, $rprefix, $content);
$rprefix = $rprefix2;
}
} elseif ($printContent) {
$prefixes = self::GENERIC_PREFIXES[$level]["step"];
if ($color) {
$prefix = $prefixes[1];
if ($prefix !== null) $prefix .= " ";
$prefix2 = $out->filterColors($out->filterContent($prefix));
$prefix2 = str_repeat(" ", mb_strlen($prefix2));
$suffix = $prefixes[2];
} else {
$prefix = $prefixes[0];
if ($prefix !== null) $prefix .= " ";
$prefix2 = str_repeat(" ", mb_strlen($prefix));
$suffix = null;
}
A::ensure_array($content);
$content[] = ":";
$lines = $out->getLines(false, ...$content);
foreach ($lines as $content) {
if ($linePrefix !== null) $out->write($linePrefix);
$out->iprint($indentLevel, $prefix, $content, $suffix);
$prefix = $prefix2;
}
} elseif ($printResult) {
if (!$rcontent) {
if ($type === "success") $rcontent = $color? "succès": "";
elseif ($type === "failure") $rcontent = $color? "échec": "";
elseif ($type === "done") $rcontent = "fait";
}
$rprefix = " $rprefix";
$rprefix2 = " $rprefix2";
$lines = $out->getLines(false, $rcontent);
foreach ($lines as $rcontent) {
if ($linePrefix !== null) $out->write($linePrefix);
$out->iprint($indentLevel, $rprefix, $rcontent);
$rprefix = $rprefix2;
}
}
}
protected function _printGeneric(?string $linePrefix, int $level,
string $type, $content,
int $indentLevel, StdOutput $out): void {
$prefixes = self::GENERIC_PREFIXES[$level][$type];
$content = cl::with($content);
if ($out->isColor()) {
$prefix = $prefixes[1];
$prefix2 = null;
if ($prefix !== null) {
$prefix .= " ";
$prefix2 = $out->filterColors($out->filterContent($prefix));
$prefix2 = str_repeat(" ", mb_strlen($prefix2));
}
$suffix = $prefixes[2];
$lines = $out->getLines(false, ...$content);
foreach ($lines as $content) {
if ($linePrefix !== null) $out->write($linePrefix);
$out->iprint($indentLevel, $prefix, $content, $suffix);
$prefix = $prefix2;
}
} else {
$prefix = $prefixes[0];
if ($prefix !== null) $prefix .= " ";
$prefix2 = str_repeat(" ", mb_strlen($prefix));
$lines = $out->getLines(false, ...$content);
foreach ($lines as $content) {
if ($linePrefix !== null) $out->write($linePrefix);
$out->iprint($indentLevel, $prefix, $content);
$prefix = $prefix2;
}
}
}
protected function _printGenericOrException(?int $level, string $type, $content, int $indentLevel, StdOutput $out): void {
$linePrefix = $this->getLinePrefix();
# si $content contient des exceptions, les afficher avec un level moindre
$exceptions = null;
if (is_array($content)) {
$valueContent = null;
foreach ($content as $value) {
if ($value instanceof Throwable || $value instanceof ExceptionShadow) {
$exceptions[] = $value;
} else {
$valueContent[] = $value;
}
}
if ($valueContent === null) $content = null;
elseif (count($valueContent) == 1) $content = $valueContent[0];
else $content = $valueContent;
} elseif ($content instanceof Throwable || $content instanceof ExceptionShadow) {
$exceptions[] = $content;
$content = null;
}
$printActions = true;
$showContent = $this->checkLevel($level);
if ($content !== null && $showContent) {
$this->printActions(); $printActions = false;
$this->_printGeneric($linePrefix, $level, $type, $content, $indentLevel, $out);
}
if ($exceptions !== null) {
$level1 = $this->decrLevel($level);
$showTraceback = $this->checkLevel($level1);
foreach ($exceptions as $exception) {
# tout d'abord userMessage
if ($exception instanceof UserException) {
$userMessage = UserException::get_user_message($exception);
$userMessage ??= "Une erreur technique s'est produite";
$showSummary = true;
} else {
$userMessage = UserException::get_summary($exception);
$showSummary = false;
}
if ($userMessage !== null && $showContent) {
if ($printActions) { $this->printActions(); $printActions = false; }
$this->_printGeneric($linePrefix, $level, $type, $userMessage, $indentLevel, $out);
}
# puis summary et traceback
if ($showTraceback) {
if ($printActions) { $this->printActions(); $printActions = false; }
if ($showSummary) {
$summary = UserException::get_summary($exception);
$this->_printGeneric($linePrefix, $level1, $type, $summary, $indentLevel, $out);
}
$traceback = UserException::get_traceback($exception);
$this->_printGeneric($linePrefix, $level1, $type, $traceback, $indentLevel, $out);
}
}
}
}
/** @var bool est-on dans une section? */
protected $inSection;
/** @var array section qui est en attente d'affichage */
protected $section;
function section($content, ?callable $func=null, ?int $level=null): void {
$this->_endSection();
$this->inSection = true;
if (!$this->checkLevel($level)) return;
$this->section = [
"line_prefix" => $this->getLinePrefix(),
"level" => $level,
"content" => $content,
"print_content" => true,
];
if ($func !== null) {
try {
$func($this);
} finally {
$this->_endSection();
}
}
}
protected function printSection() {
$section =& $this->section;
if ($section !== null && $section["print_content"]) {
$this->_printTitle(
$section["line_prefix"], $section["level"],
"section", $section["content"],
0, $this->err);
$section["print_content"] = false;
}
}
function _endSection(): void {
$this->inSection = false;
$this->section = null;
}
/** @var array */
protected $titles;
/** @var array */
protected $title;
function _getTitleMark(): int {
return count($this->titles);
}
function title($content, ?callable $func=null, ?int $level=null): void {
if (!$this->checkLevel($level)) return;
$until = $this->_getTitleMark();
$this->titles[] = [
"line_prefix" => $this->getLinePrefix(),
"level" => $level,
"content" => $content,
"print_content" => true,
"descs" => [],
"print_descs" => false,
];
$this->title =& $this->titles[$until];
if ($func !== null) {
try {
$func($this);
} finally {
$this->_endTitle($until);
}
}
}
function desc($content, ?int $level=null): void {
if (!$this->checkLevel($level)) return;
$title =& $this->title;
$title["descs"][] = [
"line_prefix" => $this->getLinePrefix(),
"level" => $level,
"content" => $content,
];
$title["print_descs"] = true;
}
protected function printTitles(): void {
$this->printSection();
$err = $this->err;
$indentLevel = 0;
foreach ($this->titles as &$title) {
if ($title["print_content"]) {
$this->_printTitle(
$title["line_prefix"], $title["level"],
"title", $title["content"],
$indentLevel, $err);
$title["print_content"] = false;
}
if ($title["print_descs"]) {
foreach ($title["descs"] as $desc) {
$this->_printGeneric(
$desc["line_prefix"], $desc["level"],
"desc", $desc["content"],
$indentLevel, $err);
}
$title["descs"] = [];
$title["print_descs"] = false;
}
$indentLevel++;
}; unset($title);
}
function _endTitle(?int $until=null): void {
if ($until === null) $until = $this->_getTitleMark() - 1;
while (count($this->titles) > $until) {
array_pop($this->titles);
}
if ($this->titles) {
$this->title =& $this->titles[count($this->titles) - 1];
} else {
$this->titles = [];
unset($this->title);
}
}
/** @var array */
protected $actions;
/** @var array */
protected $action;
function _getActionMark(): int {
return count($this->actions);
}
function action($content, ?callable $func=null, ?int $level=null): void {
$this->checkLevel($level);
$until = $this->_getActionMark();
$this->actions[] = [
"line_prefix" => $this->getLinePrefix(),
"level" => $level,
"content" => $content,
"print_content" => true,
"result_success" => null,
"result_content" => null,
];
$this->action =& $this->actions[$until];
if ($func !== null) {
try {
$result = $func($this);
if ($this->_getActionMark() > $until) {
$this->aresult($result);
}
} catch (Exception $e) {
$this->afailure($e);
throw $e;
} finally {
$this->_endAction($until);
}
}
}
function printActions(bool $endAction=false, ?int $overrideLevel=null): void {
$this->printTitles();
$err = $this->err;
$indentLevel = $this->getIndentLevel(false);
$lastIndex = count($this->actions) - 1;
$index = 0;
foreach ($this->actions as &$action) {
$mergeResult = $index++ == $lastIndex && $endAction;
$linePrefix = $action["line_prefix"];
$level = $overrideLevel?? $action["level"];
$content = $action["content"];
$printContent = $action["print_content"];
$rsuccess = $action["result_success"];
$rcontent = $action["result_content"];
if ($level < $this->minLevel) continue;
if ($mergeResult) {
$this->_printAction(
$linePrefix, $level,
$printContent, $content,
true, $rsuccess, $rcontent,
$indentLevel, $err);
} elseif ($printContent) {
$this->_printAction(
$linePrefix, $level,
$printContent, $content,
false, $rsuccess, $rcontent,
$indentLevel, $err);
$action["print_content"] = false;
}
$indentLevel++;
}; unset($action);
if ($endAction) $this->_endAction();
}
function step($content, ?int $level=null): void {
$this->_printGenericOrException($level, "step", $content, $this->getIndentLevel(), $this->err);
}
function asuccess($content=null, ?int $overrideLevel=null): void {
if (!$this->actions) $this->action(null);
$this->action["result_success"] = true;
$this->action["result_content"] = $content;
$this->printActions(true, $overrideLevel);
}
function afailure($content=null, ?int $overrideLevel=null): void {
if (!$this->actions) $this->action(null);
$this->action["result_success"] = false;
$this->action["result_content"] = $content;
$this->printActions(true, $overrideLevel);
}
function adone($content=null, ?int $overrideLevel=null): void {
if (!$this->actions) $this->action(null);
$this->action["result_success"] = null;
$this->action["result_content"] = $content;
$this->printActions(true, $overrideLevel);
}
function aresult($result=null, ?int $overrideLevel=null): void {
if (!$this->actions) $this->action(null);
if ($result === true) $this->asuccess(null, $overrideLevel);
elseif ($result === false) $this->afailure(null, $overrideLevel);
elseif ($result instanceof Exception) $this->afailure($result, $overrideLevel);
else $this->adone($result, $overrideLevel);
}
function _endAction(?int $until=null): void {
if ($until === null) $until = $this->_getActionMark() - 1;
while (count($this->actions) > $until) {
array_pop($this->actions);
}
if ($this->actions) {
$this->action =& $this->actions[count($this->actions) - 1];
} else {
$this->actions = [];
unset($this->action);
}
}
function print($content, ?int $level=null): void {
$this->_printGenericOrException($level, "print", $content, $this->getIndentLevel(), $this->out);
}
function info($content, ?int $level=null): void {
$this->_printGenericOrException($level, "info", $content, $this->getIndentLevel(), $this->err);
}
function note($content, ?int $level=null): void {
$this->_printGenericOrException($level, "note", $content, $this->getIndentLevel(), $this->err);
}
function warning($content, ?int $level=null): void {
$this->_printGenericOrException($level, "warning", $content, $this->getIndentLevel(), $this->err);
}
function error($content, ?int $level=null): void {
$this->_printGenericOrException($level, "error", $content, $this->getIndentLevel(), $this->err);
}
function end(bool $all=false): void {
if ($all) {
while ($this->actions) $this->adone();
while ($this->titles) $this->_endTitle();
$this->_endSection();
} elseif ($this->actions) {
$this->_endAction();
} elseif ($this->titles) {
$this->_endTitle();
} else {
$this->_endSection();
}
}
}

View File

@ -79,12 +79,12 @@ class StdOutput {
} }
function resetParams(?array $params=null): void { function resetParams(?array $params=null): void {
$output = cl::get($params, "output"); $output = $params["output"] ?? null;
$maskErrors = null; $maskErrors = null;
$color = cl::get($params, "color"); $color = $params["color"] ?? null;
$filterTags = cl::get($params, "filter_tags"); $filterTags = $params["filter_tags"] ?? null;
$indent = cl::get($params, "indent"); $indent = $params["indent"] ?? null;
$flush = cl::get($params, "flush"); $flush = $params["flush"] ?? null;
if ($output instanceof Stream) $output = $output->getResource(); if ($output instanceof Stream) $output = $output->getResource();
if ($output !== null) { if ($output !== null) {
@ -105,14 +105,14 @@ class StdOutput {
else $message = "$output: open error"; else $message = "$output: open error";
throw new Exception($message); throw new Exception($message);
} }
if ($flush === null) $flush = true; $flush ??= true;
} else { } else {
$outf = $output; $outf = $output;
} }
$this->outf = $outf; $this->outf = $outf;
$this->maskErrors = $maskErrors; $this->maskErrors = $maskErrors;
if ($color === null) $color = stream_isatty($outf); $color ??= stream_isatty($outf);
if ($flush === null) $flush = false; $flush ??= false;
} }
if ($color !== null) $this->color = boolval($color); if ($color !== null) $this->color = boolval($color);
if ($filterTags !== null) $this->filterTags = boolval($filterTags); if ($filterTags !== null) $this->filterTags = boolval($filterTags);
@ -124,23 +124,23 @@ class StdOutput {
protected $outf; protected $outf;
/** @var bool faut-il masquer les erreurs d'écriture? */ /** @var bool faut-il masquer les erreurs d'écriture? */
protected $maskErrors; protected ?bool $maskErrors;
/** @var bool faut-il autoriser la sortie en couleur? */ /** @var bool faut-il autoriser la sortie en couleur? */
protected $color; protected bool $color = false;
function isColor(): bool { function isColor(): bool {
return $this->color; return $this->color;
} }
/** @var bool faut-il enlever les tags dans la sortie? */ /** @var bool faut-il enlever les tags dans la sortie? */
protected $filterTags; protected bool $filterTags = true;
/** @var string indentation unitaire */ /** @var string indentation unitaire */
protected $indent; protected string $indent = " ";
/** @var bool faut-il flush le fichier après l'écriture de chaque ligne */ /** @var bool faut-il flush le fichier après l'écriture de chaque ligne */
protected $flush; protected bool $flush = true;
function isatty(): bool { function isatty(): bool {
return stream_isatty($this->outf); return stream_isatty($this->outf);
@ -167,6 +167,7 @@ class StdOutput {
$text .= "m"; $text .= "m";
return $text; return $text;
} }
function filterContent(string $text): string { function filterContent(string $text): string {
# couleur au début # couleur au début
$text = preg_replace_callback('/<color([^>]*)>/', [self::class, "replace_colors"], $text); $text = preg_replace_callback('/<color([^>]*)>/', [self::class, "replace_colors"], $text);
@ -178,6 +179,7 @@ class StdOutput {
} }
return $text; return $text;
} }
function filterColors(string $text): string { function filterColors(string $text): string {
return preg_replace('/\x1B\[.*?m/', "", $text); return preg_replace('/\x1B\[.*?m/', "", $text);
} }

View File

@ -7,13 +7,86 @@ use nulib\output\IMessenger;
* Interface _IMessenger: méthodes privées de IMessenger * Interface _IMessenger: méthodes privées de IMessenger
*/ */
interface _IMessenger extends IMessenger { interface _IMessenger extends IMessenger {
function _endSection(): void; const INDENT = " ";
function _getTitleMark(): int; const DATE_FORMAT = 'Y-m-d\TH:i:s.u';
function _endTitle(?int $until=null): void; const VALID_LEVELS = [self::DEBUG, self::MINOR, self::NORMAL, self::MAJOR, self::NONE];
function _getActionMark(): int; const LEVEL_MAP = [
"debug" => self::DEBUG,
"minor" => self::MINOR, "verbose" => self::MINOR,
"normal" => self::NORMAL,
"major" => self::MAJOR, "quiet" => self::MAJOR,
"none" => self::NONE, "silent" => self::NONE,
];
function _endAction(?int $until=null): void; const GENERIC_PREFIXES = [
self::MAJOR => [
"section" => [true, "SECTION!", "===", "<color @b>=", "=</color>", "==="],
"title" => [false, "TITLE!", null, "<color @b>T", "</color>", "==="],
"desc" => ["DESC!", "<color @b>></color>", ""],
"error" => ["CRIT.ERROR!", "<color @r>E!", "</color>"],
"warning" => ["CRIT.WARNING!", "<color @y>W!", "</color>"],
"note" => ["ATTENTION!", "<color @g>N!", "</color>"],
"info" => ["IMPORTANT!", "<color @b>N!", "</color>"],
"step" => ["*", "<color @w>.</color>", ""],
"print" => [null, null, null],
],
self::NORMAL => [
"section" => [true, "SECTION:", "---", "<color @b>-", "-</color>", "---"],
"title" => [false, "TITLE:", null, "<color @b>T</color><color b>", "</color>", "---"],
"desc" => ["DESC:", "<color @b>></color>", ""],
"error" => ["ERROR:", "<color @r>E</color><color r>", "</color>"],
"warning" => ["WARNING:", "<color @y>W</color><color y>", "</color>"],
"note" => ["NOTE:", "<color @g>N</color>", ""],
"info" => ["INFO:", "<color @b>I</color>", ""],
"step" => ["*", "<color @w>.</color>", ""],
"print" => [null, null, null],
],
self::MINOR => [
"section" => [true, "section", null, "<color @w>>>", "<<</color>", null],
"title" => [false, "title", null, "<color b>t", "</color>", null],
"desc" => ["desc", "<color b>></color>", ""],
"error" => ["error", "<color r>E</color><color -r>", "</color>"],
"warning" => ["warning", "<color y>W</color><color -y>", "</color>"],
"note" => ["note", "<color g>N</color>", ""],
"info" => ["info", "<color b>I</color><color w>", "</color>"],
"step" => ["*", "<color w>.</color>", ""],
"print" => [null, null, null],
],
self::DEBUG => [
"section" => [true, "section", null, "<color @w>>>", "<<</color>", null],
"title" => [false, "title", null, "<color b>t", "</color>", null],
"desc" => ["desc", "<color b>></color>", ""],
"error" => ["debugE", "<color r>e</color><color -r>", "</color>"],
"warning" => ["debugW", "<color y>w</color><color -y>", "</color>"],
"note" => ["debugN", "<color b>i</color>", ""],
"info" => ["debug", "<color @w>D</color><color w>", "</color>"],
"step" => ["*", "<color w>.</color>", ""],
"print" => [null, null, null],
],
];
const RESULT_PREFIXES = [
"failure" => ["(FAILURE)", "<color r>✘</color>"],
"success" => ["(SUCCESS)", "<color @g>✔</color>"],
"done" => [null, null],
];
function section__afterFunc(): void;
/** @return int[] */
function title__getMarks(): array;
/** @param int[] $marks */
function title__beforeFunc(array $marks): void;
/** @param int[] $marks */
function title__afterFunc(array $marks): void;
/** @return int[] */
function action__getMarks(): array;
/** @param int[] $marks */
function action__beforeFunc(array $marks): void;
/** @param int[] $marks */
function action__afterFunc(array $marks, $result): void;
} }

15
php/src/output/web.php Normal file
View File

@ -0,0 +1,15 @@
<?php
namespace nulib\output;
/**
* Class web: afficher un message à l'utilisateur en HTML
*
* Cette classe est utilisable directement sans initialisation préalable.
*/
class web extends _messenger {
use _TMessenger;
static function get(): IMessenger {
return static::$msg ??= new WebMessenger();
}
}

View File

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

View File

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

View File

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

View File

@ -30,6 +30,11 @@ class Delay {
else return new static($delay, $from); else return new static($delay, $from);
} }
/**
* pour une durée infinie, l'intervalle est toujours de 1000 ans dans le futur
*/
const INF_INTERVAL = "P1000Y";
/** valeurs par défaut de x et y pour les unités supportées */ /** valeurs par défaut de x et y pour les unités supportées */
const DEFAULTS = [ const DEFAULTS = [
"w" => [0, 5], "w" => [0, 5],
@ -39,8 +44,7 @@ class Delay {
"s" => [1, 0], "s" => [1, 0],
]; ];
static function compute_dest(int $x, string $u, ?int $y, ?DateTimeInterface $from): array { protected static function compute_dest(int $x, string $u, ?int $y, MutableDateTime $dest): array {
$dest = DateTime::with($from)->clone();
$yu = null; $yu = null;
switch ($u) { switch ($u) {
case "w": case "w":
@ -89,10 +93,10 @@ class Delay {
} }
function __construct($delay, ?DateTimeInterface $from=null) { function __construct($delay, ?DateTimeInterface $from=null) {
if ($from === null) $from = new DateTime(); $from = MutableDateTime::with($from)->clone(true);
if ($delay === "INF") { if ($delay === null || $delay === "INF") {
$dest = DateTime::with($from)->clone(); # $dest === null signifie un délai infini
$dest->add(new DateInterval("P9999Y")); $dest = null;
$repr = "INF"; $repr = "INF";
} elseif (is_int($delay)) { } elseif (is_int($delay)) {
[$dest, $repr] = self::compute_dest($delay, "s", null, $from); [$dest, $repr] = self::compute_dest($delay, "s", null, $from);
@ -116,37 +120,53 @@ class Delay {
} }
function __clone() { function __clone() {
$this->dest = clone $this->dest; if ($this->dest !== null) {
$this->dest = clone $this->dest;
}
} }
function __serialize(): array { function __serialize(): array {
return [$this->dest, $this->repr]; $dest = $this->dest;
if ($dest !== null) $dest = $dest->clone();
return [$dest, $this->repr];
} }
function __unserialize(array $data): void { function __unserialize(array $data): void {
[$this->dest, $this->repr] = $data; [$dest, $this->repr] = $data;
if ($dest !== null) $dest = $dest->clone(true);
$this->dest = $dest;
} }
/** @var DateTime */ protected ?MutableDateTime $dest;
protected $dest;
function getDest(): DateTime { function getDest(): DateTime {
return $this->dest; $dest = $this->dest;
if ($dest === null) {
$dest = new MutableDateTime();
$dest->add(new \DateInterval(self::INF_INTERVAL));
}
return $dest->clone();
} }
function addDuration($duration) { function addDuration($duration): self {
if (is_numeric($duration) && $duration < 0) { if ($this->dest !== null) {
$this->dest->sub(DateInterval::with(-$duration)); if (is_numeric($duration) && $duration < 0) {
} else { $this->dest->sub(DateInterval::with(-$duration));
$this->dest->add(DateInterval::with($duration)); } else {
$this->dest->add(DateInterval::with($duration));
}
} }
return $this;
} }
function subDuration($duration) { function subDuration($duration): self {
if (is_numeric($duration) && $duration < 0) { if ($this->dest !== null) {
$this->dest->add(DateInterval::with(-$duration)); if (is_numeric($duration) && $duration < 0) {
} else { $this->dest->add(DateInterval::with(-$duration));
$this->dest->sub(DateInterval::with($duration)); } else {
$this->dest->sub(DateInterval::with($duration));
}
} }
return $this;
} }
/** @var string */ /** @var string */
@ -156,23 +176,20 @@ class Delay {
return $this->repr; return $this->repr;
} }
protected function _getDiff(?DateTimeInterface $now=null): \DateInterval {
if ($now === null) $now = new DateTime();
return $this->dest->diff($now);
}
/** retourner true si le délai imparti est écoulé */
function isElapsed(?DateTimeInterface $now=null): bool {
if ($this->repr === "INF") return false;
else return $this->_getDiff($now)->invert == 0;
}
/** /**
* retourner l'intervalle entre le moment courant et la destination. * retourner l'intervalle entre le moment courant et la destination.
* *
* l'intervalle est négatif si le délai n'est pas écoulé, positif sinon * l'intervalle est négatif si le délai n'est pas écoulé, positif sinon
*/ */
function getDiff(?DateTimeInterface $now=null): DateInterval { function getDiff(?DateTimeInterface $now=null): DateInterval {
return new DateInterval($this->_getDiff($now)); $dest = $this->dest;
if ($dest !== null) return $dest->diff($now ?? new \DateTime());
else return new DateInterval("-".self::INF_INTERVAL);
}
/** retourner true si le délai imparti est écoulé */
function isElapsed(?DateTimeInterface $now=null): bool {
if ($this->dest === null) return false;
else return $this->getDiff($now)->invert == 0;
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

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

147
php/src/php/time/_utils.php Normal file
View File

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

View File

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

11
php/src/ref/ref_cache.php Normal file
View File

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

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