diff --git a/.idea/nulib-base.iml b/.idea/nulib-base.iml index 55df4ce..419ada4 100644 --- a/.idea/nulib-base.iml +++ b/.idea/nulib-base.iml @@ -4,7 +4,7 @@ - + diff --git a/.idea/php-docker-settings.xml b/.idea/php-docker-settings.xml index bd786be..9e9123b 100644 --- a/.idea/php-docker-settings.xml +++ b/.idea/php-docker-settings.xml @@ -17,6 +17,36 @@ + + + + + + + + + + + + + + diff --git a/.idea/php.xml b/.idea/php.xml index 7e6be21..06ee8b4 100644 --- a/.idea/php.xml +++ b/.idea/php.xml @@ -2,7 +2,7 @@ - + @@ -17,44 +17,61 @@ - + - - - - - - - + + + + + + + + + + + + + - - - - + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + diff --git a/.pman.conf b/.pman.conf index 1686b67..184adfa 100644 --- a/.pman.conf +++ b/.pman.conf @@ -4,7 +4,7 @@ UPSTREAM=dev74 DEVELOP=dev82 FEATURE=wip82/ RELEASE=rel82- -MAIN=dist82 +MAIN=main82 TAG_SUFFIX=p82 HOTFIX=hotf82- DIST= diff --git a/TODO.md b/TODO.md index 8eea214..4531d55 100644 --- a/TODO.md +++ b/TODO.md @@ -1,3 +1,7 @@ +# nulib + +* [wip](wip/TODO.md) + # nulib/bash * [nulib/bash](bash/TODO.md) diff --git a/bash/src/_output_color.sh b/bash/src/_output_color.sh index afe6428..2cc5cc8 100644 --- a/bash/src/_output_color.sh +++ b/bash/src/_output_color.sh @@ -6,7 +6,8 @@ function __esection() { local length="${COLUMNS:-80}" setx lsep=__complete "$prefix" "$length" - - recho "$COULEUR_BLEUE$lsep$COULEUR_NORMALE" + recho " +$COULEUR_BLEUE$lsep$COULEUR_NORMALE" [ -n "$*" ] || return 0 length=$((length - 1)) setx -a lines=echo "$1" diff --git a/bash/src/_output_vanilla.sh b/bash/src/_output_vanilla.sh index cbd466f..165f1d7 100644 --- a/bash/src/_output_vanilla.sh +++ b/bash/src/_output_vanilla.sh @@ -6,7 +6,8 @@ function __esection() { local length="${COLUMNS:-80}" setx lsep=__complete "$prefix" "$length" - - recho "$lsep" + recho " +$lsep" [ -n "$*" ] || return 0 length=$((length - 1)) setx -a lines=echo "$1" diff --git a/bash/src/base.args.sh b/bash/src/base.args.sh index 43e63ae..c70cfa8 100644 --- a/bash/src/base.args.sh +++ b/bash/src/base.args.sh @@ -184,7 +184,7 @@ function __nulib_args_parse_args() { *) die "Invalid arg definition: expected option, got '$1'" || return;; esac # est-ce que l'option prend un argument? - local __def __longdef __witharg __valdesc + local __def __longdef __witharg __valdesc __defvaldesc __witharg= for __def in "${__defs[@]}"; do if [ "${__def%::*}" != "$__def" ]; then @@ -346,16 +346,19 @@ $prefix$usage" fi # est-ce que l'option prend un argument? __witharg= - __valdesc=value + __valdesc= + __defvaldesc=value for __def in "${__defs[@]}"; do if [ "${__def%::*}" != "$__def" ]; then [ "$__witharg" != : ] && __witharg=:: [ -n "${__def#*::}" ] && __valdesc="[${__def#*::}]" + __defvaldesc="[value]" elif [ "${__def%:*}" != "$__def" ]; then __witharg=: [ -n "${__def#*:}" ] && __valdesc="${__def#*:}" fi done + [ -n "$__valdesc" ] || __valdesc="$__defvaldesc" # description de l'option local first=1 thelp tdesc for __def in "${__defs[@]}"; do diff --git a/bash/src/base.init.sh b/bash/src/base.init.sh index de5ae8c..671bb33 100644 --- a/bash/src/base.init.sh +++ b/bash/src/base.init.sh @@ -21,6 +21,13 @@ if [ -z "$NULIB_NO_INIT_ENV" ]; then fi [ -n "$NULIBDIR" ] || NULIBDIR="$MYDIR" + # Si le script courant est un lien, calculer le répertoire destination + if [ -n "$MYREALSELF" -a -n "$MYSELF" ]; then + MYREALSELF="$(readlink -f "$MYSELF")" + MYREALDIR="$(dirname -- "$MYREALSELF")" + MYREALNAME="$(basename -- "$MYREALSELF")" + fi + # Repertoire temporaire [ -z "$TMPDIR" -a -d "$HOME/tmp" ] && TMPDIR="$HOME/tmp" [ -z "$TMPDIR" ] && TMPDIR="${TMP:-${TEMP:-/tmp}}" diff --git a/bash/src/install.sh b/bash/src/install.sh new file mode 100644 index 0000000..fc4dc51 --- /dev/null +++ b/bash/src/install.sh @@ -0,0 +1,147 @@ +# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8 +##@cooked nocomments +module: install "Outils de haut niveau pour installer des fichiers de configuration" +require: base + +if [ -z "$NULIB_INSTALL_CONFIGURED" ]; then + # Faut-il afficher le nom des fichiers copié/créés? + export NULIB_INSTALL_VERBOSE=1 + # Faut-il afficher les destinations avec ppath? + export NULIB_INSTALL_USES_PPATH= +fi +export NULIB_INSTALL_CONFIGURED=1 + +function ensure_exists() { + # Créer le fichier vide "$1" s'il n'existe pas déjà, avec les permissions + # $2(=644). retourner vrai si le fichier a été créé sans erreur + [ -f "$1" ] || { + if [ -n "$NULIB_INSTALL_VERBOSE" ]; then + if [ -n "$NULIB_INSTALL_USES_PPATH" ]; then + estep "$(ppath "$1")" + else + estep "$1" + fi + fi + mkdirof "$1" + local r=0 + touch "$1" && chmod "${2:-644}" "$1" || r=$? + return $r + } + return 1 +} + +function __nulib_install_show_args() { + if [ -z "$NULIB_INSTALL_VERBOSE" ]; then + : + elif [ -n "$NULIB_INSTALL_USES_PPATH" ]; then + estep "$1 --> $(ppath "$2")${3:+/}" + else + estep "$1 --> $2${3:+/}" + fi +} + +function copy_replace() { + # Copier de façon inconditionnelle le fichier $1 vers le fichier $2, en + # réinitialisation les permissions à la valeur $3 + local src="$1" dest="$2" + local srcname="$(basename -- "$src")" + + [ -d "$dest" ] && dest="$dest/$srcname" + mkdirof "$dest" || return 1 + + if [ -n "$NULIB_INSTALL_VERBOSE" ]; then + local destarg destname slash + destarg="$(dirname -- "$dest")" + destname="$(basename -- "$dest")" + if [ "$srcname" == "$destname" ]; then + slash=1 + else + destarg="$destarg/$destname" + fi + __nulib_install_show_args "$srcname" "$destarg" "$slash" + fi + local r=0 + if cp "$src" "$dest"; then + if [ -n "$3" ]; then + chmod "$3" "$dest" || r=$? + fi + fi + return $r +} + +function copy_new() { + # Copier le fichier "$1" vers le fichier "$2", avec les permissions $3(=644) + # Ne pas écraser le fichier destination s'il existe déjà + # Retourner vrai si le fichier a été copié sans erreur + local src="$1" dest="$2" + + [ -d "$dest" ] && dest="$dest/$(basename -- "$src")" + mkdirof "$dest" || return 1 + + if [ ! -e "$dest" ]; then + copy_replace "$src" "$dest" "$3" + else + return 1 + fi +} + +function copy_update() { + # Copier le fichier "$1" vers le fichier "$2", si $2 n'existe pas, ou si $1 + # a été modifié par rapport à $2. Réinitialiser le cas échéant les + # permissions à la valeur $3 + # Retourner vrai si le fichier a été copié sans erreur. + local src="$1" dest="$2" + + [ -d "$dest" ] && dest="$dest/$(basename -- "$src")" + mkdirof "$dest" || return 1 + + if [ ! -e "$dest" ]; then + copy_replace "$src" "$dest" "$3" + elif testdiff "$src" "$dest"; then + copy_replace "$src" "$dest" "$3" + else + return 1 + fi +} + +COPY_UPDATE_ASK_DEFAULT= +function copy_update_ask() { + # Copier ou mettre à jour le fichier $1 vers le fichier $2. + # Si le fichier existe déjà, la différence est affichée, et une confirmation + # est demandée pour l'écrasement du fichier. + # si $1 commence par '-' alors on s'en sert comme option pour configurer le + # niveau d'interaction pour demander la confirmation. les paramètres sont + # alors décalés + # Retourner vrai si le fichier a été copié sans erreur. + local interopt=-c + if [[ "$1" == -* ]]; then + interopt="$1" + shift + fi + local src="$1" dest="$2" + + [ -d "$dest" ] && dest="$dest/$(basename -- "$src")" + mkdirof "$dest" || return 1 + + [ -f "$dest" ] || copy_replace "$src" "$dest" + if testdiff "$src" "$dest"; then + check_interaction "$interopt" && diff -u "$dest" "$src" + if ask_yesno "$interopt" "Voulez-vous remplacer $(ppath "$dest") par la nouvelle version?" "${COPY_UPDATE_ASK_DEFAULT:-C}"; then + copy_replace "$src" "$dest" "$3" + return $? + elif ! check_interaction "$interopt"; then + ewarn "Les mises à jours suivantes sont disponibles:" + diff -u "$dest" "$src" + ewarn "Le fichier $(ppath "$dest") n'a pas été mis à jour" + fi + fi + return 1 +} + +function link_new() { + # Si $2 n'existe pas, créer le lien symbolique $2 pointant vers $1 + [ -e "$2" ] && return 0 + + __nulib_install_show_args "$(basename -- "$2")" "$(dirname -- "$1")" + ln -s "$1" "$2" +} diff --git a/bash/src/pman.conf.sh b/bash/src/pman.conf.sh index c33874c..f5a4ace 100644 --- a/bash/src/pman.conf.sh +++ b/bash/src/pman.conf.sh @@ -1,7 +1,5 @@ # -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8 -## configuration par défaut - UPSTREAM= DEVELOP=develop FEATURE=wip/ diff --git a/bash/src/pman.sh b/bash/src/pman.sh index 094b269..41e2da9 100644 --- a/bash/src/pman.sh +++ b/bash/src/pman.sh @@ -25,9 +25,146 @@ DIST= NOAUTO= CONFIG_VARS=( - UPSTREAM DEVELOP FEATURE RELEASE MAIN TAG_PREFIX TAG_SUFFIX HOTFIX DIST NOAUTO + UPSTREAM DEVELOP FEATURE RELEASE MAIN HOTFIX DIST + TAG_PREFIX TAG_SUFFIX NOAUTO ) +################################################################################ + +PMAN_BRANCHES=(UPSTREAM DEVELOP FEATURE MAIN DIST) +PMAN_TOOL_PDEV=DEVELOP +PMAN_TOOL_PWIP=FEATURE +PMAN_TOOL_PMAIN=MAIN +PMAN_TOOL_PDIST=DIST +UPSTREAM_BASE=DEVELOP ; UPSTREAM_MERGE_FROM= ; UPSTREAM_MERGE_TO=DEVELOP ; UPSTREAM_PREL= ; UPSTREAM_DELETE= +DEVELOP_BASE=MAIN ; DEVELOP_MERGE_FROM=FEATURE ; DEVELOP_MERGE_TO=MAIN ; DEVELOP_PREL=from ; DEVELOP_DELETE=to +MAIN_BASE=DEVELOP ; MAIN_MERGE_FROM=DEVELOP ; MAIN_MERGE_TO=DIST ; MAIN_PREL=to ; 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=from + +UPSTREAM_CREATE_FUNCTION=_create_upstream_action + +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" merge_dir="${2:-to}" infos + [ -n "$branch" ] || return 1 + infos="${branch^^}_PREL" + [ "${!infos}" == "$merge_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" merge_dir="${2:-to}" infos + [ -n "$branch" ] || return 1 + infos="${branch^^}_DELETE" + [ "${!infos}" == "$merge_dir" ] +} + +: " +# description des variables # + +* REF_BRANCH -- code de la branche de référence basé sur le nom de l'outil +* RefBranch -- 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 + +* REF_UNIQUE -- si la branche de référence est unique. est vide pour les + codes de branches multiples, telle que FEATURE + +* BASE_BRANCH -- branche de base à partir de laquelle créer la branche + de référence +* BaseBranch -- 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 + +* MERGE_FROM -- code de la branche source à partir de laquelle la fusion + est faite dans REF_BRANCH. vide si la branche n'a pas de source +* MERGE_TO -- code de la branche destination dans laquelle la fusion est + faite depuis REF_BRANCH. vide si la branche n'a pas de destination +* MERGE_DIR -- direction de la fusion: + 'from' si on fait REF_BRANCH --> MERGE_TO + 'to' si on fait MERGE_FROM --> REF_BRANCH +* PREL_MERGE -- si la fusion devrait se faire avec prel +* DELETE_MERGED -- s'il faut supprimer la branche source après la fusion + +* MERGE_SRC -- code de la branche source pour la fusion, ou vide si la + fusion n'est pas possible +* MergeSrc -- 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 + +* MERGE_DEST -- code de la branche destination pour la fusion, ou vide si + la fusion n'est pas possible +* MergeDest -- 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" + +function set_pman_vars() { + RefBranch="${!REF_BRANCH}" + case "$REF_BRANCH" in + FEATURE|RELEASE|HOTFIX) REF_UNIQUE=;; + *) REF_UNIQUE=1;; + esac + + BASE_BRANCH=$(get_base_branch "$REF_BRANCH") + [ -n "$BASE_BRANCH" ] && BaseBranch="${!BASE_BRANCH}" || BaseBranch= + + MERGE_FROM=$(get_merge_from_branch "$REF_BRANCH") + MERGE_TO=$(get_merge_to_branch "$REF_BRANCH") + if [ -n "$1" ]; then MERGE_DIR="$1" + else MERGE_DIR=from + fi + PREL_MERGE=$(should_prel_merge "$REF_BRANCH" "$MERGE_DIR" && echo 1) + DELETE_MERGED=$(should_delete_merged "$REF_BRANCH" "$MERGE_DIR" && echo 1) + case "$MERGE_DIR" in + from) + MERGE_SRC="$REF_BRANCH" + MERGE_DEST="$MERGE_TO" + ;; + to) + MERGE_SRC="$MERGE_FROM" + MERGE_DEST="$REF_BRANCH" + ;; + esac + + [ -n "$MERGE_SRC" ] && MergeSrc="${!MERGE_SRC}" || MergeSrc= + [ -n "$MERGE_DEST" ] && MergeDest="${!MERGE_DEST}" || MergeDest= +} + +################################################################################ + function _init_changelog() { setx date=date +%d/%m/%Y-%H:%M ac_set_tmpfile changelog @@ -77,7 +214,7 @@ $1 == "|" { } function _list_commits() { - local source="${1:-$SrcBranch}" dest="${2:-$DestBranch}" mergebase + local source="${1:-$MergeSrc}" dest="${2:-$MergeDest}" mergebase setx mergebase=git merge-base "$dest" "$source" git log --oneline --graph --no-decorate "$mergebase..$source" | grep -vF '|\' | grep -vF '|/' | sed -r 's/^(\| )+\* +/| /; s/^\* +/+ /' | @@ -85,7 +222,7 @@ function _list_commits() { } function _show_diff() { - local source="${1:-$SrcBranch}" dest="${2:-$DestBranch}" mergebase + local source="${1:-$MergeSrc}" dest="${2:-$MergeDest}" mergebase setx mergebase=git merge-base "$dest" "$source" git diff ${_sd_COLOR:+--color=$_sd_COLOR} "$mergebase..$source" } @@ -147,22 +284,27 @@ EOF ################################################################################ # Config -function ensure_gitdir() { +function check_gitdir() { # commencer dans le répertoire indiqué local chdir="$1" if [ -n "$chdir" ]; then - cd "$chdir" || die || return + cd "$chdir" || return 1 fi # se mettre à la racine du dépôt git local gitdir - git_ensure_gitvcs + git_check_gitvcs || return 1 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() { - local what="${1:-all}"; shift + local branch what="${1:-all}"; shift case "$what" in all) [ -n "$Origin" ] || Origin=origin @@ -172,30 +314,6 @@ function load_branches() { setx -a AllBranches=git_list_pbranches "$Origin" ;; current) - SrcBranch="$1" - [ -n "$SrcBranch" ] || SrcBranch="$CurrentBranch" - case "$SrcBranch" in - "$UPSTREAM") SrcType=upstream; DestBranch="$DEVELOP";; - "$FEATURE"*) SrcType=feature; DestBranch="$DEVELOP";; - "$DEVELOP") SrcType=develop; DestBranch="$MAIN";; - "$RELEASE"*) SrcType=release; DestBranch="$MAIN";; - "$HOTFIX"*) SrcType=hotfix; DestBranch="$MAIN";; - "$MAIN") SrcType=main; DestBranch="$DIST";; - "$DIST") SrcType=dist; DestBranch=;; - *) SrcType=; DestBranch=;; - esac - case "$DestBranch" in - "$UPSTREAM") DestType=upstream;; - "$FEATURE"*) DestType=feature;; - "$DEVELOP") DestType=develop;; - "$RELEASE"*) DestType=release;; - "$HOTFIX"*) DestType=hotfix;; - "$MAIN") DestType=main;; - "$DIST") DestType=dist;; - *) DestType=;; - esac - - local branch UpstreamBranch= FeatureBranches=() DevelopBranch= @@ -203,23 +321,32 @@ function load_branches() { HotfixBranch= MainBranch= DistBranch= + IfRefBranch= + IfBaseBranch= + IfMergeSrc= + IfMergeDest= for branch in "${LocalBranches[@]}"; do if [ "$branch" == "$UPSTREAM" ]; then UpstreamBranch="$branch" - elif [[ "$branch" == "$FEATURE"* ]]; then + elif [ -n "$FEATURE" ] && [[ "$branch" == "$FEATURE"* ]]; then FeatureBranches+=("$branch") - elif [ "$branch" == "$DEVELOP" ]; then + elif [ -n "$DEVELOP" -a "$branch" == "$DEVELOP" ]; then DevelopBranch="$branch" - elif [[ "$branch" == "$RELEASE"* ]]; then + elif [ -n "$RELEASE" ] && [[ "$branch" == "$RELEASE"* ]]; then ReleaseBranch="$branch" - elif [[ "$branch" == "$HOTFIX"* ]]; then + elif [ -n "$HOTFIX" ] && [[ "$branch" == "$HOTFIX"* ]]; then HotfixBranch="$branch" - elif [ "$branch" == "$MAIN" ]; then + elif [ -n "$MAIN" -a "$branch" == "$MAIN" ]; then MainBranch="$branch" - elif [ "$branch" == "$DIST" ]; then + elif [ -n "$DIST" -a "$branch" == "$DIST" ]; then DistBranch="$branch" fi + [ -n "$RefBranch" -a "$branch" == "$RefBranch" ] && IfRefBranch="$branch" + [ -n "$BaseBranch" -a "$branch" == "$BaseBranch" ] && IfBaseBranch="$branch" + [ -n "$MergeSrc" -a "$branch" == "$MergeSrc" ] && IfMergeSrc="$branch" + [ -n "$MergeDest" -a "$branch" == "$MergeDest" ] && IfMergeDest="$branch" done + [ -n "$IfMergeSrc" -a "$IfMergeDest" ] && IfCanMerge=1 || IfCanMerge= ;; esac } @@ -244,9 +371,6 @@ function load_config() { elif [ -f .pman.conf ]; then ConfigFile="$(pwd)/.pman.conf" source "$ConfigFile" - elif [ -n "$1" -a -n "${MYNAME#$1}" ]; then - ConfigFile="$NULIBDIR/bash/src/pman${MYNAME#$1}.conf.sh" - source "$ConfigFile" else ConfigFile="$NULIBDIR/bash/src/pman.conf.sh" fi @@ -319,10 +443,8 @@ function _mscript_start() { #!/bin/bash $(qvals source "$NULIBDIR/load.sh") || exit 1 -$(echo_setv SrcBranch="$SrcBranch") -$(echo_setv SrcType="$SrcType") -$(echo_setv DestBranch="$DestBranch") -$(echo_setv DestType="$DestType") +$(echo_setv MergeSrc="$MergeSrc") +$(echo_setv MergeDest="$MergeDest") merge= delete= @@ -342,32 +464,32 @@ function _mscript_merge_branch() { local msg # basculer sur la branche - _scripta "switch to branch $DestBranch" <>"$changelog" - if [ -s CHANGES.md ]; then - echo >>"$changelog" - cat CHANGES.md >>"$changelog" - fi "${EDITOR:-nano}" +7 "$changelog" [ -s "$changelog" ] || exit_with ewarn "Création de la release annulée" # créer la branche de release et basculer dessus _scripta "create branch $ReleaseBranch" <CHANGES.md +')") >"\$tmpchanges" +if [ -s CHANGES.md ]; then + echo >>"\$tmpchanges" + cat CHANGES.md >>"\$tmpchanges" +fi +cat "\$tmpchanges" >CHANGES.md +rm -f "\$tmpchanges" git add CHANGES.md EOF @@ -471,3 +594,176 @@ function _rscript_delete_release_branch() { $comment$(qvals git branch -D "$ReleaseBranch")$or_die EOF } + +################################################################################ +# Outils + +function dump_action() { + enote "Valeurs des variables: +REF_BRANCH=$REF_BRANCH${RefBranch:+ RefBranch=$RefBranch IfRefBranch=$IfRefBranch} +BASE_BRANCH=$BASE_BRANCH${BaseBranch:+ BaseBranch=$BaseBranch IfBaseBranch=$IfBaseBranch} +MERGE_FROM=$MERGE_FROM +MERGE_TO=$MERGE_TO +MERGE_DIR=$MERGE_DIR +PREL_MERGE=$PREL_MERGE +DELETE_MERGED=$DELETE_MERGED +MERGE_SRC=$MERGE_SRC${MergeSrc:+ MergeSrc=$MergeSrc IfMergeSrc=$IfMergeSrc} +MERGE_DEST=$MERGE_DEST${MergeDest:+ MergeDest=$MergeDest 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 [ "$REF_BRANCH" == FEATURE ]; then + if [ $# -gt 0 ]; then + RefBranch="$FEATURE${1#$FEATURE}" + elif [[ "$CurrentBranch" == "$FEATURE"* ]]; then + RefBranch="$CurrentBranch" + elif [ ${#FeatureBranches[*]} -eq 0 ]; then + die "Vous devez spécifier la branche de feature" + elif [ ${#FeatureBranches[*]} -eq 1 ]; then + RefBranch="${FeatureBranches[0]}" + else + simple_menu \ + RefBranch FeatureBranches \ + -t "Branches de feature" \ + -m "Veuillez choisir la branche de feature" \ + -d "${FeatureBranches[0]}" + fi + else + die "resolve_unique_branch: $REF_BRANCH: non implémenté" + fi + if [ "$MERGE_DIR" == from ]; then + MergeSrc="$RefBranch" + elif [ "$MERGE_DIR" == to ]; then + MergeDest="$RefBranch" + fi +} + +function _ensure_ref_branch() { + [ -n "$RefBranch" ] || die "\ +La branche $REF_BRANCH n'a pas été définie. +Veuillez éditer le fichier .pman.conf" + [ "$1" == init -o -n "$IfRefBranch" ] || die "$RefBranch: cette branche n'existe pas (le dépôt a-t-il été initialisé?)" +} + +function _ensure_base_branch() { + [ -n "$BaseBranch" ] || die "\ +La branche $BASE_BRANCH n'a pas été définie. +Veuillez éditer le fichier .pman.conf" + [ "$1" == init -o -n "$IfBaseBranch" ] || die "$BaseBranch: cette branche n'existe pas (le dépôt a-t-il été initialisé?)" +} + +function _create_default_action() { + enote "Vous allez créer la branche ${COULEUR_BLEUE}$RefBranch${COULEUR_NORMALE} <-- ${COULEUR_ROUGE}$BaseBranch${COULEUR_NORMALE}" + ask_yesno "Voulez-vous continuer?" O || die + + einfo "Création de la branche $RefBranch" + git checkout -b "$RefBranch" "$BaseBranch" || die + push_branches+=("$RefBranch") +} + +function _create_upstream_action() { + enote "Vous allez créer la branche ${COULEUR_BLEUE}$RefBranch${COULEUR_NORMALE}" + ask_yesno "Voulez-vous continuer?" O || die + + # faire une copie de la configuration actuelle + local config; ac_set_tmpfile config + set -x; ls -l "$ConfigFile" #XXX + cp "$ConfigFile" "$config" + set +x #XXX + + einfo "Création de la branche $RefBranch" + git checkout --orphan "$RefBranch" || die + git rm -rf . + cp "$config" .pman.conf + git add .pman.conf + git commit -m "commit initial" + push_branches+=("$RefBranch") + + einfo "Fusion dans $DevelopBranch" + git checkout "$DevelopBranch" + git merge \ + --no-ff -m "Intégration initiale de la branche $RefBranch" \ + -srecursive -Xours --allow-unrelated-histories \ + "$RefBranch" + push_branches+=("$DevelopBranch") +} + +function checkout_action() { + local -a push_branches + + [ -n "$REF_UNIQUE" ] || resolve_unique_branch "$@" + _ensure_ref_branch init + + if array_contains LocalBranches "$RefBranch"; then + git checkout "$RefBranch" + elif array_contains AllBranches "$RefBranch"; then + enote "$RefBranch: une branche du même nom existe dans l'origine" + ask_yesno "Voulez-vous basculer sur cette branche?" O || die + git checkout "$RefBranch" + else + _ensure_base_branch + resolve_should_push + + local create_function + create_function="${REF_BRANCH}_CREATE_FUNCTION"; create_function="${!create_function}" + [ -n "$create_function" ] || create_function=_create_default_action + "$create_function" + + _push_branches + fi +} + +function ensure_merge_branches() { + [ -n "$MergeSrc" ] || die "\ +$RefBranch: configuration de fusion non trouvée: la branche $MERGE_SRC n'a pas été définie. +Veuillez éditer le fichier .pman.conf" + [ -n "$MergeDest" ] || die "\ +$RefBranch: configuration de fusion non trouvée: la branche $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" "$MergeSrc" || die "$MergeSrc: branche source introuvable" + array_contains "$branches" "$MergeDest" || die "$MergeDest: branche destination introuvable" +} + +function _show_action() { + local commits + setx commits=_list_commits "$MergeSrc" "$MergeDest" + if [ -n "$commits" ]; then + if [ $ShowLevel -ge 2 ]; then + { + echo "\ +# Commits à fusionner $MergeSrc --> $MergeDest + +$commits +" + _sd_COLOR=always _show_diff + } | less -eRF + else + einfo "Commits à fusionner $MergeSrc --> $MergeDest" + eecho "$commits" + fi + fi +} + +function show_action() { + git_check_cleancheckout || ewarn "$git_cleancheckout_DIRTY" + [ -n "$REF_UNIQUE" ] || resolve_unique_branch "$@" + ensure_merge_branches + _show_action "$@" +} diff --git a/bash/src/pman74.conf.sh b/bash/src/pman74.conf.sh index f179165..ab4453f 100644 --- a/bash/src/pman74.conf.sh +++ b/bash/src/pman74.conf.sh @@ -1,14 +1,10 @@ # -*- 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= DEVELOP=dev74 FEATURE=wip74/ RELEASE=rel74- -MAIN=dist74 +MAIN=main74 TAG_PREFIX= TAG_SUFFIX=p74 HOTFIX=hotf74- diff --git a/bash/src/pman82.conf.sh b/bash/src/pman82.conf.sh index 85262bc..b6393be 100644 --- a/bash/src/pman82.conf.sh +++ b/bash/src/pman82.conf.sh @@ -1,14 +1,10 @@ # -*- 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 DEVELOP=dev82 FEATURE=wip82/ RELEASE=rel82- -MAIN=dist82 +MAIN=main82 TAG_PREFIX= TAG_SUFFIX=p82 HOTFIX=hotf82- diff --git a/bash/src/pman84.conf.sh b/bash/src/pman84.conf.sh new file mode 100644 index 0000000..63590c4 --- /dev/null +++ b/bash/src/pman84.conf.sh @@ -0,0 +1,12 @@ +# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8 + +UPSTREAM=dev74 +DEVELOP=dev84 +FEATURE=wip84/ +RELEASE=rel84- +MAIN=main84 +TAG_PREFIX= +TAG_SUFFIX=p84 +HOTFIX=hotf84- +DIST= +NOAUTO= diff --git a/bash/src/template.sh b/bash/src/template.sh index c3d3376..f97506c 100644 --- a/bash/src/template.sh +++ b/bash/src/template.sh @@ -22,7 +22,12 @@ et \$2 vaudra alors 'file' si un fichier \${2#.}.local existe (e.g 'file.ext.local'), prendre ce fichier à la place comme source -Ajouter file au tableau userfiles" +Ajouter file au tableau userfiles + +retourner: +- 0 en cas de copie avec succès +- 2 si la source n'existe pas +- 3 si une erreur I/O s'est produite lors de la copie" function template_copy_replace() { local src="$1" dest="$2" local srcdir srcname lsrcname @@ -37,8 +42,28 @@ function template_copy_replace() { lsrcname="${srcname#.}.local" [ -e "$srcdir/$lsrcname" ] && src="$srcdir/$lsrcname" + [ -e "$src" ] || return 2 + userfiles+=("$dest") - cp -P "$src" "$dest" + local have_backup + if [ -e "$dest" ]; then + # copie de sauvegarde avant + if ! cp -P --preserve=all "$dest" "$dest.bck.$$"; then + rm "$dest.bck.$$" + return 3 + fi + have_backup=1 + fi + if ! cp -P "$src" "$dest"; then + rm "$dest" + if [ -n "$have_backup" ]; then + # restaurer la sauvegarde en cas d'erreur + cp -P --preserve=all "$dest.bck.$$" "$dest" && + rm "$dest.bck.$$" + fi + return 3 + fi + [ -n "$have_backup" ] && rm "$dest.bck.$$" return 0 } @@ -51,7 +76,13 @@ et \$2 vaudra alors 'file' si un fichier \${1#.}.local existe (e.g 'file.ext.local'), prendre ce fichier à la place comme source -Ajouter file au tableau userfiles" +Ajouter file au tableau userfiles + +retourner: +- 0 en cas de copie avec succès +- 1 si le fichier existait déjà +- 2 si la source n'existe pas +- 3 si une erreur I/O s'est produite lors de la copie" function template_copy_missing() { local src="$1" dest="$2" local srcdir srcname lsrcname @@ -63,15 +94,33 @@ function template_copy_missing() { dest="$srcdir/$dest" fi - userfiles+=("$dest") - if [ ! -e "$dest" ]; then - lsrcname="${srcname#.}.local" - [ -e "$srcdir/$lsrcname" ] && src="$srcdir/$lsrcname" + lsrcname="${srcname#.}.local" + [ -e "$srcdir/$lsrcname" ] && src="$srcdir/$lsrcname" - cp -P "$src" "$dest" - return 0 + [ -e "$src" ] || return 2 + + userfiles+=("$dest") + [ -e "$dest" ] && return 1 + + if ! cp -P "$src" "$dest"; then + # ne pas garder le fichier en cas d'erreur de copie + rm "$dest" + return 3 fi - return 1 + return 0 +} + +function: template_ioerror "\ +tester si une erreur de copie s'est produite lors de l'appel à +template_copy_missing() ou template_copy_replace(), par exemple en cas de +dépassement de capacité du disque ou si le fichier source n'existe pas + +il faut appeler cette fonction avec la valeur de retour de ces fonctions, e.g + template_copy_missing file + template_ioerror $? && die" +function template_ioerror() { + local r="${1:-$?}" + [ $r -ge 2 ] } function: template_dump_vars "\ @@ -219,8 +268,13 @@ function _template_can_process() { esac } +function: template_process_userfiles "\ +retourner: +- 0 en cas de traitement avec succès des fichiers +- 3 si une erreur I/O s'est produite lors du traitement d'un des fichiers" function template_process_userfiles() { local awkscript sedscript workfile userfile + local have_backup ac_set_tmpfile awkscript ac_set_tmpfile sedscript template_generate_scripts "$awkscript" "$sedscript" "$@" @@ -231,10 +285,28 @@ function template_process_userfiles() { if cat "$userfile" | awk -f "$awkscript" | sed -rf "$sedscript" >"$workfile"; then if testdiff "$workfile" "$userfile"; then # n'écrire le fichier que s'il a changé - cat "$workfile" >"$userfile" + if [ -e "$userfile" ]; then + # copie de sauvegarde avant + if ! cp -P --preserve=all "$userfile" "$userfile.bck.$$"; then + rm "$userfile.bck.$$" + return 3 + fi + have_backup=1 + fi + if ! cat "$workfile" >"$userfile"; then + rm "$userfile" + if [ -n "$have_backup" ]; then + # restaurer la sauvegarde en cas d'erreur + cp -P --preserve=all "$userfile.bck.$$" "$userfile" && + rm "$userfile.bck.$$" + fi + return 3 + fi + [ -n "$have_backup" ] && rm "$userfile.bck.$$" fi fi done ac_clean "$awkscript" "$sedscript" "$workfile" + return 0 } diff --git a/bin/.cachectl.php b/bin/.cachectl.php new file mode 120000 index 0000000..c9604f8 --- /dev/null +++ b/bin/.cachectl.php @@ -0,0 +1 @@ +../php/bin/cachectl.php \ No newline at end of file diff --git a/bin/.dumpser.php b/bin/.dumpser.php new file mode 120000 index 0000000..46cfbdc --- /dev/null +++ b/bin/.dumpser.php @@ -0,0 +1 @@ +../php/bin/dumpser.php \ No newline at end of file diff --git a/bin/.json2yml.php b/bin/.json2yml.php new file mode 120000 index 0000000..ff6141b --- /dev/null +++ b/bin/.json2yml.php @@ -0,0 +1 @@ +../php/bin/json2yml.php \ No newline at end of file diff --git a/bin/.mysql.capacitor.php b/bin/.mysql.capacitor.php new file mode 120000 index 0000000..ab7aba5 --- /dev/null +++ b/bin/.mysql.capacitor.php @@ -0,0 +1 @@ +../php/bin/mysql.capacitor.php \ No newline at end of file diff --git a/bin/._pman-composer_local_deps.php b/bin/.pcomp-local_deps.php similarity index 66% rename from bin/._pman-composer_local_deps.php rename to bin/.pcomp-local_deps.php index 92aeda8..c99d615 100755 --- a/bin/._pman-composer_local_deps.php +++ b/bin/.pcomp-local_deps.php @@ -2,9 +2,7 @@ .gitignore "\ +.~lock*# +.*.swp" + git add .gitignore + fi + return 0 +} + +function init_repo_action() { + local -a push_branches; local config + + [ ${#LocalBranches[*]} -eq 0 ] || die "Ce dépôt a déjà été initialisé" + + _init_config || exit_with ewarn "Initialisation du dépôt annulée" + + einfo "Création de la branche $MAIN" + git symbolic-ref HEAD "refs/heads/$MAIN" + git commit -m "commit initial" + push_branches+=("$MAIN") + + einfo "Création de la branche $DEVELOP" + git checkout -b "$DEVELOP" + push_branches+=("$DEVELOP") + + _push_branches +} + +function init_config_action() { + local -a push_branches; local config + + [ -f .pman.conf -a -z "$ForceCreate" ] && die "La configuration pman a déjà été initialisée" + + resolve_should_push + + _init_config || exit_with ewarn "Initialisation de la configuration annulée" + git commit -m "configuration pman" + push_branches+=("$CurrentBranch") + + _push_branches +} + +function _init_composer() { + if [ ! -f .composer.pman.yml -o -n "$ForceCreate" ]; then + ac_set_tmpfile config + cat >"$config" < $DestBranch - -$commits -" - _sd_COLOR=always _show_diff - } | less -eRF - else - einfo "Commits à fusionner $SrcBranch --> $DestBranch" - eecho "$commits" - fi - fi -} - -function ensure_branches() { - [ -n "$SrcBranch" -a -n "$DestBranch" ] || - die "$SrcBranch: Aucune configuration de fusion trouvée pour cette branche" - - array_contains LocalBranches "$SrcBranch" || die "$SrcBranch: branche source introuvable" - array_contains LocalBranches "$DestBranch" || die "$DestBranch: branche destination introuvable" - +function ensure_rel_infos() { Tag="$TAG_PREFIX$Version$TAG_SUFFIX" local -a tags setx -a tags=git tag -l "${TAG_PREFIX}*${TAG_SUFFIX}" @@ -71,14 +45,14 @@ L'option --no-push a été forcée puisque ce dépôt n'a pas d'origine" if [ -n "$Merge" ]; then enote "\ Ce script va: -- créer la branche de release ${COULEUR_VERTE}$ReleaseBranch${COULEUR_NORMALE} <-- ${COULEUR_BLEUE}$SrcBranch${COULEUR_NORMALE} +- créer la branche de release ${COULEUR_VERTE}$ReleaseBranch${COULEUR_NORMALE} <-- ${COULEUR_BLEUE}$MergeSrc${COULEUR_NORMALE} - la provisionner avec une description des changements -- la fusionner dans la branche destination ${COULEUR_ROUGE}$DestBranch${COULEUR_NORMALE}${Push:+ +- la fusionner dans la branche destination ${COULEUR_ROUGE}$MergeDest${COULEUR_NORMALE}${Push:+ - pousser les branches modifiées}" else enote "\ Ce script va: -- créer la branche de release ${COULEUR_VERTE}$ReleaseBranch${COULEUR_NORMALE} <-- ${COULEUR_BLEUE}$SrcBranch${COULEUR_NORMALE} +- créer la branche de release ${COULEUR_VERTE}$ReleaseBranch${COULEUR_NORMALE} <-- ${COULEUR_BLEUE}$MergeSrc${COULEUR_NORMALE} - la provisionner avec une description des changements Vous devrez: - mettre à jour les informations de release puis relancer ce script" @@ -123,8 +97,8 @@ EOF $BEFORE_MERGE_RELEASE )$or_die EOF - _rscript_merge_release_branch "$DestBranch" "$Tag" - _rscript_merge_release_branch "$SrcBranch" + _rscript_merge_release_branch "$MergeDest" "$Tag" + _rscript_merge_release_branch "$MergeSrc" _rscript_delete_release_branch [ -n "$AFTER_MERGE_RELEASE" ] && _scripta < DEST. DEST est calculé en fonction de REF + -t, --merge-to REF + spécifier la branche de référence et indiquer que la fusion se fait dans + le sens SRC --> REF. SRC est calculé en fonction de REF" + fi + + ref="$1"; shift + merge_dir=to + [ -n "$ref" ] || die "vous spécifier la branche de référence" + + case "$ref" in + -f|--merge-from) + ref="$1"; shift + merge_dir=from + ;; + -f*) + ref="${ref#-f}" + merge_dir=from + ;; + -t|--merge-to) + ref="$1"; shift + merge_dir=to + ;; + -t*) + ref="${ref#-t}" + merge_dir=to + ;; + esac + REF_BRANCH="${ref^^}" + array_contains PMAN_BRANCHES "$REF_BRANCH" || die "$ref: invalid branch" + +else + REF_BRANCH="PMAN_TOOL_${MYNAME^^}"; REF_BRANCH="${!REF_BRANCH}" +fi + +if check_gitdir; then + load_branches all + load_config + set_pman_vars "$merge_dir" + load_branches current + loaded_config=1 +else + set_pman_vars "$merge_dir" +fi + +RefDesc= +MergeSrcDesc= +MergeDestDesc= +if [ -n "$REF_BRANCH" ]; then + RefDesc="${COULEUR_BLANCHE}<$REF_BRANCH>" + [ -n "$RefBranch" -a -n "$REF_UNIQUE" ] && RefDesc="$RefDesc ($RefBranch)" + RefDesc="$RefDesc${COULEUR_NORMALE}" +fi +if [ -n "$MERGE_SRC" ]; then + MergeSrcDesc="${COULEUR_BLEUE}<$MERGE_SRC>" + [ -n "$MergeSrc" -a -n "$REF_UNIQUE" ] && MergeSrcDesc="$MergeSrcDesc ($MergeSrc)" + MergeSrcDesc="$MergeSrcDesc${COULEUR_NORMALE}" +fi +if [ -n "$MERGE_DEST" ]; then + MergeDestDesc="${COULEUR_ROUGE}<$MERGE_DEST>" + [ -n "$MergeDest" -a -n "$REF_UNIQUE" ] && MergeDestDesc="$MergeDestDesc ($MergeDest)" + MergeDestDesc="$MergeDestDesc${COULEUR_NORMALE}" +fi + +if [ -n "$REF_UNIQUE" ] +then purpose="gérer la branche $RefDesc" +else purpose="gérer les branches $RefDesc" +fi +usage="--checkout" +variables= + +chdir_def=(chdir= "répertoire dans lequel se placer avant de lancer les opérations") +origin_def=(Origin= "++origine à partir de laquelle les branches distantes sont considérées") +config_branch_def=(ConfigBranch= "++branche à partir de laquelle charger la configuration") +config_file_def=(ConfigFile= "++\ +fichier de configuration des branches. le fichier .pman.conf dans le répertoire +du dépôt est utilisé par défaut s'il existe. cette option est prioritaire sur +--config-branch") +fake_def=(_Fake=1 "++option non documentée") +keep_script_def=(_KeepScript=1 "++option non documentée") +dump_action_def=(action=dump "++afficher les noms des branches") +checkout_action_def=('$:' "++non applicable") +show_action_def=('$:' "++non applicable") +rebase_action_def=('$:' "++non applicable") +merge_action_def=('$:' "++non applicable") +tech_merge_def=('$:' "++non applicable") +squash_def=('$:' "++non applicable") +force_merge_def=('$:' "++non applicable") +no_push_def=('$:' "++non applicable") +push_def=('$:' "++non applicable") +no_delete_def=('$:' "++non applicable") +delete_def=('$:' "++non applicable") +after_merge_def=('$:' "++non applicable") + +if [ -n "$RefBranch" -a -n "$REF_UNIQUE" ]; then + checkout_action_def=(action=checkout "++\ +créer le cas échéant la branche $RefDesc et basculer vers elle. +c'est l'option par défaut") +elif [ -z "$REF_UNIQUE" ]; then + checkout_action_def=(action=checkout "\ +créer le cas échéant la branche $RefDesc et basculer vers elle. +c'est l'option par défaut") +else + checkout_action_def=(action=checkout "\ +créer la branche $MergeDestDesc et basculer vers elle. +c'est l'option par défaut") +fi + +if [ -n "$MERGE_SRC" -a -n "$MERGE_DEST" ]; then + if [ -n "$REF_UNIQUE" ] + then usage="${usage}|--show|--merge" + else usage="${usage} $REF_BRANCH +--show|--merge" + fi + if [ "$REF_BRANCH" != "$MERGE_SRC" ] + then bewareDir=" +NB: la fusion se fait dans le sens inverse" + else bewareDir= + fi + variables="Les variables supplémentaires suivantes peuvent être définies: + BEFORE_MERGE_${MERGE_SRC} + AFTER_MERGE_${MERGE_SRC}" + + show_action_def=('$action=show; inc@ ShowLevel' "\ +lister ce qui serait fusionné dans la branche $MergeDestDesc") + rebase_action_def=('$:' "++non implémenté") +# rebase_action_def=(action=rebase "\ +#lancer git rebase -i sur la branche $MergeSrcDesc. cela permet de réordonner +#les commits pour nettoyer l'historique avant la fusion") + merge_action_def=(action=merge "\ +fusionner la branche $MergeSrcDesc dans la branche $MergeDestDesc$bewareDir") + tech_merge_def=('$action=merge; TechMerge=1' "++option non documentée") + squash_def=('$action=merge; res@ SquashMsg' "fusionner les modifications de la branche comme un seul commit") + [ -n "$PREL_MERGE" ] && force_merge_def=(ForceMerge=1 "++\ +forcer la fusion pour une branche qui devrait être traitée par prel") + no_push_def=(Push= "ne pas pousser les branches vers leur origine après la fusion") + push_def=(Push=1 "++\ +pousser les branches vers leur origine après la fusion. +c'est l'option par défaut") + + if [ -n "$DELETE_MERGED" ]; then + variables="${variables} + AFTER_DELETE_${MERGE_SRC}" + no_delete_def=(Delete= "\ +ne pas supprimer la branche $MergeSrcDesc après la fusion dans la branche +$MergeDestDesc. cette option ne devrait pas être utilisée avec --squash") + delete_def=(Delete=1 "++\ +supprimer la branche $MergeSrcDesc après la fusion dans la branche +$MergeDestDesc. +c'est l'option par défaut") + fi + + [ -n "$MERGE_DEST" ] && variables="${variables} + BEFORE_PUSH_${MERGE_DEST} + AFTER_PUSH_${MERGE_DEST}" + + after_merge_def=(AfterMerge= "évaluer le script spécifié après une fusion *réussie*") +fi + +args=( + "$purpose" + "\ + $usage + +CONFIGURATION + +Le fichier .pman.conf contient la configuration des branches. +$variables" + -d:,--chdir:BASEDIR "${chdir_def[@]}" + -O:,--origin "${origin_def[@]}" + -B:,--config-branch "${config_branch_def[@]}" + -c:,--config-file:CONFIG "${config_file_def[@]}" + --fake "${fake_def[@]}" + --keep-script "${keep_script_def[@]}" + --dump "${dump_action_def[@]}" + --checkout "${checkout_action_def[@]}" + -w,--show "${show_action_def[@]}" + -b,--rebase "${rebase_action_def[@]}" + -m,--merge "${merge_action_def[@]}" + --tech-merge "${tech_merge_def[@]}" + -s:,--squash:COMMIT_MSG "${squash_def[@]}" + -f,--force-merge "${force_merge_def[@]}" + -n,--no-push "${no_push_def[@]}" + --push "${push_def[@]}" + -k,--no-delete "${no_delete_def[@]}" + --delete "${delete_def[@]}" + -a:,--after-merge "${after_merge_def[@]}" +) +parse_args "$@"; set -- "${args[@]}" + +if [ -z "$loaded_config" -o -n "$chdir" -o -n "$ConfigFile" -o -n "$ConfigBranch" ]; then + # charger la configuration + ensure_gitdir "$chdir" + load_branches all + load_config + set_pman_vars "$merge_dir" + load_branches current +fi +resolve_should_push quiet + +"${action}_action" "$@" diff --git a/bin/pwip b/bin/pwip deleted file mode 100755 index 787676b..0000000 --- a/bin/pwip +++ /dev/null @@ -1,60 +0,0 @@ -#!/bin/bash -# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8 -source "$(dirname -- "$0")/../load.sh" || exit 1 -require: git pman pman.conf - -git_cleancheckout_DIRTY="\ -Vous avez des modifications locales. -Enregistrez ces modifications avant de créer une nouvelle branche" - -chdir= -Origin= -ConfigBranch= -ConfigFile= -[ -z "$PMAN_NO_PUSH" ] && Push=1 || Push= -args=( - "créer une branche de feature" - "" - -d:,--chdir:BASEDIR chdir= "répertoire dans lequel se placer avant de lancer les opérations" - -O:,--origin Origin= "++\ -origine à partir de laquelle les branches distantes sont considérées" - -B:,--config-branch ConfigBranch= "++\ -branche à partir de laquelle charger la configuration" - -c:,--config-file:CONFIG ConfigFile= "++\ -fichier de configuration des branches. cette option est prioritaire sur --config-branch -par défaut, utiliser le fichier .pman.conf dans le répertoire du dépôt s'il existe" - -n,--no-push Push= "\ -ne pas pousser les branches vers leur origine après la fusion" - --push Push=1 "++\ -pousser les branches vers leur origine après la fusion. -c'est l'option par défaut" -) -parse_args "$@"; set -- "${args[@]}" - -# charger la configuration -ensure_gitdir "$chdir" -load_branches all -load_config "$MYNAME" -load_branches current - -branch="$1" -if [ -z "$branch" -a ${#FeatureBranches[*]} -eq 1 ]; then - branch="${FeatureBranches[0]}" -fi -[ -n "$branch" ] || die "Vous devez spécifier la branche à créer" -branch="$FEATURE${branch#$FEATURE}" - -resolve_should_push -git_ensure_cleancheckout - -if array_contains AllBranches "$branch"; then - git checkout -q "$branch" -else - # si la branche source n'existe pas, la créer - args=(--origin "$Origin") - if [ -n "$ConfigFile" ]; then args+=(--config-file "$ConfigFile") - elif [ -n "$ConfigBranch" ]; then args+=(--config-branch "$ConfigBranch") - fi - [ -z "$Push" ] && args+=(--no-push) - exec "$MYDIR/pman" "${args[@]}" "$branch" -fi diff --git a/bin/pwip b/bin/pwip new file mode 120000 index 0000000..22100e6 --- /dev/null +++ b/bin/pwip @@ -0,0 +1 @@ +ptool \ No newline at end of file diff --git a/bin/runphp b/bin/runphp index 3e59b61..f15db8a 100755 --- a/bin/runphp +++ b/bin/runphp @@ -19,6 +19,7 @@ while true; do fi cd .. done +cd "$owd" export RUNPHP_MOUNT= if [ "$MYNAME" == composer ]; then diff --git a/bin/sqlite.capacitor.php b/bin/sqlite.capacitor.php new file mode 120000 index 0000000..42fbf67 --- /dev/null +++ b/bin/sqlite.capacitor.php @@ -0,0 +1 @@ +runphp \ No newline at end of file diff --git a/bin/yml2json.php b/bin/yml2json.php new file mode 120000 index 0000000..42fbf67 --- /dev/null +++ b/bin/yml2json.php @@ -0,0 +1 @@ +runphp \ No newline at end of file diff --git a/composer.json b/composer.json index b0d3eba..f9a109a 100644 --- a/composer.json +++ b/composer.json @@ -18,7 +18,10 @@ "nulib/php": "*" }, "require": { - "symfony/yaml": "^7.1", + "symfony/yaml": "^7.3", + "symfony/expression-language": "^7.3", + "phpmailer/phpmailer": "^6.8", + "league/commonmark": "^2.7", "ext-json": "*", "php": "^8.2" }, @@ -35,7 +38,8 @@ }, "autoload": { "psr-4": { - "nulib\\": "php/src" + "nulib\\": "php/src", + "cli\\": "php/cli" } }, "autoload-dev": { @@ -43,6 +47,15 @@ "nulib\\": "php/tests" } }, + "bin": [ + "php/bin/cachectl.php", + "php/bin/dumpser.php", + "php/bin/json2yml.php", + "php/bin/yml2json.php", + "php/bin/sqlite.capacitor.php", + "php/bin/mysql.capacitor.php", + "php/bin/pgsql.capacitor.php" + ], "config": { "vendor-dir": "php/vendor" }, diff --git a/composer.lock b/composer.lock index eb7f2b1..330ab8b 100644 --- a/composer.lock +++ b/composer.lock @@ -4,8 +4,875 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "71744d15224f445d1aeefe16ec7d1099", + "content-hash": "424dc194faea590269d136c8ffaf2505", "packages": [ + { + "name": "dflydev/dot-access-data", + "version": "v3.0.3", + "source": { + "type": "git", + "url": "https://github.com/dflydev/dflydev-dot-access-data.git", + "reference": "a23a2bf4f31d3518f3ecb38660c95715dfead60f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/dflydev/dflydev-dot-access-data/zipball/a23a2bf4f31d3518f3ecb38660c95715dfead60f", + "reference": "a23a2bf4f31d3518f3ecb38660c95715dfead60f", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^0.12.42", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.3", + "scrutinizer/ocular": "1.6.0", + "squizlabs/php_codesniffer": "^3.5", + "vimeo/psalm": "^4.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Dflydev\\DotAccessData\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Dragonfly Development Inc.", + "email": "info@dflydev.com", + "homepage": "http://dflydev.com" + }, + { + "name": "Beau Simensen", + "email": "beau@dflydev.com", + "homepage": "http://beausimensen.com" + }, + { + "name": "Carlos Frutos", + "email": "carlos@kiwing.it", + "homepage": "https://github.com/cfrutos" + }, + { + "name": "Colin O'Dell", + "email": "colinodell@gmail.com", + "homepage": "https://www.colinodell.com" + } + ], + "description": "Given a deep data structure, access data by dot notation.", + "homepage": "https://github.com/dflydev/dflydev-dot-access-data", + "keywords": [ + "access", + "data", + "dot", + "notation" + ], + "support": { + "issues": "https://github.com/dflydev/dflydev-dot-access-data/issues", + "source": "https://github.com/dflydev/dflydev-dot-access-data/tree/v3.0.3" + }, + "time": "2024-07-08T12:26:09+00:00" + }, + { + "name": "league/commonmark", + "version": "2.7.1", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/commonmark.git", + "reference": "10732241927d3971d28e7ea7b5712721fa2296ca" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/10732241927d3971d28e7ea7b5712721fa2296ca", + "reference": "10732241927d3971d28e7ea7b5712721fa2296ca", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "league/config": "^1.1.1", + "php": "^7.4 || ^8.0", + "psr/event-dispatcher": "^1.0", + "symfony/deprecation-contracts": "^2.1 || ^3.0", + "symfony/polyfill-php80": "^1.16" + }, + "require-dev": { + "cebe/markdown": "^1.0", + "commonmark/cmark": "0.31.1", + "commonmark/commonmark.js": "0.31.1", + "composer/package-versions-deprecated": "^1.8", + "embed/embed": "^4.4", + "erusev/parsedown": "^1.0", + "ext-json": "*", + "github/gfm": "0.29.0", + "michelf/php-markdown": "^1.4 || ^2.0", + "nyholm/psr7": "^1.5", + "phpstan/phpstan": "^1.8.2", + "phpunit/phpunit": "^9.5.21 || ^10.5.9 || ^11.0.0", + "scrutinizer/ocular": "^1.8.1", + "symfony/finder": "^5.3 | ^6.0 | ^7.0", + "symfony/process": "^5.4 | ^6.0 | ^7.0", + "symfony/yaml": "^2.3 | ^3.0 | ^4.0 | ^5.0 | ^6.0 | ^7.0", + "unleashedtech/php-coding-standard": "^3.1.1", + "vimeo/psalm": "^4.24.0 || ^5.0.0 || ^6.0.0" + }, + "suggest": { + "symfony/yaml": "v2.3+ required if using the Front Matter extension" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.8-dev" + } + }, + "autoload": { + "psr-4": { + "League\\CommonMark\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Colin O'Dell", + "email": "colinodell@gmail.com", + "homepage": "https://www.colinodell.com", + "role": "Lead Developer" + } + ], + "description": "Highly-extensible PHP Markdown parser which fully supports the CommonMark spec and GitHub-Flavored Markdown (GFM)", + "homepage": "https://commonmark.thephpleague.com", + "keywords": [ + "commonmark", + "flavored", + "gfm", + "github", + "github-flavored", + "markdown", + "md", + "parser" + ], + "support": { + "docs": "https://commonmark.thephpleague.com/", + "forum": "https://github.com/thephpleague/commonmark/discussions", + "issues": "https://github.com/thephpleague/commonmark/issues", + "rss": "https://github.com/thephpleague/commonmark/releases.atom", + "source": "https://github.com/thephpleague/commonmark" + }, + "funding": [ + { + "url": "https://www.colinodell.com/sponsor", + "type": "custom" + }, + { + "url": "https://www.paypal.me/colinpodell/10.00", + "type": "custom" + }, + { + "url": "https://github.com/colinodell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/league/commonmark", + "type": "tidelift" + } + ], + "time": "2025-07-20T12:47:49+00:00" + }, + { + "name": "league/config", + "version": "v1.2.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/config.git", + "reference": "754b3604fb2984c71f4af4a9cbe7b57f346ec1f3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/config/zipball/754b3604fb2984c71f4af4a9cbe7b57f346ec1f3", + "reference": "754b3604fb2984c71f4af4a9cbe7b57f346ec1f3", + "shasum": "" + }, + "require": { + "dflydev/dot-access-data": "^3.0.1", + "nette/schema": "^1.2", + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.8.2", + "phpunit/phpunit": "^9.5.5", + "scrutinizer/ocular": "^1.8.1", + "unleashedtech/php-coding-standard": "^3.1", + "vimeo/psalm": "^4.7.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.2-dev" + } + }, + "autoload": { + "psr-4": { + "League\\Config\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Colin O'Dell", + "email": "colinodell@gmail.com", + "homepage": "https://www.colinodell.com", + "role": "Lead Developer" + } + ], + "description": "Define configuration arrays with strict schemas and access values with dot notation", + "homepage": "https://config.thephpleague.com", + "keywords": [ + "array", + "config", + "configuration", + "dot", + "dot-access", + "nested", + "schema" + ], + "support": { + "docs": "https://config.thephpleague.com/", + "issues": "https://github.com/thephpleague/config/issues", + "rss": "https://github.com/thephpleague/config/releases.atom", + "source": "https://github.com/thephpleague/config" + }, + "funding": [ + { + "url": "https://www.colinodell.com/sponsor", + "type": "custom" + }, + { + "url": "https://www.paypal.me/colinpodell/10.00", + "type": "custom" + }, + { + "url": "https://github.com/colinodell", + "type": "github" + } + ], + "time": "2022-12-11T20:36:23+00:00" + }, + { + "name": "nette/schema", + "version": "v1.2.5", + "source": { + "type": "git", + "url": "https://github.com/nette/schema.git", + "reference": "0462f0166e823aad657c9224d0f849ecac1ba10a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nette/schema/zipball/0462f0166e823aad657c9224d0f849ecac1ba10a", + "reference": "0462f0166e823aad657c9224d0f849ecac1ba10a", + "shasum": "" + }, + "require": { + "nette/utils": "^2.5.7 || ^3.1.5 || ^4.0", + "php": "7.1 - 8.3" + }, + "require-dev": { + "nette/tester": "^2.3 || ^2.4", + "phpstan/phpstan-nette": "^1.0", + "tracy/tracy": "^2.7" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.2-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause", + "GPL-2.0-only", + "GPL-3.0-only" + ], + "authors": [ + { + "name": "David Grudl", + "homepage": "https://davidgrudl.com" + }, + { + "name": "Nette Community", + "homepage": "https://nette.org/contributors" + } + ], + "description": "📐 Nette Schema: validating data structures against a given Schema.", + "homepage": "https://nette.org", + "keywords": [ + "config", + "nette" + ], + "support": { + "issues": "https://github.com/nette/schema/issues", + "source": "https://github.com/nette/schema/tree/v1.2.5" + }, + "time": "2023-10-05T20:37:59+00:00" + }, + { + "name": "nette/utils", + "version": "v3.2.10", + "source": { + "type": "git", + "url": "https://github.com/nette/utils.git", + "reference": "a4175c62652f2300c8017fb7e640f9ccb11648d2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nette/utils/zipball/a4175c62652f2300c8017fb7e640f9ccb11648d2", + "reference": "a4175c62652f2300c8017fb7e640f9ccb11648d2", + "shasum": "" + }, + "require": { + "php": ">=7.2 <8.4" + }, + "conflict": { + "nette/di": "<3.0.6" + }, + "require-dev": { + "jetbrains/phpstorm-attributes": "dev-master", + "nette/tester": "~2.0", + "phpstan/phpstan": "^1.0", + "tracy/tracy": "^2.3" + }, + "suggest": { + "ext-gd": "to use Image", + "ext-iconv": "to use Strings::webalize(), toAscii(), chr() and reverse()", + "ext-intl": "to use Strings::webalize(), toAscii(), normalize() and compare()", + "ext-json": "to use Nette\\Utils\\Json", + "ext-mbstring": "to use Strings::lower() etc...", + "ext-tokenizer": "to use Nette\\Utils\\Reflection::getUseStatements()", + "ext-xml": "to use Strings::length() etc. when mbstring is not available" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.2-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause", + "GPL-2.0-only", + "GPL-3.0-only" + ], + "authors": [ + { + "name": "David Grudl", + "homepage": "https://davidgrudl.com" + }, + { + "name": "Nette Community", + "homepage": "https://nette.org/contributors" + } + ], + "description": "🛠 Nette Utils: lightweight utilities for string & array manipulation, image handling, safe JSON encoding/decoding, validation, slug or strong password generating etc.", + "homepage": "https://nette.org", + "keywords": [ + "array", + "core", + "datetime", + "images", + "json", + "nette", + "paginator", + "password", + "slugify", + "string", + "unicode", + "utf-8", + "utility", + "validation" + ], + "support": { + "issues": "https://github.com/nette/utils/issues", + "source": "https://github.com/nette/utils/tree/v3.2.10" + }, + "time": "2023-07-30T15:38:18+00:00" + }, + { + "name": "phpmailer/phpmailer", + "version": "v6.11.1", + "source": { + "type": "git", + "url": "https://github.com/PHPMailer/PHPMailer.git", + "reference": "d9e3b36b47f04b497a0164c5a20f92acb4593284" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPMailer/PHPMailer/zipball/d9e3b36b47f04b497a0164c5a20f92acb4593284", + "reference": "d9e3b36b47f04b497a0164c5a20f92acb4593284", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-filter": "*", + "ext-hash": "*", + "php": ">=5.5.0" + }, + "require-dev": { + "dealerdirect/phpcodesniffer-composer-installer": "^1.0", + "doctrine/annotations": "^1.2.6 || ^1.13.3", + "php-parallel-lint/php-console-highlighter": "^1.0.0", + "php-parallel-lint/php-parallel-lint": "^1.3.2", + "phpcompatibility/php-compatibility": "^9.3.5", + "roave/security-advisories": "dev-latest", + "squizlabs/php_codesniffer": "^3.7.2", + "yoast/phpunit-polyfills": "^1.0.4" + }, + "suggest": { + "decomplexity/SendOauth2": "Adapter for using XOAUTH2 authentication", + "ext-imap": "Needed to support advanced email address parsing according to RFC822", + "ext-mbstring": "Needed to send email in multibyte encoding charset or decode encoded addresses", + "ext-openssl": "Needed for secure SMTP sending and DKIM signing", + "greew/oauth2-azure-provider": "Needed for Microsoft Azure XOAUTH2 authentication", + "hayageek/oauth2-yahoo": "Needed for Yahoo XOAUTH2 authentication", + "league/oauth2-google": "Needed for Google XOAUTH2 authentication", + "psr/log": "For optional PSR-3 debug logging", + "symfony/polyfill-mbstring": "To support UTF-8 if the Mbstring PHP extension is not enabled (^1.2)", + "thenetworg/oauth2-azure": "Needed for Microsoft XOAUTH2 authentication" + }, + "type": "library", + "autoload": { + "psr-4": { + "PHPMailer\\PHPMailer\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-2.1-only" + ], + "authors": [ + { + "name": "Marcus Bointon", + "email": "phpmailer@synchromedia.co.uk" + }, + { + "name": "Jim Jagielski", + "email": "jimjag@gmail.com" + }, + { + "name": "Andy Prevost", + "email": "codeworxtech@users.sourceforge.net" + }, + { + "name": "Brent R. Matzelle" + } + ], + "description": "PHPMailer is a full-featured email creation and transfer class for PHP", + "support": { + "issues": "https://github.com/PHPMailer/PHPMailer/issues", + "source": "https://github.com/PHPMailer/PHPMailer/tree/v6.11.1" + }, + "funding": [ + { + "url": "https://github.com/Synchro", + "type": "github" + } + ], + "time": "2025-09-30T11:54:53+00:00" + }, + { + "name": "psr/cache", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/php-fig/cache.git", + "reference": "d11b50ad223250cf17b86e38383413f5a6764bf8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/cache/zipball/d11b50ad223250cf17b86e38383413f5a6764bf8", + "reference": "d11b50ad223250cf17b86e38383413f5a6764bf8", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Cache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interface for caching libraries", + "keywords": [ + "cache", + "psr", + "psr-6" + ], + "support": { + "source": "https://github.com/php-fig/cache/tree/master" + }, + "time": "2016-08-06T20:24:11+00:00" + }, + { + "name": "psr/container", + "version": "1.1.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/container.git", + "reference": "513e0666f7216c7459170d56df27dfcefe1689ea" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/container/zipball/513e0666f7216c7459170d56df27dfcefe1689ea", + "reference": "513e0666f7216c7459170d56df27dfcefe1689ea", + "shasum": "" + }, + "require": { + "php": ">=7.4.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Psr\\Container\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common Container Interface (PHP FIG PSR-11)", + "homepage": "https://github.com/php-fig/container", + "keywords": [ + "PSR-11", + "container", + "container-interface", + "container-interop", + "psr" + ], + "support": { + "issues": "https://github.com/php-fig/container/issues", + "source": "https://github.com/php-fig/container/tree/1.1.2" + }, + "time": "2021-11-05T16:50:12+00:00" + }, + { + "name": "psr/event-dispatcher", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/event-dispatcher.git", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/event-dispatcher/zipball/dbefd12671e8a14ec7f180cab83036ed26714bb0", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0", + "shasum": "" + }, + "require": { + "php": ">=7.2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\EventDispatcher\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Standard interfaces for event handling.", + "keywords": [ + "events", + "psr", + "psr-14" + ], + "support": { + "issues": "https://github.com/php-fig/event-dispatcher/issues", + "source": "https://github.com/php-fig/event-dispatcher/tree/1.0.0" + }, + "time": "2019-01-08T18:20:26+00:00" + }, + { + "name": "psr/log", + "version": "1.1.4", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "d49695b909c3b7628b6289db5479a1c204601f11" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/d49695b909c3b7628b6289db5479a1c204601f11", + "reference": "d49695b909c3b7628b6289db5479a1c204601f11", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Log\\": "Psr/Log/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", + "keywords": [ + "log", + "psr", + "psr-3" + ], + "support": { + "source": "https://github.com/php-fig/log/tree/1.1.4" + }, + "time": "2021-05-03T11:20:27+00:00" + }, + { + "name": "symfony/cache", + "version": "v5.4.46", + "source": { + "type": "git", + "url": "https://github.com/symfony/cache.git", + "reference": "0fe08ee32cec2748fbfea10c52d3ee02049e0f6b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/cache/zipball/0fe08ee32cec2748fbfea10c52d3ee02049e0f6b", + "reference": "0fe08ee32cec2748fbfea10c52d3ee02049e0f6b", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "psr/cache": "^1.0|^2.0", + "psr/log": "^1.1|^2|^3", + "symfony/cache-contracts": "^1.1.7|^2", + "symfony/deprecation-contracts": "^2.1|^3", + "symfony/polyfill-php73": "^1.9", + "symfony/polyfill-php80": "^1.16", + "symfony/service-contracts": "^1.1|^2|^3", + "symfony/var-exporter": "^4.4|^5.0|^6.0" + }, + "conflict": { + "doctrine/dbal": "<2.13.1", + "symfony/dependency-injection": "<4.4", + "symfony/http-kernel": "<4.4", + "symfony/var-dumper": "<4.4" + }, + "provide": { + "psr/cache-implementation": "1.0|2.0", + "psr/simple-cache-implementation": "1.0|2.0", + "symfony/cache-implementation": "1.0|2.0" + }, + "require-dev": { + "cache/integration-tests": "dev-master", + "doctrine/cache": "^1.6|^2.0", + "doctrine/dbal": "^2.13.1|^3|^4", + "predis/predis": "^1.1|^2.0", + "psr/simple-cache": "^1.0|^2.0", + "symfony/config": "^4.4|^5.0|^6.0", + "symfony/dependency-injection": "^4.4|^5.0|^6.0", + "symfony/filesystem": "^4.4|^5.0|^6.0", + "symfony/http-kernel": "^4.4|^5.0|^6.0", + "symfony/messenger": "^4.4|^5.0|^6.0", + "symfony/var-dumper": "^4.4|^5.0|^6.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Cache\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides extended PSR-6, PSR-16 (and tags) implementations", + "homepage": "https://symfony.com", + "keywords": [ + "caching", + "psr6" + ], + "support": { + "source": "https://github.com/symfony/cache/tree/v5.4.46" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-11-04T11:43:55+00:00" + }, + { + "name": "symfony/cache-contracts", + "version": "v2.5.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/cache-contracts.git", + "reference": "517c3a3619dadfa6952c4651767fcadffb4df65e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/cache-contracts/zipball/517c3a3619dadfa6952c4651767fcadffb4df65e", + "reference": "517c3a3619dadfa6952c4651767fcadffb4df65e", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "psr/cache": "^1.0|^2.0|^3.0" + }, + "suggest": { + "symfony/cache-implementation": "" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "2.5-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Cache\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to caching", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/cache-contracts/tree/v2.5.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:11:13+00:00" + }, { "name": "symfony/deprecation-contracts", "version": "v2.5.4", @@ -73,9 +940,72 @@ ], "time": "2024-09-25T14:11:13+00:00" }, + { + "name": "symfony/expression-language", + "version": "v5.4.45", + "source": { + "type": "git", + "url": "https://github.com/symfony/expression-language.git", + "reference": "a784b66edc4c151eb05076d04707906ee2c209a9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/expression-language/zipball/a784b66edc4c151eb05076d04707906ee2c209a9", + "reference": "a784b66edc4c151eb05076d04707906ee2c209a9", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "symfony/cache": "^4.4|^5.0|^6.0", + "symfony/service-contracts": "^1.1|^2|^3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\ExpressionLanguage\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an engine that can compile and evaluate expressions", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/expression-language/tree/v5.4.45" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-10-04T14:55:40+00:00" + }, { "name": "symfony/polyfill-ctype", - "version": "v1.32.0", + "version": "v1.33.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", @@ -134,7 +1064,258 @@ "portable" ], "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.32.0" + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/polyfill-php73", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php73.git", + "reference": "0f68c03565dcaaf25a890667542e8bd75fe7e5bb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/0f68c03565dcaaf25a890667542e8bd75fe7e5bb", + "reference": "0f68c03565dcaaf25a890667542e8bd75fe7e5bb", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php73\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 7.3+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php73/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/polyfill-php80", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php80.git", + "reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/0cc9dd0f17f61d8131e7df6b84bd344899fe2608", + "reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php80\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ion Bazan", + "email": "ion.bazan@gmail.com" + }, + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php80/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-01-02T08:10:11+00:00" + }, + { + "name": "symfony/service-contracts", + "version": "v2.5.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/service-contracts.git", + "reference": "f37b419f7aea2e9abf10abd261832cace12e3300" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/f37b419f7aea2e9abf10abd261832cace12e3300", + "reference": "f37b419f7aea2e9abf10abd261832cace12e3300", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "psr/container": "^1.1", + "symfony/deprecation-contracts": "^2.1|^3" + }, + "conflict": { + "ext-psr": "<1.1|>=2" + }, + "suggest": { + "symfony/service-implementation": "" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "2.5-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Service\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to writing services", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/service-contracts/tree/v2.5.4" }, "funding": [ { @@ -150,7 +1331,80 @@ "type": "tidelift" } ], - "time": "2024-09-09T11:45:10+00:00" + "time": "2024-09-25T14:11:13+00:00" + }, + { + "name": "symfony/var-exporter", + "version": "v5.4.45", + "source": { + "type": "git", + "url": "https://github.com/symfony/var-exporter.git", + "reference": "862700068db0ddfd8c5b850671e029a90246ec75" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/var-exporter/zipball/862700068db0ddfd8c5b850671e029a90246ec75", + "reference": "862700068db0ddfd8c5b850671e029a90246ec75", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "symfony/polyfill-php80": "^1.16" + }, + "require-dev": { + "symfony/var-dumper": "^4.4.9|^5.0.9|^6.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\VarExporter\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Allows exporting any serializable PHP data structure to plain PHP code", + "homepage": "https://symfony.com", + "keywords": [ + "clone", + "construct", + "export", + "hydrate", + "instantiate", + "serialize" + ], + "support": { + "source": "https://github.com/symfony/var-exporter/tree/v5.4.45" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:11:13+00:00" }, { "name": "symfony/yaml", @@ -301,16 +1555,16 @@ }, { "name": "myclabs/deep-copy", - "version": "1.13.3", + "version": "1.13.4", "source": { "type": "git", "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "faed855a7b5f4d4637717c2b3863e277116beb36" + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/faed855a7b5f4d4637717c2b3863e277116beb36", - "reference": "faed855a7b5f4d4637717c2b3863e277116beb36", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/07d290f0c47959fd5eed98c95ee5602db07e0b6a", + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a", "shasum": "" }, "require": { @@ -349,7 +1603,7 @@ ], "support": { "issues": "https://github.com/myclabs/DeepCopy/issues", - "source": "https://github.com/myclabs/DeepCopy/tree/1.13.3" + "source": "https://github.com/myclabs/DeepCopy/tree/1.13.4" }, "funding": [ { @@ -357,20 +1611,20 @@ "type": "tidelift" } ], - "time": "2025-07-05T12:25:42+00:00" + "time": "2025-08-01T08:46:24+00:00" }, { "name": "nikic/php-parser", - "version": "v5.5.0", + "version": "v5.6.1", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "ae59794362fe85e051a58ad36b289443f57be7a9" + "reference": "f103601b29efebd7ff4a1ca7b3eeea9e3336a2a2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/ae59794362fe85e051a58ad36b289443f57be7a9", - "reference": "ae59794362fe85e051a58ad36b289443f57be7a9", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/f103601b29efebd7ff4a1ca7b3eeea9e3336a2a2", + "reference": "f103601b29efebd7ff4a1ca7b3eeea9e3336a2a2", "shasum": "" }, "require": { @@ -389,7 +1643,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "5.0-dev" + "dev-master": "5.x-dev" } }, "autoload": { @@ -413,9 +1667,9 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v5.5.0" + "source": "https://github.com/nikic/PHP-Parser/tree/v5.6.1" }, - "time": "2025-05-31T08:24:38+00:00" + "time": "2025-08-13T20:13:15+00:00" }, { "name": "nulib/tests", @@ -894,16 +2148,16 @@ }, { "name": "phpunit/phpunit", - "version": "9.6.23", + "version": "9.6.29", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "43d2cb18d0675c38bd44982a5d1d88f6d53d8d95" + "reference": "9ecfec57835a5581bc888ea7e13b51eb55ab9dd3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/43d2cb18d0675c38bd44982a5d1d88f6d53d8d95", - "reference": "43d2cb18d0675c38bd44982a5d1d88f6d53d8d95", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/9ecfec57835a5581bc888ea7e13b51eb55ab9dd3", + "reference": "9ecfec57835a5581bc888ea7e13b51eb55ab9dd3", "shasum": "" }, "require": { @@ -914,7 +2168,7 @@ "ext-mbstring": "*", "ext-xml": "*", "ext-xmlwriter": "*", - "myclabs/deep-copy": "^1.13.1", + "myclabs/deep-copy": "^1.13.4", "phar-io/manifest": "^2.0.4", "phar-io/version": "^3.2.1", "php": ">=7.3", @@ -925,11 +2179,11 @@ "phpunit/php-timer": "^5.0.3", "sebastian/cli-parser": "^1.0.2", "sebastian/code-unit": "^1.0.8", - "sebastian/comparator": "^4.0.8", + "sebastian/comparator": "^4.0.9", "sebastian/diff": "^4.0.6", "sebastian/environment": "^5.1.5", - "sebastian/exporter": "^4.0.6", - "sebastian/global-state": "^5.0.7", + "sebastian/exporter": "^4.0.8", + "sebastian/global-state": "^5.0.8", "sebastian/object-enumerator": "^4.0.4", "sebastian/resource-operations": "^3.0.4", "sebastian/type": "^3.2.1", @@ -977,7 +2231,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.23" + "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.29" }, "funding": [ { @@ -1001,7 +2255,7 @@ "type": "tidelift" } ], - "time": "2025-05-02T06:40:34+00:00" + "time": "2025-09-24T06:29:11+00:00" }, { "name": "sebastian/cli-parser", @@ -1172,16 +2426,16 @@ }, { "name": "sebastian/comparator", - "version": "4.0.8", + "version": "4.0.9", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "fa0f136dd2334583309d32b62544682ee972b51a" + "reference": "67a2df3a62639eab2cc5906065e9805d4fd5dfc5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/fa0f136dd2334583309d32b62544682ee972b51a", - "reference": "fa0f136dd2334583309d32b62544682ee972b51a", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/67a2df3a62639eab2cc5906065e9805d4fd5dfc5", + "reference": "67a2df3a62639eab2cc5906065e9805d4fd5dfc5", "shasum": "" }, "require": { @@ -1234,15 +2488,27 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/comparator/issues", - "source": "https://github.com/sebastianbergmann/comparator/tree/4.0.8" + "source": "https://github.com/sebastianbergmann/comparator/tree/4.0.9" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/comparator", + "type": "tidelift" } ], - "time": "2022-09-14T12:41:17+00:00" + "time": "2025-08-10T06:51:50+00:00" }, { "name": "sebastian/complexity", @@ -1432,16 +2698,16 @@ }, { "name": "sebastian/exporter", - "version": "4.0.6", + "version": "4.0.8", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "78c00df8f170e02473b682df15bfcdacc3d32d72" + "reference": "14c6ba52f95a36c3d27c835d65efc7123c446e8c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/78c00df8f170e02473b682df15bfcdacc3d32d72", - "reference": "78c00df8f170e02473b682df15bfcdacc3d32d72", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/14c6ba52f95a36c3d27c835d65efc7123c446e8c", + "reference": "14c6ba52f95a36c3d27c835d65efc7123c446e8c", "shasum": "" }, "require": { @@ -1497,28 +2763,40 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/exporter/issues", - "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.6" + "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.8" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/exporter", + "type": "tidelift" } ], - "time": "2024-03-02T06:33:00+00:00" + "time": "2025-09-24T06:03:27+00:00" }, { "name": "sebastian/global-state", - "version": "5.0.7", + "version": "5.0.8", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/global-state.git", - "reference": "bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9" + "reference": "b6781316bdcd28260904e7cc18ec983d0d2ef4f6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9", - "reference": "bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/b6781316bdcd28260904e7cc18ec983d0d2ef4f6", + "reference": "b6781316bdcd28260904e7cc18ec983d0d2ef4f6", "shasum": "" }, "require": { @@ -1561,15 +2839,27 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/global-state/issues", - "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.7" + "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.8" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/global-state", + "type": "tidelift" } ], - "time": "2024-03-02T06:35:11+00:00" + "time": "2025-08-10T07:10:35+00:00" }, { "name": "sebastian/lines-of-code", @@ -1742,16 +3032,16 @@ }, { "name": "sebastian/recursion-context", - "version": "4.0.5", + "version": "4.0.6", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/recursion-context.git", - "reference": "e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1" + "reference": "539c6691e0623af6dc6f9c20384c120f963465a0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1", - "reference": "e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/539c6691e0623af6dc6f9c20384c120f963465a0", + "reference": "539c6691e0623af6dc6f9c20384c120f963465a0", "shasum": "" }, "require": { @@ -1793,15 +3083,27 @@ "homepage": "https://github.com/sebastianbergmann/recursion-context", "support": { "issues": "https://github.com/sebastianbergmann/recursion-context/issues", - "source": "https://github.com/sebastianbergmann/recursion-context/tree/4.0.5" + "source": "https://github.com/sebastianbergmann/recursion-context/tree/4.0.6" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/recursion-context", + "type": "tidelift" } ], - "time": "2023-02-03T06:07:39+00:00" + "time": "2025-08-10T06:57:39+00:00" }, { "name": "sebastian/resource-operations", diff --git a/php/bin/cachectl.php b/php/bin/cachectl.php new file mode 100755 index 0000000..48de2bc --- /dev/null +++ b/php/bin/cachectl.php @@ -0,0 +1,7 @@ +#!/usr/bin/php +|<|>|<=|>=|(?:is\s+)?null|(?:is\s+)?not\s+null)\s*(.*)$/', $arg, $ms); + } + + protected function storageCtl(CapacitorStorage $storage): void { + $args = $this->args; + + $channelClass = $this->channelClass; + $tableName = $this->tableName; + if ($channelClass === null && $tableName === null) { + $name = A::shift($args); + if ($name !== null) { + if (!$storage->channelExists($name, $row)) { + self::die("$name: nom de canal de données introuvable"); + } + if ($row["class_name"] !== "class@anonymous") $channelClass = $row["class_name"]; + else $tableName = $row["table_name"]; + } + } + if ($channelClass !== null) { + $channelClass = str_replace("/", "\\", $channelClass); + $channel = new $channelClass; + } elseif ($tableName !== null) { + $channel = new class($tableName) extends CapacitorChannel { + function __construct(?string $name=null) { + parent::__construct($name); + $this->tableName = $name; + } + }; + } else { + $found = false; + foreach ($storage->getChannels() as $row) { + msg::print($row["name"]); + $found = true; + } + if ($found) self::exit(); + self::die("Vous devez spécifier le canal de données"); + } + $capacitor = new Capacitor($storage, $channel); + + switch ($this->action) { + case self::ACTION_RESET: + $capacitor->reset($this->recreate); + break; + case self::ACTION_QUERY: + if (!$args) { + # lister les id + $out = new Stream(STDOUT); + $primaryKeys = $storage->getPrimaryKeys($channel); + $rows = $storage->db()->all([ + "select", + "cols" => $primaryKeys, + "from" => $channel->getTableName(), + ]); + $out->fputcsv($primaryKeys); + foreach ($rows as $row) { + $rowIds = $storage->getRowIds($channel, $row); + $out->fputcsv($rowIds); + } + } else { + # afficher les lignes correspondantes + if (count($args) == 1 && !self::isa_cond($args[0])) { + $filter = $args[0]; + } else { + $filter = []; + $ms = null; + foreach ($args as $arg) { + if (self::isa_cond($arg, $ms)) { + $filter[$ms[1]] = [$ms[2], $ms[3]]; + } else { + $filter[$arg] = ["not null"]; + } + } + } + $first = true; + $capacitor->each($filter, function ($row) use (&$first) { + if ($first) $first = false; + else echo "---\n"; + yaml::dump($row); + }); + } + break; + case self::ACTION_SQL: + echo $capacitor->getCreateSql()."\n"; + break; + } + } +} diff --git a/php/cli/BgLauncherApp.php b/php/cli/BgLauncherApp.php new file mode 100644 index 0000000..3f965fd --- /dev/null +++ b/php/cli/BgLauncherApp.php @@ -0,0 +1,122 @@ + "lancer un script en tâche de fond", + "usage" => "ApplicationClass args...", + + "sections" => [ + parent::VERBOSITY_SECTION, + ], + + ["-i", "--infos", "name" => "action", "value" => self::ACTION_INFOS, + "help" => "Afficher des informations sur la tâche", + ], + ["-s", "--start", "name" => "action", "value" => self::ACTION_START, + "help" => "Démarrer la tâche", + ], + ["-k", "--stop", "name" => "action", "value" => self::ACTION_STOP, + "help" => "Arrêter la tâche", + ], + ]; + + protected int $action = self::ACTION_START; + + static function show_infos(RunFile $runfile, ?int $level=null): void { + msg::print($runfile->getDesc(), $level); + msg::print(yaml::with(["data" => $runfile->read()]), ($level ?? 0) - 1); + } + + function main() { + $args = $this->args; + + $appClass = $args[0] ?? null; + if ($appClass === null) { + self::die("Vous devez spécifier la classe de l'application"); + } + $appClass = $args[0] = str_replace("/", "\\", $appClass); + if (!class_exists($appClass)) { + self::die("$appClass: classe non trouvée"); + } + + $useRunfile = constant("$appClass::USE_RUNFILE"); + if (!$useRunfile) { + self::die("Cette application ne supporte le lancement en tâche de fond"); + } + + $runfile = app::with($appClass)->getRunfile(); + switch ($this->action) { + case self::ACTION_START: + $argc = count($args); + $appClass::_manage_runfile($argc, $args, $runfile); + if ($runfile->warnIfLocked()) self::exit(app::EC_LOCKED); + array_splice($args, 0, 0, [ + PHP_BINARY, + path::abspath(NULIB_APP_app_launcher), + ]); + app::params_putenv(); + self::_start($args, $runfile); + break; + case self::ACTION_STOP: + self::_stop($runfile); + self::show_infos($runfile, -1); + break; + case self::ACTION_INFOS: + self::show_infos($runfile); + break; + } + } + + public static function _start(array $args, Runfile $runfile): void { + $pid = pcntl_fork(); + if ($pid == -1) { + # parent, impossible de forker + throw new ExitError(app::EC_FORK_PARENT, "Unable to fork"); + } elseif (!$pid) { + # child, fork ok + $runfile->wfPrepare($pid); + $outfile = $runfile->getOutfile() ?? "/tmp/NULIB_APP_app_console.out"; + $exitcode = app::EC_FORK_CHILD; + try { + # rediriger STDIN, STDOUT et STDERR + fclose(fopen($outfile, "wb")); // vider le fichier + fclose(STDIN); $in = fopen("/dev/null", "rb"); + fclose(STDOUT); $out = fopen($outfile, "ab"); + fclose(STDERR); $err = fopen($outfile, "ab"); + # puis lancer la commande + $cmd = new Cmd($args); + $cmd->addSource("/g/init.env"); + $cmd->addRedir("both", $outfile, true); + $cmd->fork_exec($exitcode, false); + sh::_waitpid(-$pid, $exitcode); + } finally { + $runfile->wfReaped($exitcode); + } + } + } + + public static function _stop(Runfile $runfile): bool { + $data = $runfile->read(); + $pid = $runfile->_getCid($data); + msg::action("stop $pid"); + if ($runfile->wfKill($reason)) { + msg::asuccess(); + return true; + } else { + msg::afailure($reason); + return false; + } + } +} diff --git a/php/cli/CachectlApp.php b/php/cli/CachectlApp.php new file mode 100644 index 0000000..91f8636 --- /dev/null +++ b/php/cli/CachectlApp.php @@ -0,0 +1,132 @@ + 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"); + } + } + } +} diff --git a/php/cli/DumpserApp.php b/php/cli/DumpserApp.php new file mode 100644 index 0000000..61c4aa7 --- /dev/null +++ b/php/cli/DumpserApp.php @@ -0,0 +1,31 @@ + parent::ARGS, + "purpose" => "afficher des données sérialisées", + ]; + + function main() { + $files = []; + foreach ($this->args as $arg) { + if (is_file($arg)) { + $files[] = $arg; + } else { + msg::warning("$arg: fichier invalide ou introuvable"); + } + } + $showSection = count($files) > 1; + foreach ($files as $file) { + if ($showSection) msg::section($file); + $sfile = new SharedFile($file); + yaml::dump($sfile->unserialize()); + } + } +} diff --git a/php/cli/Json2yamlApp.php b/php/cli/Json2yamlApp.php new file mode 100644 index 0000000..138f184 --- /dev/null +++ b/php/cli/Json2yamlApp.php @@ -0,0 +1,21 @@ +args[0] ?? null; + if ($input === null || $input === "-") { + $output = null; + } else { + $output = path::ensure_ext($input, ".yml", ".json"); + } + + $data = json::load($input); + yaml::dump($data, $output); + } +} \ No newline at end of file diff --git a/php/cli/MysqlCapacitorApp.php b/php/cli/MysqlCapacitorApp.php new file mode 100644 index 0000000..2587d93 --- /dev/null +++ b/php/cli/MysqlCapacitorApp.php @@ -0,0 +1,45 @@ + parent::ARGS, + "purpose" => "gestion d'un capacitor mysql", + "usage" => [ + "DBCONN [channelName | -t table | -c ChannelClass] [--query] key=value...", + "DBCONN [channelName | -t table | -c ChannelClass] --sql-create", + ], + ["-t:table", "--table-name", + "help" => "nom de la table porteuse du canal de données", + ], + ["-c:class", "--channel-class", + "help" => "nom de la classe dérivée de CapacitorChannel", + ], + ["-z", "--reset", "name" => "action", "value" => self::ACTION_RESET, + "help" => "réinitialiser le canal", + ], + ["-n", "--no-recreate", "name" => "recreate", "value" => false, + "help" => "ne pas recréer la table correspondant au canal" + ], + ["--query", "name" => "action", "value" => self::ACTION_QUERY, + "help" => "lister les lignes correspondant aux valeurs spécifiées. c'est l'action par défaut", + ], + ["-s", "--sql-create", "name" => "action", "value" => self::ACTION_SQL, + "help" => "afficher la requête pour créer la table", + ], + ]; + + function main() { + $dbconn = A::shift($this->args); + if ($dbconn === null) self::die("Vous devez spécifier la base de données"); + $tmp = config::db($dbconn); + if ($tmp === null) self::die("$dbconn: base de données invalide"); + $storage = new MysqlStorage($tmp); + + $this->storageCtl($storage); + } +} diff --git a/php/cli/PgsqlCapacitorApp.php b/php/cli/PgsqlCapacitorApp.php new file mode 100644 index 0000000..73e10a2 --- /dev/null +++ b/php/cli/PgsqlCapacitorApp.php @@ -0,0 +1,45 @@ + parent::ARGS, + "purpose" => "gestion d'un capacitor pgsql", + "usage" => [ + "DBCONN [channelName | -t table | -c ChannelClass] [--query] key=value...", + "DBCONN [channelName | -t table | -c ChannelClass] --sql-create", + ], + ["-t:table", "--table-name", + "help" => "nom de la table porteuse du canal de données", + ], + ["-c:class", "--channel-class", + "help" => "nom de la classe dérivée de CapacitorChannel", + ], + ["-z", "--reset", "name" => "action", "value" => self::ACTION_RESET, + "help" => "réinitialiser le canal", + ], + ["-n", "--no-recreate", "name" => "recreate", "value" => false, + "help" => "ne pas recréer la table correspondant au canal" + ], + ["--query", "name" => "action", "value" => self::ACTION_QUERY, + "help" => "lister les lignes correspondant aux valeurs spécifiées. c'est l'action par défaut", + ], + ["-s", "--sql-create", "name" => "action", "value" => self::ACTION_SQL, + "help" => "afficher la requête pour créer la table", + ], + ]; + + function main() { + $dbconn = A::shift($this->args); + if ($dbconn === null) self::die("Vous devez spécifier la base de données"); + $tmp = config::db($dbconn); + if ($tmp === null) self::die("$dbconn: base de données invalide"); + $storage = new PgsqlStorage($tmp); + + $this->storageCtl($storage); + } +} diff --git a/php/cli/SqliteCapacitorApp.php b/php/cli/SqliteCapacitorApp.php new file mode 100644 index 0000000..5f9985a --- /dev/null +++ b/php/cli/SqliteCapacitorApp.php @@ -0,0 +1,43 @@ + parent::ARGS, + "purpose" => "gestion d'un capacitor sqlite", + "usage" => [ + "DBFILE [channelName | -t table | -c ChannelClass] [--query] key=value...", + "DBFILE [channelName | -t table | -c ChannelClass] --sql-create", + ], + ["-t:table", "--table-name", + "help" => "nom de la table porteuse du canal de données", + ], + ["-c:class", "--channel-class", + "help" => "nom de la classe dérivée de CapacitorChannel", + ], + ["-z", "--reset", "name" => "action", "value" => self::ACTION_RESET, + "help" => "réinitialiser le canal", + ], + ["-n", "--no-recreate", "name" => "recreate", "value" => false, + "help" => "ne pas recréer la table correspondant au canal" + ], + ["--query", "name" => "action", "value" => self::ACTION_QUERY, + "help" => "lister les lignes correspondant aux valeurs spécifiées. c'est l'action par défaut", + ], + ["-s", "--sql-create", "name" => "action", "value" => self::ACTION_SQL, + "help" => "afficher la requête pour créer la table", + ], + ]; + + function main() { + $dbfile = A::shift($this->args); + if ($dbfile === null) self::die("Vous devez spécifier la base de données"); + if (!file_exists($dbfile)) self::die("$dbfile: fichier introuvable"); + $storage = new SqliteStorage($dbfile); + + $this->storageCtl($storage); + } +} diff --git a/php/cli/Yaml2jsonApp.php b/php/cli/Yaml2jsonApp.php new file mode 100644 index 0000000..fb3f96f --- /dev/null +++ b/php/cli/Yaml2jsonApp.php @@ -0,0 +1,21 @@ +args[0] ?? null; + if ($input === null || $input === "-") { + $output = null; + } else { + $output = path::ensure_ext($input, ".json", [".yml", ".yaml"]); + } + + $data = yaml::load($input); + json::dump($data, $output); + } +} \ No newline at end of file diff --git a/php/cli/_SteamTrainApp.php b/php/cli/_SteamTrainApp.php new file mode 100644 index 0000000..9ccf13d --- /dev/null +++ b/php/cli/_SteamTrainApp.php @@ -0,0 +1,53 @@ + self::TITLE, + "description" => << "spécifier le nombre d'étapes", + ], + ["-f", "--force-enabled", "value" => true, + "help" => "lancer la commande même si les tâches planifiées sont désactivées", + ], + ["-n", "--no-install-signal-handler", "value" => false, + "help" => "ne pas installer le gestionnaire de signaux", + ], + ]; + + protected $count = 100; + + protected bool $forceEnabled = false; + + protected bool $installSignalHandler = true; + + function main() { + app::check_bgapplication_enabled($this->forceEnabled); + if ($this->installSignalHandler) app::install_signal_handler(); + $count = intval($this->count); + msg::info("Starting train for ".words::q($count, "step#s")); + app::action("Running train...", $count); + for ($i = 1; $i <= $count; $i++) { + msg::print("Tchou-tchou! x $i"); + app::step(); + sleep(1); + } + msg::info("Stopping train at ".new DateTime()); + } +} diff --git a/php/src/tools/pman/ComposerFile.php b/php/cli/pman/ComposerFile.php similarity index 96% rename from php/src/tools/pman/ComposerFile.php rename to php/cli/pman/ComposerFile.php index c3dd7a1..e40a0df 100644 --- a/php/src/tools/pman/ComposerFile.php +++ b/php/cli/pman/ComposerFile.php @@ -1,18 +1,17 @@ composerFile = $composerFile; $this->load(); diff --git a/php/src/tools/pman/ComposerPmanFile.php b/php/cli/pman/ComposerPmanFile.php similarity index 92% rename from php/src/tools/pman/ComposerPmanFile.php rename to php/cli/pman/ComposerPmanFile.php index d8c6474..1ab44c7 100644 --- a/php/src/tools/pman/ComposerPmanFile.php +++ b/php/cli/pman/ComposerPmanFile.php @@ -1,11 +1,11 @@ configFile = $configFile; $this->load(); @@ -66,9 +65,7 @@ class ComposerPmanFile { function getProfileConfig(string $profile, ?array $composerRequires=null, ?array $composerRequireDevs=null): array { $config = $this->data["composer"][$profile] ?? null; - if ($config === null) { - throw new ValueException("$profile: profil invalide"); - } + if ($config === null) throw exceptions::invalid_value($profile, "ce profil"); if ($composerRequires !== null) { $matchRequires = $this->data["composer"]["match_require"]; foreach ($composerRequires as $dep => $version) { diff --git a/php/run-tests b/php/run-tests index 1b0c5a1..75d58d8 100755 --- a/php/run-tests +++ b/php/run-tests @@ -1,5 +1,5 @@ #!/bin/bash # -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8 MYDIR="$(dirname -- "$0")" -VENDOR="$MYDIR/../vendor" +VENDOR="$MYDIR/vendor" "$VENDOR/bin/phpunit" --bootstrap "$VENDOR/autoload.php" "$@" "$MYDIR/tests" diff --git a/php/src/A.php b/php/src/A.php index dffd3d2..b1e7cc6 100644 --- a/php/src/A.php +++ b/php/src/A.php @@ -1,7 +1,6 @@ trace = self::extract_trace($exception->getTrace()); $previous = $exception->getPrevious(); 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 $class; + protected string $class; function getClass(): string { return $this->class; } - /** @var string */ - protected $message; + protected string $message; function getMessage(): string { return $this->message; @@ -61,22 +66,19 @@ class ExceptionShadow { return $this->code; } - /** @var string */ - protected $file; + protected string $file; function getFile(): string { return $this->file; } - /** @var int */ - protected $line; + protected int $line; function getLine(): int { return $this->line; } - /** @var array */ - protected $trace; + protected array $trace; function getTrace(): array { return $this->trace; @@ -92,10 +94,21 @@ class ExceptionShadow { return implode("\n", $lines); } - /** @var ExceptionShadow */ - protected $previous; + protected ?ExceptionShadow $previous; function getPrevious(): ?ExceptionShadow { return $this->previous; } + + protected ?array $userMessage; + + function getUserMessage(): ?array { + return $this->userMessage; + } + + protected ?array $techMessage; + + function getTechMessage(): ?array { + return $this->techMessage; + } } diff --git a/php/src/ExitError.php b/php/src/ExitError.php index a14c3a8..de8501b 100644 --- a/php/src/ExitError.php +++ b/php/src/ExitError.php @@ -18,8 +18,7 @@ class ExitError extends Error { return $this->getCode() !== 0; } - /** @var ?string */ - protected $userMessage; + protected ?string $userMessage; function haveUserMessage(): bool { return $this->userMessage !== null; diff --git a/php/src/StateException.php b/php/src/StateException.php index 3eadf1d..a2f6bfe 100644 --- a/php/src/StateException.php +++ b/php/src/StateException.php @@ -12,12 +12,12 @@ class StateException extends LogicException { if ($method === null) $method = "this method"; $message = "$method is not implemented"; if ($prefix) $prefix = "$prefix: "; - return new static($prefix.$message); + return new static("$prefix$message"); } static final function unexpected_state(?string $suffix=null): self { $message = "unexpected state"; if ($suffix) $suffix = ": $suffix"; - return new static($message.$suffix); + return new static("$message$suffix"); } } diff --git a/php/src/UserException.php b/php/src/UserException.php index 1bef745..e41dc2c 100644 --- a/php/src/UserException.php +++ b/php/src/UserException.php @@ -1,90 +1,35 @@ getUserMessage(); - else return null; + function __construct($userMessage, $code=0, ?Throwable $previous=null) { + $this->userMessage = $userMessage = c::resolve($userMessage); + parent::__construct(c::to_string($userMessage), $code, $previous); } - /** @param Throwable|ExceptionShadow $e */ - 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); - } + protected ?array $userMessage; - /** @param Throwable|ExceptionShadow $e */ - 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 { + function getUserMessage(): ?array { 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; + } } diff --git a/php/src/ValueException.php b/php/src/ValueException.php index 12813d2..b321866 100644 --- a/php/src/ValueException.php +++ b/php/src/ValueException.php @@ -5,72 +5,4 @@ namespace nulib; * Class ValueException: indiquer qu'une valeur est invalide */ 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")); - } } diff --git a/php/src/app/RunFile.php b/php/src/app/RunFile.php index bd34357..c82b5ad 100644 --- a/php/src/app/RunFile.php +++ b/php/src/app/RunFile.php @@ -2,7 +2,6 @@ namespace nulib\app; use nulib\A; -use nulib\app; use nulib\cl; use nulib\file\SharedFile; use nulib\os\path; diff --git a/php/src/app/TODO.md b/php/src/app/TODO.md index bec0657..4b12019 100644 --- a/php/src/app/TODO.md +++ b/php/src/app/TODO.md @@ -1,8 +1,5 @@ # nulib\app -* [ ] ajouter des méthodes normalisées `app::get_cachedir()` et - `app::get_cachefile($name)` avec la valeur par défaut - `cachedir = $vardir/cache` * [ ] `app::action()` et `app::step()` appellent automatiquement `app::_dispatch_signals()` diff --git a/php/src/app/app.php b/php/src/app/app.php new file mode 100644 index 0000000..87b88bc --- /dev/null +++ b/php/src/app/app.php @@ -0,0 +1,655 @@ +getParams(); + } elseif ($app instanceof Application) { + $class = get_class($app); + $params = [ + "class" => $class, + "projdir" => $app::PROJDIR, + "vendor" => $app::VENDOR, + "projcode" => $app::PROJCODE, + "datadir" => $app::DATADIR, + "etcdir" => $app::ETCDIR, + "vardir" => $app::VARDIR, + "cachedir" => $app::CACHEDIR, + "logdir" => $app::LOGDIR, + "appgroup" => $app::APPGROUP, + "name" => $app::NAME, + "title" => $app::TITLE, + ]; + } elseif (self::isa_Application($app)) { + $class = $app; + $params = [ + "class" => $class, + "projdir" => constant("$app::PROJDIR"), + "vendor" => constant("$app::VENDOR"), + "projcode" => constant("$app::PROJCODE"), + "datadir" => constant("$app::DATADIR"), + "etcdir" => constant("$app::ETCDIR"), + "vardir" => constant("$app::VARDIR"), + "cachedir" => constant("$app::CACHEDIR"), + "logdir" => constant("$app::LOGDIR"), + "appgroup" => constant("$app::APPGROUP"), + "name" => constant("$app::NAME"), + "title" => constant("$app::TITLE"), + ]; + } elseif (is_array($app)) { + $params = $app; + } else { + throw exceptions::invalid_type($app, "app", Application::class); + } + return $params; + } + + protected static ?self $app = null; + + /** + * @param Application|string|array $app + * @param Application|string|array|null $proj + */ + static function with($app, $proj=null): self { + $params = self::get_params($app); + $proj ??= self::params_getenv(); + $proj ??= self::$app; + $proj_params = $proj !== null? self::get_params($proj): null; + if ($proj_params !== null) { + A::merge($params, cl::select($proj_params, [ + "projdir", + "vendor", + "projcode", + "cwd", + "datadir", + "etcdir", + "vardir", + "cachedir", + "logdir", + "profile", + "facts", + "debug", + ])); + } + return new static($params, $proj_params !== null); + } + + static function init($app, $proj=null): void { + self::$app = static::with($app, $proj); + } + + static function get(): self { + return self::$app ??= new static(null); + } + + static function params_putenv(): void { + $params = serialize(self::get()->getParams()); + putenv("NULIB_APP_app_params=$params"); + } + + static function params_getenv(): ?array { + $params = getenv("NULIB_APP_app_params"); + if ($params === false) return null; + return unserialize($params); + } + + static function get_profile(?bool &$productionMode=null): string { + return self::get()->getProfile($productionMode); + } + + static function is_production_mode(): bool { + return self::get()->isProductionMode(); + } + + static function is_prod(): bool { + return self::get_profile() === ref_profiles::PROD; + } + + static function is_test(): bool { + return self::get_profile() === ref_profiles::TEST; + } + + static function is_devel(): bool { + return self::get_profile() === ref_profiles::DEVEL; + } + + static function set_profile(?string $profile=null, ?bool $productionMode=null): void { + self::get()->setProfile($profile, $productionMode); + } + + const FACT_WEB_APP = "web-app"; + const FACT_CLI_APP = "cli-app"; + + static final function is_fact(string $fact, $value=true): bool { + return self::get()->isFact($fact, $value); + } + + static final function set_fact(string $fact, $value=true): void { + self::get()->setFact($fact, $value); + } + + static function is_debug(): bool { + return self::get()->isDebug(); + } + + static function set_debug(?bool $debug=true): void { + self::get()->setDebug($debug); + } + + /** + * @var array répertoires vendor exprimés relativement à PROJDIR + */ + const DEFAULT_VENDOR = [ + "bindir" => "vendor/bin", + "autoload" => "vendor/autoload.php", + ]; + + function __construct(?array $params, bool $useProjParams=false) { + if ($useProjParams) { + [ + "projdir" => $projdir, + "vendor" => $vendor, + "projcode" => $projcode, + "datadir" => $datadir, + "etcdir" => $etcdir, + "vardir" => $vardir, + "cachedir" => $cachedir, + "logdir" => $logdir, + ] = $params; + $cwd = $params["cwd"] ?? null; + $datadirIsDefined = true; + } else { + # projdir + $projdir = $params["projdir"] ?? null; + if ($projdir === null) { + global $_composer_autoload_path, $_composer_bin_dir; + $autoload = $_composer_autoload_path ?? null; + $bindir = $_composer_bin_dir ?? null; + if ($autoload !== null) { + $vendor = preg_replace('/\/[^\/]+\.php$/', "", $autoload); + $bindir ??= "$vendor/bin"; + $projdir = preg_replace('/\/[^\/]+$/', "", $vendor); + $params["vendor"] = [ + "autoload" => $autoload, + "bindir" => $bindir, + ]; + } + } + if ($projdir === null) $projdir = "."; + $projdir = path::abspath($projdir); + # vendor + $vendor = $params["vendor"] ?? self::DEFAULT_VENDOR; + $vendor["bindir"] = path::reljoin($projdir, $vendor["bindir"]); + $vendor["autoload"] = path::reljoin($projdir, $vendor["autoload"]); + # projcode + $projcode = $params["projcode"] ?? null; + if ($projcode === null) { + $projcode = str::without_suffix("-app", path::basename($projdir)); + } + $PROJCODE = str_replace("-", "_", strtoupper($projcode)); + # cwd + $cwd = $params["cwd"] ?? null; + # datadir + $datadir = getenv("${PROJCODE}_DATADIR"); + $datadirIsDefined = $datadir !== false; + if ($datadir === false) $datadir = $params["datadir"] ?? null; + if ($datadir === null) $datadir = "devel"; + $datadir = path::reljoin($projdir, $datadir); + # etcdir + $etcdir = getenv("${PROJCODE}_ETCDIR"); + if ($etcdir === false) $etcdir = $params["etcdir"] ?? null; + if ($etcdir === null) $etcdir = "etc"; + $etcdir = path::reljoin($datadir, $etcdir); + # vardir + $vardir = getenv("${PROJCODE}_VARDIR"); + if ($vardir === false) $vardir = $params["vardir"] ?? null; + if ($vardir === null) $vardir = "var"; + $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 = getenv("${PROJCODE}_LOGDIR"); + if ($logdir === false) $logdir = $params["logdir"] ?? null; + if ($logdir === null) $logdir = "log"; + $logdir = path::reljoin($datadir, $logdir); + } + # cwd + $cwd ??= getcwd(); + # profile + $this->profileManager = new ProfileManager([ + "app" => true, + "name" => $projcode, + "default_profile" => $datadirIsDefined? "prod": "devel", + "profile" => $params["profile"] ?? null, + ]); + # $facts + $this->facts = $params["facts"] ?? null; + # debug + $this->debug = $params["debug"] ?? null; + + $this->projdir = $projdir; + $this->vendor = $vendor; + $this->projcode = $projcode; + $this->cwd = $cwd; + $this->datadir = $datadir; + $this->etcdir = $etcdir; + $this->vardir = $vardir; + $this->cachedir = $cachedir; + $this->logdir = $logdir; + + # name, title + $appgroup = $params["appgroup"] ?? null; + $name = $params["name"] ?? $params["class"] ?? null; + if ($name === null) { + $name = $projcode; + } else { + # si $name est une classe, enlever le package et normaliser i.e + # my\package\MyApplication --> my-application.php + $name = preg_replace('/.*\\\\/', "", $name); + $name = str::camel2us($name, false, "-"); + $name = str::without_suffix("-app", $name); + } + $this->appgroup = $appgroup; + $this->name = $name; + $this->title = $params["title"] ?? null; + } + + ############################################################################# + # Paramètres partagés par tous les scripts d'un projet (et les scripts lancés + # à partir d'une application de ce projet) + + protected string $projdir; + + function getProjdir(): string { + return $this->projdir; + } + + protected array $vendor; + + function getVendorBindir(): string { + return $this->vendor["bindir"]; + } + + function getVendorAutoload(): string { + return $this->vendor["autoload"]; + } + + protected string $projcode; + + function getProjcode(): string { + return $this->projcode; + } + + protected string $cwd; + + function getCwd(): string { + return $this->cwd; + } + + protected string $datadir; + + function getDatadir(): string { + return $this->datadir; + } + + protected string $etcdir; + + function getEtcdir(): string { + return $this->etcdir; + } + + protected string $vardir; + + function getVardir(): string { + return $this->vardir; + } + + protected string $cachedir; + + function getCachedir(): string { + return $this->cachedir; + } + + protected string $logdir; + + function getLogdir(): string { + return $this->logdir; + } + + protected ProfileManager $profileManager; + + function getProfile(?bool &$productionMode=null): string { + return $this->profileManager->getProfile($productionMode); + } + + function isProductionMode(): bool { + return $this->profileManager->isProductionMode(); + } + + function setProfile(?string $profile, ?bool $productionMode=null): void { + $this->profileManager->setProfile($profile, $productionMode); + } + + protected ?array $facts; + + function isFact(string $fact, $value=true): bool { + return ($this->facts[$fact] ?? false) === $value; + } + + function setFact(string $fact, $value=true): void { + $this->facts[$fact] = $value; + } + + protected ?bool $debug; + + function isDebug(): bool { + $debug = $this->debug; + if ($debug === null) { + $debug = defined("DEBUG")? DEBUG: null; + $DEBUG = getenv("DEBUG"); + $debug ??= $DEBUG !== false? $DEBUG: null; + $debug ??= config::k("debug"); + $debug ??= false; + $this->debug = $debug; + } + return $debug; + } + + function setDebug(bool $debug=true): void { + $this->debug = $debug; + } + + /** + * @param ?string|false $profile + * + * false === pas de profil + * null === profil par défaut + */ + function withProfile(string $file, $profile): string { + if ($profile !== false) { + $profile ??= $this->getProfile(); + [$dir, $filename] = path::split($file); + $basename = path::basename($filename); + $ext = path::ext($file); + $file = path::join($dir, "$basename.$profile$ext"); + } + return $file; + } + + function findFile(array $dirs, array $names, $profile=null): string { + # d'abord chercher avec le profil + if ($profile !== false) { + foreach ($dirs as $dir) { + foreach ($names as $name) { + $file = path::join($dir, $name); + $file = $this->withProfile($file, $profile); + if (file_exists($file)) return $file; + } + } + } + # puis sans profil + foreach ($dirs as $dir) { + foreach ($names as $name) { + $file = path::join($dir, $name); + if (file_exists($file)) return $file; + } + } + # la valeur par défaut est avec profil + return $this->withProfile(path::join($dirs[0], $names[0]), $profile); + } + + function fencedJoin(string $basedir, ?string ...$paths): string { + $path = path::reljoin($basedir, ...$paths); + if (!path::is_within($path, $basedir)) { + throw exceptions::invalid_value($path, "path"); + } + return $path; + } + + ############################################################################# + # Paramètres spécifiques à cette application + + protected ?string $appgroup; + + function getAppgroup(): ?string { + return $this->appgroup; + } + + protected string $name; + + function getName(): ?string { + return $this->name; + } + + protected ?string $title; + + function getTitle(): ?string { + return $this->title; + } + + ############################################################################# + # Méthodes outils + + /** recréer le tableau des paramètres */ + function getParams(): array { + return [ + "projdir" => $this->projdir, + "vendor" => $this->vendor, + "projcode" => $this->projcode, + "cwd" => $this->cwd, + "datadir" => $this->datadir, + "etcdir" => $this->etcdir, + "vardir" => $this->vardir, + "cachedir" => $this->cachedir, + "logdir" => $this->logdir, + "profile" => $this->getProfile(), + "facts" => $this->facts, + "debug" => $this->debug, + "appgroup" => $this->appgroup, + "name" => $this->name, + "title" => $this->title, + ]; + } + + /** + * obtenir le chemin vers le fichier de configuration. par défaut, retourner + * une valeur de la forme "$ETCDIR/$name[.$profile].conf" + */ + function getEtcfile(?string $name=null, $profile=null): string { + $name ??= "{$this->name}.conf"; + return $this->findFile([$this->etcdir], [$name], $profile); + } + + /** + * obtenir le chemin vers le fichier de travail. par défaut, retourner une + * valeur de la forme "$VARDIR/$appgroup/$name[.$profile].tmp" + */ + function getVarfile(?string $name=null, $profile=null): string { + $name ??= "{$this->name}.tmp"; + $file = $this->fencedJoin($this->vardir, $this->appgroup, $name); + $file = $this->withProfile($file, $profile); + sh::mkdirof($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 + * valeur de la forme "$LOGDIR/$appgroup/$name.log" (sans le profil, parce + * qu'il s'agit du fichier de log par défaut) + * + * Si $name est spécifié, la valeur retournée sera de la forme + * "$LOGDIR/$appgroup/$basename[.$profile].$ext" + */ + function getLogfile(?string $name=null, $profile=null): string { + if ($name === null) { + $name = "{$this->name}.log"; + $profile ??= false; + } + $logfile = $this->fencedJoin($this->logdir, $this->appgroup, $name); + $logfile = $this->withProfile($logfile, $profile); + sh::mkdirof($logfile); + return $logfile; + } + + /** + * obtenir le chemin absolu vers un fichier de travail + * - si le chemin est absolu, il est inchangé + * - sinon le chemin est exprimé par rapport à $vardir/$appgroup + * + * is $ensureDir, créer le répertoire du fichier s'il n'existe pas déjà + * + * la différence avec {@link self::getVarfile()} est que le fichier peut + * au final être situé ailleurs que dans $vardir. de plus, il n'y a pas de + * valeur par défaut pour $file + */ + function getWorkfile(string $file, $profile=null, bool $ensureDir=true): string { + $file = path::reljoin($this->vardir, $this->appgroup, $file); + $file = $this->withProfile($file, $profile); + if ($ensureDir) sh::mkdirof($file); + return $file; + } + + /** + * obtenir le chemin absolu vers un fichier spécifié par l'utilisateur. + * - si le chemin commence par /, il est laissé en l'état + * - si le chemin commence par ./ ou ../, il est exprimé par rapport à $cwd + * - sinon le chemin est exprimé par rapport à $vardir/$appgroup + * + * la différence est avec {@link self::getVarfile()} est que le fichier peut + * au final être situé ailleurs que dans $vardir. de plus, il n'y a pas de + * valeur par défaut pour $file + */ + function getUserfile(string $file): string { + if (path::is_qualified($file)) { + return path::reljoin($this->cwd, $file); + } else { + return path::reljoin($this->vardir, $this->appgroup, $file); + } + } + + protected ?RunFile $runfile = null; + + function getRunfile(): RunFile { + $name = $this->name; + $runfile = $this->getWorkfile($name); + $logfile = $this->getLogfile("$name.out", false); + return $this->runfile ??= new RunFile($name, $runfile, $logfile); + } + + protected ?array $lockFiles = null; + + function getLockfile(?string $name=null): LockFile { + $this->lockFiles[$name] ??= $this->getRunfile()->getLockFile($name, $this->title); + return $this->lockFiles[$name]; + } + + ############################################################################# + + const EC_FORK_CHILD = 250; + const EC_FORK_PARENT = 251; + const EC_DISABLED = 252; + const EC_LOCKED = 253; + const EC_BAD_COMMAND = 254; + const EC_UNEXPECTED = 255; + + ############################################################################# + + static bool $dispach_signals = false; + + static function install_signal_handler(bool $allow=true): void { + if (!$allow) return; + $signalHandler = function(int $signo, $siginfo) { + throw new ExitError(128 + $signo); + }; + pcntl_signal(SIGHUP, $signalHandler); + pcntl_signal(SIGINT, $signalHandler); + pcntl_signal(SIGQUIT, $signalHandler); + pcntl_signal(SIGTERM, $signalHandler); + self::$dispach_signals = true; + } + + static function _dispatch_signals() { + if (self::$dispach_signals) pcntl_signal_dispatch(); + } + + ############################################################################# + + static ?func $bgapplication_enabled = null; + + /** + * spécifier la fonction permettant de vérifier si l'exécution de tâches + * de fond est autorisée. Si cette méthode n'est pas utilisée, par défaut, + * les tâches planifiées sont autorisées + * + * si $func===true, spécifier une fonction qui retourne toujours vrai + * si $func===false, spécifiée une fonction qui retourne toujours faux + * sinon, $func doit être une fonction valide + */ + static function set_bgapplication_enabled($func): void { + if (is_bool($func)) { + $enabled = $func; + $func = function () use ($enabled) { + return $enabled; + }; + } + self::$bgapplication_enabled = func::with($func); + } + + /** + * Si les exécutions en tâche de fond sont autorisée, retourner. Sinon + * afficher une erreur et quitter l'application + */ + static function check_bgapplication_enabled(bool $forceEnabled=false): void { + if (self::$bgapplication_enabled === null || $forceEnabled) return; + if (!self::$bgapplication_enabled->invoke()) { + throw new ExitError(self::EC_DISABLED, "Planifications désactivées. La tâche n'a pas été lancée"); + } + } + + ############################################################################# + + static function action(?string $title, ?int $maxSteps=null): void { + self::get()->getRunfile()->action($title, $maxSteps); + } + + static function step(int $nbSteps=1): void { + self::get()->getRunfile()->step($nbSteps); + } +} diff --git a/php/src/app/args/AbstractArgsParser.php b/php/src/app/args/AbstractArgsParser.php new file mode 100644 index 0000000..f1fc241 --- /dev/null +++ b/php/src/app/args/AbstractArgsParser.php @@ -0,0 +1,110 @@ + 0) throw $this->notEnoughArgs($count, $option); + } + + protected function tooManyArgs(int $count, int $expected, ?string $arg=null): ArgsException { + if ($arg !== null) $arg .= ": "; + $reason = $arg._exceptions::unexpected_value_message($count - $expected); + return _exceptions::unexpected_value(null, null, $reason); + } + + protected function invalidArg(string $arg): ArgsException { + return _exceptions::invalid_value($arg); + } + + protected function ambiguousArg(string $arg, array $candidates): ArgsException { + $candidates = implode(", ", $candidates); + return new ArgsException("$arg: cet argument est ambigû (les valeurs possibles sont $candidates)"); + } + + /** + * consommer les arguments de $src en avançant l'index $srci et provisionner + * $dest à partir de $desti. si $desti est plus grand que 0, celà veut dire + * que $dest a déjà commencé à être provisionné, et qu'il faut continuer. + * + * $destmin est le nombre minimum d'arguments à consommer. $destmax est le + * nombre maximum d'arguments à consommer. + * + * $srci est la position de l'élément courant à consommer le cas échéant + * retourner le nombre d'arguments qui manquent (ou 0 si tous les arguments + * ont été consommés) + * + * pour les arguments optionnels, ils sont consommés tant qu'il y en a de + * disponible, ou jusqu'à la présence de '--'. Si $keepsep, l'argument '--' + * est gardé dans la liste des arguments optionnels. + */ + protected static function consume_args($src, &$srci, &$dest, $desti, $destmin, $destmax, bool $keepsep): int { + $srcmax = count($src); + # arguments obligatoires + while ($desti < $destmin) { + if ($srci < $srcmax) { + $dest[] = $src[$srci]; + } else { + # pas assez d'arguments + return $destmin - $desti; + } + $srci++; + $desti++; + } + # arguments facultatifs + $eoo = false; // l'option a-t-elle été terminée? + while ($desti < $destmax && $srci < $srcmax) { + $opt = $src[$srci]; + $srci++; + $desti++; + if ($opt === "--") { + # fin des arguments facultatifs en entrée + $eoo = true; + if ($keepsep) $dest[] = "--"; + break; + } + $dest[] = $opt; + } + if (!$eoo && $desti < $destmax) { + # pas assez d'arguments en entrée, terminer avec "--" + if ($keepsep) $dest[] = "--"; + } + return 0; + } + + abstract function normalize(array $args): array; + + /** @var object|array objet destination */ + protected $dest; + + protected function setDest(&$dest): void { + $this->dest =& $dest; + } + + protected function unsetDest(): void { + unset($this->dest); + } + + abstract function process(array $args); + + function parse(&$dest, array $args=null): void { + if ($args === null) { + global $argv; + $args = array_slice($argv, 1); + } + $args = $this->normalize($args); + $dest ??= new stdClass(); + $this->setDest($dest); + $this->process($args); + $this->unsetDest(); + } + + abstract function actionPrintHelp(string $arg): void; +} diff --git a/php/src/app/args/Aodef.php b/php/src/app/args/Aodef.php new file mode 100644 index 0000000..921a197 --- /dev/null +++ b/php/src/app/args/Aodef.php @@ -0,0 +1,643 @@ +origDef = $def; + $this->mergeParse($def); + //$this->debugTrace("construct"); + } + + protected array $origDef; + + public bool $show = true; + public ?bool $disabled = null; + public ?bool $isRemains = null; + public ?string $extends = null; + + protected ?array $_removes = null; + protected ?array $_adds = null; + + protected ?array $_args = null; + public ?string $argsdesc = null; + + public ?bool $ensureArray = null; + public $action = null; + public ?func $func = null; + public ?bool $inverse = null; + public $value = null; + public ?string $name = null; + public ?string $property = null; + public ?string $key = null; + + public ?string $help = null; + + protected ?array $_options = []; + + public bool $haveShortOptions = false; + public bool $haveLongOptions = false; + public bool $isCommand = false; + public bool $isHelp = false; + + public bool $haveArgs = false; + public ?int $minArgs = null; + public ?int $maxArgs = null; + + protected function mergeParse(array $def): void { + $merges = $defs["merges"] ?? null; + $merge = $defs["merge"] ?? null; + if ($merge !== null) $merges[] = $merge; + if ($merges !== null) { + foreach ($merges as $merge) { + if ($merge !== null) $this->mergeParse($merge); + } + } + + $this->parse($def); + + $merge = $defs["merge_after"] ?? null; + if ($merge !== null) $this->mergeParse($merge); + } + + private static function verifix_args(?array &$options): ?array { + $args = null; + if ($options !== null) { + foreach ($options as &$option) { + if (preg_match('/^(.*:)([^:].*)$/', $option, $ms)) { + $option = $ms[1]; + $args ??= explode(",", $ms[2]); + } + }; unset($option); + } + return $args; + } + + protected function parse(array $def): void { + [$options, $params] = cl::split_assoc($def); + + $this->show ??= $params["show"] ?? true; + $this->extends ??= $params["extends"] ?? null; + + $args ??= $params["args"] ?? null; + $args ??= $params["arg"] ?? null; + if ($args === true) $args = 1; + elseif ($args === "*") $args = [null]; + elseif ($args === "+") $args = ["value", null]; + if (is_int($args)) $args = array_fill(0, $args, "value"); + + $this->disabled = vbool::withn($params["disabled"] ?? null); + $adds = varray::withn($params["add"] ?? null); + A::merge($this->_adds, $adds); + A::merge($this->_adds, $options); + $args ??= self::verifix_args($this->_adds); + $removes = varray::withn($params["remove"] ?? null); + A::merge($this->_removes, $removes); + self::verifix_args($this->_adds); + + $this->_args ??= cl::withn($args); + $this->argsdesc ??= $params["argsdesc"] ?? null; + + $this->ensureArray ??= $params["ensure_array"] ?? null; + $this->action = $params["action"] ?? null; + $this->inverse ??= $params["inverse"] ?? null; + $this->value ??= $params["value"] ?? null; + $this->name ??= $params["name"] ?? null; + $this->property ??= $params["property"] ?? null; + $this->key ??= $params["key"] ?? null; + + $this->help ??= $params["help"] ?? null; + } + + function isExtends(): bool { + return $this->extends !== null; + } + + function setup1(bool $extends=false, ?Aolist $aolist=null): void { + if (!$extends && !$this->isExtends()) { + $this->processOptions(); + } elseif ($extends && $this->isExtends()) { + $this->processExtends($aolist); + } + $this->initRemains(); + //$this->debugTrace("setup1"); + } + + protected function processExtends(Aolist $argdefs): void { + $option = $this->extends; + if ($option === null) { + throw _exceptions::null_value("extends", "il doit spécifier l'argument destination"); + } + $dest = $argdefs->get($option); + if ($dest === null) { + throw _exceptions::invalid_value($option, "extends", "il doit spécifier un argument valide"); + } + + if ($this->ensureArray !== null) $dest->ensureArray = $this->ensureArray; + if ($this->action !== null) $dest->action = $this->action; + if ($this->inverse !== null) $dest->inverse = $this->inverse; + if ($this->value !== null) $dest->value = $this->value; + if ($this->name !== null) $dest->name = $this->name; + if ($this->property !== null) $dest->property = $this->property; + if ($this->key !== null) $dest->key = $this->key; + + A::merge($dest->_removes, $this->_removes); + A::merge($dest->_adds, $this->_adds); + $dest->processOptions(); + } + + function buildOptions(?array $options): array { + $result = []; + if ($options !== null) { + foreach ($options as $option) { + if (substr($option, 0, 2) === "--") { + $type = self::TYPE_LONG; + if (preg_match('/^--([^:-][^:]*)(::?)?$/', $option, $ms)) { + $name = $ms[1]; + $args = $ms[2] ?? null; + $option = "--$name"; + } else { + throw _exceptions::invalid_value($option, "cette option longue"); + } + } elseif (substr($option, 0, 1) === "-") { + $type = self::TYPE_SHORT; + if (preg_match('/^-([^:-])(::?)?$/', $option, $ms)) { + $name = $ms[1]; + $args = $ms[2] ?? null; + $option = "-$name"; + } else { + throw _exceptions::invalid_value($option, " cette option courte"); + } + } else { + $type = self::TYPE_COMMAND; + if (preg_match('/^([^:-][^:]*)$/', $option, $ms)) { + $name = $ms[1]; + $args = null; + $option = "$name"; + } else { + throw _exceptions::invalid_value($option, "cette commande"); + } + } + if ($args === ":") { + $argsType = self::ARGS_MANDATORY; + } elseif ($args === "::") { + $argsType = self::ARGS_OPTIONAL; + } else { + $argsType = self::ARGS_NONE; + } + $result[$option] = [ + "name" => $name, + "option" => $option, + "type" => $type, + "args_type" => $argsType, + ]; + } + } + return $result; + } + + protected function initRemains(): void { + if ($this->isRemains === null) { + $options = array_fill_keys(array_keys($this->_options), true); + foreach (array_keys($this->buildOptions($this->_removes)) as $option) { + unset($options[$option]); + } + foreach (array_keys($this->buildOptions($this->_adds)) as $option) { + unset($options[$option]); + } + if (!$options) $this->isRemains = true; + } + } + + /** traiter le paramètre parent */ + protected function processOptions(): void { + $this->removeOptions($this->_removes); + $this->_removes = null; + $this->addOptions($this->_adds); + $this->_adds = null; + } + + function addOptions(?array $options): void { + // les options pouvant être numériques (e.g "-1"), utiliser A::merge2 + A::merge2($this->_options, $this->buildOptions($options)); + $this->updateType(); + } + + function removeOptions(?array $options): void { + foreach ($this->buildOptions($options) as $option) { + unset($this->_options[$option["option"]]); + } + $this->updateType(); + } + + function removeOption(string $option): void { + unset($this->_options[$option]); + } + + /** mettre à jour le type d'option */ + protected function updateType(): void { + $haveShortOptions = false; + $haveLongOptions = false; + $isCommand = false; + $isHelp = false; + foreach ($this->_options as $option) { + switch ($option["type"]) { + case self::TYPE_SHORT: + $haveShortOptions = true; + break; + case self::TYPE_LONG: + $haveLongOptions = true; + break; + case self::TYPE_COMMAND: + $isCommand = true; + break; + } + switch ($option["option"]) { + case "--help": + case "--help++": + $isHelp = true; + break; + } + } + $this->haveShortOptions = $haveShortOptions; + $this->haveLongOptions = $haveLongOptions; + $this->isCommand = $isCommand; + $this->isHelp = $isHelp; + } + + function setup2(): void { + $this->processArgs(); + $this->processAction(); + $this->afterSetup(); + //$this->debugTrace("setup2"); + } + + /** + * traiter les informations concernant les arguments puis calculer les nombres + * minimum et maximum d'arguments que prend l'option + */ + protected function processArgs(): void { + $args = $this->_args; + if ($this->isRemains) { + $args ??= [null]; + $haveArgs = boolval($args); + } elseif ($args === null) { + $haveArgs = false; + $optionalArgs = null; + foreach ($this->_options as $option) { + switch ($option["args_type"]) { + case self::ARGS_NONE: + break; + case self::ARGS_MANDATORY: + $haveArgs = true; + $optionalArgs = false; + break; + case self::ARGS_OPTIONAL: + $haveArgs = true; + $optionalArgs ??= true; + break; + } + } + $optionalArgs ??= false; + if ($haveArgs) { + $args = ["value"]; + if ($optionalArgs) $args = [$args]; + } + } else { + $haveArgs = boolval($args); + } + + if ($this->isRemains) $desc = "remaining args"; + else $desc = cl::first($this->_options)["option"]; + + $args ??= []; + $argsdesc = []; + $reqs = []; + $haveNull = false; + $optArgs = null; + foreach ($args as $arg) { + if (is_string($arg)) { + $reqs[] = $arg; + $argsdesc[] = strtoupper($arg); + } elseif (is_array($arg)) { + $optArgs = $arg; + break; + } elseif ($arg === null) { + $haveNull = true; + break; + } else { + throw _exceptions::invalid_value("$desc: $arg"); + } + } + + $opts = []; + $optArgsdesc = null; + $lastarg = "VALUE"; + if ($optArgs !== null) { + $haveOpt = false; + foreach ($optArgs as $arg) { + if (is_string($arg)) { + $haveOpt = true; + $opts[] = $arg; + $lastarg = strtoupper($arg); + $optArgsdesc[] = $lastarg; + } elseif ($arg === null) { + $haveNull = true; + break; + } else { + throw _exceptions::invalid_value("$desc: $arg"); + } + } + if (!$haveOpt) $haveNull = true; + } + if ($haveNull) $optArgsdesc[] = "${lastarg}s..."; + if ($optArgsdesc !== null) { + $argsdesc[] = "[".implode(" ", $optArgsdesc)."]"; + } + + $minArgs = count($reqs); + if ($haveNull) $maxArgs = PHP_INT_MAX; + else $maxArgs = $minArgs + count($opts); + + $this->haveArgs = $haveArgs; + $this->minArgs = $minArgs; + $this->maxArgs = $maxArgs; + $this->argsdesc ??= implode(" ", $argsdesc); + } + + private static function get_longest(array $options, int $type): ?string { + $longest = null; + $maxlen = 0; + foreach ($options as $option) { + if ($option["type"] !== $type) continue; + $name = $option["name"]; + $len = strlen($name); + if ($len > $maxlen) { + $longest = $name; + $maxlen = $len; + } + } + return $longest; + } + + protected function processAction(): void { + $this->ensureArray ??= $this->isRemains || $this->maxArgs > 1; + + $action = $this->action; + $func = $this->func; + if ($action === null) { + if ($this->isCommand) $action = "--set-command"; + elseif ($this->isRemains) $action = "--set-args"; + elseif ($this->isHelp) $action = "--show-help"; + elseif ($this->haveArgs) $action = "--set"; + elseif ($this->value !== null) $action = "--set"; + else $action = "--inc"; + } + if (is_string($action) && substr($action, 0, 2) === "--") { + # fonction interne + } else { + $func = func::with($action); + $action = "--func"; + } + $this->action = $action; + $this->func = $func; + + $name = $this->name; + $property = $this->property; + $key = $this->key; + if ($action !== "--func" && !$this->isRemains && + $name === null && $property === null && $key === null + ) { + # si on ne précise pas le nom de la propriété, la dériver à partir du + # nom de l'option la plus longue + $longest = self::get_longest($this->_options, self::TYPE_LONG); + $longest ??= self::get_longest($this->_options, self::TYPE_COMMAND); + $longest ??= self::get_longest($this->_options, self::TYPE_SHORT); + if ($longest !== null) { + $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)) { + # le nom de la propriété ne doit pas commencer par un chiffre + $longest = "p$longest"; + } + $name = $longest; + } + } elseif ($name === null && $property !== null) { + $name = $property; + } elseif ($name === null && $key !== null) { + $name = $key; + } + $this->name = $name; + } + + protected function afterSetup(): void { + $this->disabled ??= false; + $this->ensureArray ??= false; + $this->inverse ??= false; + if (str::del_prefix($this->help, "++")) { + $this->show = false; + } + } + + function getOptions(): array { + if ($this->disabled) return []; + else return array_keys($this->_options); + } + + function isEmpty(): bool { + return $this->disabled || (!$this->_options && !$this->isRemains); + } + + function printHelp(?array $what=null): void { + $showDef = $what["show"] ?? $this->show; + if (!$showDef || $this->isRemains) return; + + $prefix = $what["prefix"] ?? null; + if ($prefix !== null) echo $prefix; + + $showOptions = $what["options"] ?? true; + if ($showOptions) { + echo " "; + echo implode(", ", array_keys($this->_options)); + if ($this->haveArgs) { + echo " "; + echo $this->argsdesc; + } + echo "\n"; + } + + $showHelp = $what["help"] ?? true; + if ($this->help && $showHelp) { + echo str::indent($this->help, " "); + echo "\n"; + } + } + + function action(&$dest, $value, ?string $arg, AbstractArgsParser $parser): void { + if ($this->ensureArray) { + varray::ensure($value); + } elseif (is_array($value)) { + $count = count($value); + if ($count == 0) $value = null; + elseif ($count == 1) $value = $value[0]; + } + + switch ($this->action) { + case "--set": $this->actionSet($dest, $value); break; + case "--inc": $this->actionInc($dest); break; + case "--dec": $this->actionDec($dest); break; + case "--add": $this->actionAdd($dest, $value); break; + case "--adds": $this->actionAdds($dest, $value); break; + case "--merge": $this->actionMerge($dest, $value); break; + case "--merges": $this->actionMerges($dest, $value); break; + case "--func": $this->func->bind($dest)->invoke([$value, $arg, $this]); break; + case "--set-args": $this->actionSetArgs($dest, $value); break; + case "--set-command": $this->actionSetCommand($dest, $value); break; + case "--show-help": $parser->actionPrintHelp($arg); break; + default: throw _exceptions::invalid_value($this->action, null, "action non supportée"); + } + } + + function actionSet(&$dest, $value): void { + if ($this->property !== null) { + oprop::set($dest, $this->property, $value); + } elseif ($this->key !== null) { + akey::set($dest, $this->key, $value); + } elseif ($this->name !== null) { + valx::set($dest, $this->name, $value); + } + } + + function actionInc(&$dest): void { + if ($this->property !== null) { + if ($this->inverse) oprop::dec($dest, $this->property); + else oprop::inc($dest, $this->property); + } elseif ($this->key !== null) { + if ($this->inverse) akey::dec($dest, $this->key); + else akey::inc($dest, $this->key); + } elseif ($this->name !== null) { + if ($this->inverse) valx::dec($dest, $this->name); + else valx::inc($dest, $this->name); + } + } + + function actionDec(&$dest): void { + if ($this->property !== null) { + if ($this->inverse) oprop::inc($dest, $this->property); + else oprop::dec($dest, $this->property); + } elseif ($this->key !== null) { + if ($this->inverse) akey::inc($dest, $this->key); + else akey::dec($dest, $this->key); + } elseif ($this->name !== null) { + if ($this->inverse) valx::inc($dest, $this->name); + else valx::dec($dest, $this->name); + } + } + + function actionAdd(&$dest, $value): void { + if ($this->property !== null) { + oprop::append($dest, $this->property, $value); + } elseif ($this->key !== null) { + akey::append($dest, $this->key, $value); + } elseif ($this->name !== null) { + valx::append($dest, $this->name, $value); + } + } + + function actionAdds(&$dest, $value): void { + if ($this->property !== null) { + foreach (cl::with($value) as $value) { + oprop::append($dest, $this->property, $value); + } + } elseif ($this->key !== null) { + foreach (cl::with($value) as $value) { + akey::append($dest, $this->key, $value); + } + } elseif ($this->name !== null) { + foreach (cl::with($value) as $value) { + valx::append($dest, $this->name, $value); + } + } + } + + function actionMerge(&$dest, $value): void { + if ($this->property !== null) { + oprop::merge($dest, $this->property, $value); + } elseif ($this->key !== null) { + akey::merge($dest, $this->key, $value); + } elseif ($this->name !== null) { + valx::merge($dest, $this->name, $value); + } + } + + function actionMerges(&$dest, $value): void { + if ($this->property !== null) { + foreach (cl::with($value) as $value) { + oprop::merge($dest, $this->property, $value); + } + } elseif ($this->key !== null) { + foreach (cl::with($value) as $value) { + akey::merge($dest, $this->key, $value); + } + } elseif ($this->name !== null) { + foreach (cl::with($value) as $value) { + valx::merge($dest, $this->name, $value); + } + } + } + + function actionSetArgs(&$dest, $value): void { + if ($this->property !== null) { + oprop::set($dest, $this->property, $value); + } elseif ($this->key !== null) { + akey::set($dest, $this->key, $value); + } elseif ($this->name !== null) { + valx::set($dest, $this->name, $value); + } + } + + function actionSetCommand(&$dest, $value): void { + if ($this->property !== null) { + oprop::set($dest, $this->property, $value); + } elseif ($this->key !== null) { + akey::set($dest, $this->key, $value); + } elseif ($this->name !== null) { + valx::set($dest, $this->name, $value); + } + } + + function __toString(): string { + $options = implode(",", $this->getOptions()); + $args = $this->haveArgs? " ({$this->minArgs}-{$this->maxArgs})": false; + return "$options$args"; + } + private function debugTrace(string $message): void { + $options = implode(",", cl::split_assoc($this->origDef)[0] ?? []); + echo "$options $message\n"; + } +} diff --git a/php/src/app/args/Aogroup.php b/php/src/app/args/Aogroup.php new file mode 100644 index 0000000..d293a75 --- /dev/null +++ b/php/src/app/args/Aogroup.php @@ -0,0 +1,36 @@ +all() as $aodef) { + $firstAodef ??= $aodef; + $aodef->printHelp(["help" => false]); + } + if ($firstAodef !== null) { + $firstAodef->printHelp(["options" => false]); + } + } +} diff --git a/php/src/app/args/Aolist.php b/php/src/app/args/Aolist.php new file mode 100644 index 0000000..0862615 --- /dev/null +++ b/php/src/app/args/Aolist.php @@ -0,0 +1,268 @@ +origDefs = $defs; + $this->initDefs($defs, $setup); + } + + protected array $origDefs; + + protected ?array $aomain; + protected ?array $aosections; + protected ?array $aospecials; + + public ?Aodef $remainsArgdef = null; + + function initDefs(array $defs, bool $setup=true): void { + $this->mergeParse($defs, $aobjects); + $this->aomain = $aobjects["main"] ?? null; + $this->aosections = $aobjects["sections"] ?? null; + $this->aospecials = $aobjects["specials"] ?? null; + if ($setup) $this->setup(); + } + + protected function mergeParse(array $defs, ?array &$aobjects, bool $parse=true): void { + $aobjects ??= []; + + $merges = $defs["merges"] ?? null; + $merge = $defs["merge"] ?? null; + if ($merge !== null) $merges[] = $merge; + if ($merges !== null) { + foreach ($merges as $merge) { + $this->mergeParse($merge, $aobjects, false); + $this->parse($merge, $aobjects); + } + } + + if ($parse) $this->parse($defs, $aobjects); + + $merge = $defs["merge_after"] ?? null; + if ($merge !== null) { + $this->mergeParse($merge, $aobjects, false); + $this->parse($merge, $aobjects); + } + } + + protected function parse(array $defs, array &$aobjects): void { + [$defs, $params] = cl::split_assoc($defs); + if ($defs !== null) { + $aomain =& $aobjects["main"]; + foreach ($defs as $def) { + $first = $def[0] ?? null; + if ($first === "group") { + $aobject = new Aogroup($def); + } else { + $aobject = new Aodef($def); + } + $aomain[] = $aobject; + } + } + $sections = $params["sections"] ?? null; + if ($sections !== null) { + $aosections =& $aobjects["sections"]; + $index = 0; + foreach ($sections as $key => $section) { + if ($key === $index) { + $index++; + $aosections[] = new Aosection($section); + } else { + /** @var Aosection $aosection */ + $aosection = $aosections[$key] ?? null; + if ($aosection === null) { + $aosections[$key] = new Aosection($section); + } else { + #XXX il faut implémenter la fusion en cas de section existante + # pour le moment, la liste existante est écrasée + $aosection->initDefs($section); + } + } + } + } + $this->parseParams($params); + } + + protected function parseParams(?array $params): void { + } + + function all(?array $what=null): iterable { + $returnsAodef = $what["aodef"] ?? true; + $returnsAolist = $what["aolist"] ?? false; + $returnExtends = $what["extends"] ?? false; + $withSpecials = $what["aospecials"] ?? true; + # lister les sections avant, pour que les options de la section principale + # soient prioritaires + $aosections = $this->aosections; + if ($aosections !== null) { + /** @var Aosection $aobject */ + foreach ($aosections as $aosection) { + if ($returnsAolist) { + yield $aosection; + } elseif ($returnsAodef) { + yield from $aosection->all($what); + } + } + } + + $aomain = $this->aomain; + if ($aomain !== null) { + /** @var Aodef $aobject */ + foreach ($aomain as $aobject) { + if ($aobject instanceof Aodef) { + if ($returnsAodef) { + if ($returnExtends) { + if ($aobject->isExtends()) yield $aobject; + } else { + if (!$aobject->isExtends()) yield $aobject; + } + } + } elseif ($aobject instanceof Aolist) { + if ($returnsAolist) { + yield $aobject; + } elseif ($returnsAodef) { + yield from $aobject->all($what); + } + } + } + } + + $aospecials = $this->aospecials; + if ($withSpecials && $aospecials !== null) { + /** @var Aodef $aobject */ + foreach ($aospecials as $aobject) { + yield $aobject; + } + } + } + + protected function filter(callable $callback): void { + $aomain = $this->aomain; + if ($aomain !== null) { + $filtered = []; + /** @var Aodef $aobject */ + foreach ($aomain as $aobject) { + if ($aobject instanceof Aolist) { + $aobject->filter($callback); + } + if (call_user_func($callback, $aobject)) { + $filtered[] = $aobject; + } + } + $this->aomain = $filtered; + } + $aosections = $this->aosections; + if ($aosections !== null) { + $filtered = []; + /** @var Aosection $aosection */ + foreach ($aosections as $aosection) { + $aosection->filter($callback); + if (call_user_func($callback, $aosection)) { + $filtered[] = $aosection; + } + } + $this->aosections = $filtered; + } + } + + protected function setup(): void { + # calculer les options + foreach ($this->all() as $aodef) { + $aodef->setup1(); + } + /** @var Aodef $aodef */ + foreach ($this->all(["extends" => true]) as $aodef) { + $aodef->setup1(true, $this); + } + # ne garder que les objets non vides + $this->filter(function($aobject): bool { + if ($aobject instanceof Aodef) { + return !$aobject->isEmpty(); + } elseif ($aobject instanceof Aolist) { + return !$aobject->isEmpty(); + } else { + return false; + } + }); + # puis calculer nombre d'arguments et actions + foreach ($this->all() as $aodef) { + $aodef->setup2(); + } + } + + function isEmpty(): bool { + foreach ($this->all() as $aobject) { + return false; + } + return true; + } + + function get(string $option): ?Aodef { + return null; + } + + function actionPrintHelp(string $arg): void { + $this->printHelp([ + "show_all" => $arg === "--help++", + ]); + } + + function printHelp(?array $what=null): void { + $show = $what["show_all"] ?? false; + if (!$show) $show = null; + + $aosections = $this->aosections; + if ($aosections !== null) { + /** @var Aosection $aosection */ + foreach ($aosections as $aosection) { + $aosection->printHelp(cl::merge($what, [ + "show" => $show, + "prefix" => "\n", + ])); + } + } + + $aomain = $this->aomain; + if ($aomain !== null) { + echo "\nOPTIONS\n"; + foreach ($aomain as $aobject) { + $aobject->printHelp(cl::merge($what, [ + "show" => $show, + ])); + } + } + } + + function __toString(): string { + $items = []; + $what = [ + "aodef" => true, + "aolist" => true, + ]; + foreach ($this->all($what) as $aobject) { + if ($aobject instanceof Aodef) { + $items[] = strval($aobject); + } elseif ($aobject instanceof Aogroup) { + $items[] = implode("\n", [ + "group", + str::indent(strval($aobject)), + ]); + } elseif ($aobject instanceof Aosection) { + $items[] = implode("\n", [ + "section", + str::indent(strval($aobject)), + ]); + } else { + $items[] = false; + } + } + return implode("\n", $items); + } +} diff --git a/php/src/app/args/Aosection.php b/php/src/app/args/Aosection.php new file mode 100644 index 0000000..ca85cf6 --- /dev/null +++ b/php/src/app/args/Aosection.php @@ -0,0 +1,45 @@ +show = vbool::with($params["show"] ?? true); + $this->prefix ??= $params["prefix"] ?? null; + $this->title ??= $params["title"] ?? null; + $this->description ??= $params["description"] ?? null; + $this->suffix ??= $params["suffix"] ?? null; + } + + function printHelp(?array $what=null): void { + $showSection = $what["show"] ?? $this->show; + if (!$showSection) return; + + $prefix = $what["prefix"] ?? null; + if ($prefix !== null) echo $prefix; + + if ($this->prefix) echo "{$this->prefix}\n"; + if ($this->title) echo "{$this->title}\n"; + if ($this->description) echo "\n{$this->description}\n"; + /** @var Aodef|Aolist $aobject */ + foreach ($this->all(["aolist" => true]) as $aobject) { + $aobject->printHelp(); + } + if ($this->suffix) echo "{$this->suffix}\n"; + } +} diff --git a/php/src/app/args/ArgsException.php b/php/src/app/args/ArgsException.php new file mode 100644 index 0000000..fe814e8 --- /dev/null +++ b/php/src/app/args/ArgsException.php @@ -0,0 +1,7 @@ +prefix ??= $params["prefix"] ?? null; + $this->name ??= $params["name"] ?? null; + $this->purpose ??= $params["purpose"] ?? null; + $this->usage ??= $params["usage"] ?? null; + $this->description ??= $params["description"] ?? null; + $this->suffix ??= $params["suffix"] ?? null; + + $this->commandname ??= $params["commandname"] ?? null; + $this->commandproperty ??= $params["commandproperty"] ?? null; + $this->commandkey ??= $params["commandkey"] ?? null; + + $this->argsname ??= $params["argsname"] ?? null; + $this->argsproperty ??= $params["argsproperty"] ?? null; + $this->argskey ??= $params["argskey"] ?? null; + + $this->autohelp ??= vbool::withn($params["autohelp"] ?? null); + $this->autoremains ??= vbool::withn($params["autoremains"] ?? null); + } + + /** @return string[] */ + function getOptions(): array { + return array_keys($this->index); + } + + protected function indexAodefs(): void { + $this->index = []; + foreach ($this->all() as $aodef) { + $options = $aodef->getOptions(); + foreach ($options as $option) { + /** @var Aodef $prevAodef */ + $prevAodef = $this->index[$option] ?? null; + if ($prevAodef !== null) $prevAodef->removeOption($option); + $this->index[$option] = $aodef; + } + } + } + + protected function setup(): void { + # calculer les options pour les objets déjà fusionnés + /** @var Aodef $aodef */ + foreach ($this->all() as $aodef) { + $aodef->setup1(); + } + + # puis traiter les extensions d'objets et calculer les options pour ces + # objets sur la base de l'index que l'on crée une première fois + $this->indexAodefs(); + /** @var Aodef $aodef */ + foreach ($this->all(["extends" => true]) as $aodef) { + $aodef->setup1(true, $this); + } + + # ne garder que les objets non vides + $this->filter(function($aobject) { + if ($aobject instanceof Aodef) { + return !$aobject->isEmpty(); + } elseif ($aobject instanceof Aolist) { + return !$aobject->isEmpty(); + } else { + return false; + } + }); + + # rajouter remains et help si nécessaire + $this->aospecials = []; + $helpArgdef = null; + $remainsArgdef = null; + /** @var Aodef $aodef */ + foreach ($this->all() as $aodef) { + if ($aodef->isHelp) $helpArgdef = $aodef; + if ($aodef->isRemains) $remainsArgdef = $aodef; + } + + $this->autohelp ??= true; + if ($helpArgdef === null && $this->autohelp) { + $helpArgdef = new Aodef([ + "--help", "--help++", + "action" => "--show-help", + "help" => "Afficher l'aide", + ]); + $helpArgdef->setup1(); + $this->aospecials[] = $helpArgdef; + } + + $this->autoremains ??= true; + if ($remainsArgdef === null && $this->autoremains) { + $remainsArgdef = new Aodef([ + "args" => [null], + "action" => "--set-args", + "name" => $this->argsname ?? "args", + "property" => $this->argsproperty, + "key" => $this->argskey, + ]); + $remainsArgdef->setup1(); + $this->aospecials[] = $remainsArgdef; + } + $this->remainsArgdef = $remainsArgdef; + + # puis calculer nombre d'arguments et actions + $this->indexAodefs(); + /** @var Aodef $aodef */ + foreach ($this->all() as $aodef) { + $aodef->setup2(); + } + } + + function get(string $option): ?Aodef { + return $this->index[$option] ?? null; + } + + function printHelp(?array $what = null): void { + $showList = $what["show"] ?? true; + if (!$showList) return; + + $prefix = $what["prefix"] ?? null; + if ($prefix !== null) echo $prefix; + + if ($this->prefix) echo "{$this->prefix}\n"; + if ($this->purpose) { + echo "{$this->name}: {$this->purpose}\n"; + } elseif (!$this->prefix) { + # s'il y a un préfixe sans purpose, il remplace purpose + echo "{$this->name}\n"; + } + if ($this->usage) { + echo "\nUSAGE\n"; + foreach (cl::with($this->usage) as $usage) { + echo " {$this->name} $usage\n"; + } + } + if ($this->description) echo "\n{$this->description}\n"; + parent::printHelp($what); + if ($this->suffix) echo "{$this->suffix}\n"; + } + + function __toString(): string { + return implode("\n", [ + "objects:", + str::indent(parent::__toString()), + "index:", + str::indent(implode("\n", array_keys($this->index))), + ]); + } +} diff --git a/php/src/app/args/SimpleArgsParser.php b/php/src/app/args/SimpleArgsParser.php new file mode 100644 index 0000000..cdb181a --- /dev/null +++ b/php/src/app/args/SimpleArgsParser.php @@ -0,0 +1,247 @@ +aolist = new SimpleAolist($defs); + } + + protected SimpleAolist $aolist; + + protected function getArgdef(string $option): ?Aodef { + return $this->aolist->get($option); + } + + protected function getOptions(): array { + return $this->aolist->getOptions(); + } + + function normalize(array $args): array { + $i = 0; + $max = count($args); + $options = []; + $remains = []; + $parseOpts = true; + while ($i < $max) { + $arg = $args[$i++]; + if (!$parseOpts) { + # le reste n'est que des arguments + $remains[] = $arg; + continue; + } + if ($arg === "--") { + # fin des options + $parseOpts = false; + continue; + } + + if (substr($arg, 0, 2) === "--") { + ####################################################################### + # option longue + $pos = strpos($arg, "="); + if ($pos !== false) { + # option avec valeur + $option = substr($arg, 0, $pos); + $value = substr($arg, $pos + 1); + } else { + # option sans valeur + $option = $arg; + $value = null; + } + $argdef = $this->getArgdef($option); + if ($argdef === null) { + # chercher une correspondance + $len = strlen($option); + $candidates = []; + foreach ($this->getOptions() as $candidate) { + if (substr($candidate, 0, $len) === $option) { + $candidates[] = $candidate; + } + } + switch (count($candidates)) { + case 0: throw $this->invalidArg($option); + case 1: $option = $candidates[0]; break; + default: throw $this->ambiguousArg($option, $candidates); + } + $argdef = $this->getArgdef($option); + } + + if ($argdef->haveArgs) { + $minArgs = $argdef->minArgs; + $maxArgs = $argdef->maxArgs; + $values = []; + if ($value !== null) { + $values[] = $value; + $offset = 1; + } elseif ($minArgs == 0) { + # cas particulier: la première valeur doit être collée à l'option + # si $maxArgs == 1 + $offset = $maxArgs == 1 ? 1 : 0; + } else { + $offset = 0; + } + $this->checkEnoughArgs($option, + self::consume_args($args, $i, $values, $offset, $minArgs, $maxArgs, true)); + + if ($minArgs == 0 && $maxArgs == 1) { + # cas particulier: la première valeur doit être collée à l'option + if (count($values) > 0) { + $options[] = "$option=$values[0]"; + $values = array_slice($values, 1); + } else { + $options[] = $option; + } + } else { + $options[] = $option; + } + $options = array_merge($options, $values); + } elseif ($value !== null) { + throw $this->tooManyArgs(1, 0, $option); + } else { + $options[] = $option; + } + + } elseif (substr($arg, 0, 1) === "-") { + ####################################################################### + # option courte + $pos = 1; + $len = strlen($arg); + while ($pos < $len) { + $option = "-".substr($arg, $pos, 1); + $argdef = $this->getArgdef($option); + if ($argdef === null) throw $this->invalidArg($option); + if ($argdef->haveArgs) { + $minArgs = $argdef->minArgs; + $maxArgs = $argdef->maxArgs; + $values = []; + if ($len > $pos + 1) { + $values[] = substr($arg, $pos + 1); + $offset = 1; + $pos = $len; + } elseif ($minArgs == 0) { + # cas particulier: la première valeur doit être collée à l'option + # si $maxArgs == 1 + $offset = $maxArgs == 1 ? 1 : 0; + } else { + $offset = 0; + } + $this->checkEnoughArgs($option, + self::consume_args($args, $i, $values, $offset, $minArgs, $maxArgs, true)); + + if ($minArgs == 0 && $maxArgs == 1) { + # cas particulier: la première valeur doit être collée à l'option + if (count($values) > 0) { + $options[] = "$option$values[0]"; + $values = array_slice($values, 1); + } else { + $options[] = $option; + } + } else { + $options[] = $option; + } + $options = array_merge($options, $values); + } else { + $options[] = $option; + } + $pos++; + } + } else { + #XXX implémenter les commandes + + ####################################################################### + # argument + $remains[] = $arg; + } + } + return array_merge($options, ["--"], $remains); + } + + function process(array $args) { + $i = 0; + $max = count($args); + # d'abord traiter les options + while ($i < $max) { + $arg = $args[$i++]; + if ($arg === "--") { + # fin des options + break; + } + + if (preg_match('/^(--[^=]+)(?:=(.*))?/', $arg, $ms)) { + # option longue + } elseif (preg_match('/^(-.)(.+)?/', $arg, $ms)) { + # option courte + } else { + # commande + throw StateException::unexpected_state("commands are not supported"); + } + $option = $ms[1]; + $ovalue = $ms[2] ?? null; + $argdef = $this->getArgdef($option); + if ($argdef === null) throw StateException::unexpected_state(); + $defvalue = $argdef->value; + if ($argdef->haveArgs) { + $minArgs = $argdef->minArgs; + $maxArgs = $argdef->maxArgs; + if ($minArgs == 0 && $maxArgs == 1) { + # argument facultatif + if ($ovalue !== null) $value = [$ovalue]; + else $value = cl::with($defvalue); + $offset = 1; + } else { + $value = []; + $offset = 0; + } + self::consume_args($args, $i, $value, $offset, $minArgs, $maxArgs, false); + } else { + $value = $defvalue; + } + + $this->action($value, $arg, $argdef); + } + + # construire la liste des arguments qui restent + $args = array_slice($args, $i); + $i = 0; + $max = count($args); + $argdef = $this->aolist->remainsArgdef; + if ($argdef !== null && $argdef->haveArgs) { + $minArgs = $argdef->minArgs; + $maxArgs = $argdef->maxArgs; + if ($maxArgs == PHP_INT_MAX) { + # cas particulier: si le nombre d'arguments restants est non borné, + # les prendre tous sans distinction ni traitement de '--' + $value = $args; + # mais tester tout de même s'il y a le minimum requis d'arguments + $this->checkEnoughArgs(null, $minArgs - $max); + } else { + $value = []; + $this->checkEnoughArgs(null, + self::consume_args($args, $i, $value, 0, $minArgs, $maxArgs, false)); + if ($i <= $max - 1) throw $this->tooManyArgs($max, $i); + } + $this->action($value, null, $argdef); + } elseif ($i <= $max - 1) { + throw $this->tooManyArgs($max, $i); + } + } + + function action($value, ?string $arg, Aodef $argdef) { + $argdef->action($this->dest, $value, $arg, $this); + } + + public function actionPrintHelp(string $arg): void { + $this->aolist->actionPrintHelp($arg); + throw new ExitError(0); + } + + function showDebugInfos() { + echo $this->aolist."\n"; #XXX + } +} diff --git a/php/src/app/args/TODO.md b/php/src/app/args/TODO.md new file mode 100644 index 0000000..da6b1ad --- /dev/null +++ b/php/src/app/args/TODO.md @@ -0,0 +1,20 @@ +# nulib\app\args + +* [ ] transformer un schéma en définition d'arguments, un tableau en liste d'arguments, et vice-versa +* [ ] faire une implémentation ArgsParser qui supporte les commandes, et les options dynamiques + * commandes: + `program [options] command [options]` + * multi-commandes: + `program [options] command [options] // command [options] // ...` + * dynamique: la liste des options et des commandes supportées est calculée dynamiquement + +## support des commandes + +faire une interface Runnable qui représente un composant pouvant être exécuté. +Application implémente Runnable, mais l'analyse des arguments peut retourner une +autre instance de runnable pour faciliter l'implémentation de différents +sous-outils + +## BUGS + +-*- coding: utf-8 mode: markdown -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8:noeol:binary \ No newline at end of file diff --git a/php/src/app/args/_exceptions.php b/php/src/app/args/_exceptions.php new file mode 100644 index 0000000..aa8104f --- /dev/null +++ b/php/src/app/args/_exceptions.php @@ -0,0 +1,10 @@ +getDesc(); + echo implode("\n", $desc["message"])."\n"; + $ec = $desc["exitcode"] ?? 0; + break; + case "dump": + case "d": + yaml::dump($runfile->read()); + break; + case "reset": + case "z": + if (!$runfile->isRunning()) $runfile->reset(); + else $ec = self::_error("cannot reset while running"); + break; + case "release": + case "rl": + $runfile->release(); + break; + case "start": + case "s": + array_splice($argv, 1, 1); $argc--; + return; + case "kill": + case "k": + if ($runfile->isRunning()) $runfile->wfKill(); + else $ec = self::_error("not running"); + break; + default: + $ec = self::_error("$argv[1]: unexpected command", app::EC_BAD_COMMAND); + } + exit($ec); + } + + static function run(?Application $app=null): void { + $unlock = false; + $stop = false; + $shutdown = function () use (&$unlock, &$stop) { + if ($unlock) { + app::get()->getRunfile()->release(); + $unlock = false; + } + if ($stop) { + app::get()->getRunfile()->wfStop(); + $stop = false; + } + }; + register_shutdown_function($shutdown); + app::install_signal_handler(static::INSTALL_SIGNAL_HANDLER); + try { + static::_initialize_app(); + $useRunfile = static::USE_RUNFILE; + $useRunlock = static::USE_RUNLOCK; + if ($useRunfile) { + $runfile = app::get()->getRunfile(); + + global $argc, $argv; + self::_manage_runfile($argc, $argv, $runfile); + if ($useRunlock && $runfile->warnIfLocked()) exit(app::EC_LOCKED); + + $runfile->wfStart(); + $stop = true; + if ($useRunlock) { + $runfile->lock(); + $unlock = true; + } + } + if ($app === null) $app = new static(); + static::_configure_app($app); + static::_start_app($app); + } catch (ExitError $e) { + if ($e->haveUserMessage()) msg::error($e->getUserMessage()); + exit($e->getCode()); + } catch (Exception $e) { + msg::error($e); + exit(app::EC_UNEXPECTED); + } + } + + protected static function _initialize_app(): void { + app::init(static::class); + app::set_fact(app::FACT_CLI_APP); + $con = new ConsoleMessenger([ + "min_level" => msg::DEBUG, + ]); + say::set_messenger($con); + msg::set_messenger($con); + } + + protected static function _configure_app(Application $app): void { + config::configure(config::CONFIGURE_INITIAL_ONLY); + + $con = con::set_messenger(new ConsoleMessenger([ + "min_level" => con::NORMAL, + ])); + say::set_messenger($con, true); + msg::set_messenger($con, true); + if (static::USE_LOGFILE) { + $log = log::set_messenger(new LogMessenger([ + "output" => app::get()->getLogfile(), + "min_level" => msg::MINOR, + ])); + } else { + $log = log::set_messenger(new ProxyMessenger()); + } + msg::set_messenger($log); + + $app->parseArgs(); + config::configure(); + } + + protected static function _start_app(Application $app): void { + $retcode = $app->main(); + if (is_int($retcode)) exit($retcode); + elseif (is_bool($retcode)) exit($retcode? 0: 1); + elseif ($retcode !== null) exit(strval($retcode)); + } + + /** + * sortir de l'application avec un code d'erreur, qui est 0 par défaut (i.e + * pas d'erreur) + * + * équivalent à lancer l'exception {@link ExitError} + */ + protected static final function exit(int $exitcode=0, $message=null) { + throw new ExitError($exitcode, $message); + } + + /** + * sortir de l'application avec un code d'erreur, qui vaut 1 par défaut (i.e + * une erreur s'est produite) + * + * équivalent à lancer l'exception {@link ExitError} + */ + protected static final function die($message=null, int $exitcode=1) { + throw new ExitError($exitcode, $message); + } + + const PROFILE_SECTION = [ + "title" => "PROFIL D'EXECUTION", + "show" => false, + ["-c", "--config", "--app-config", + "args" => "file", "argsdesc" => "CONFIG.yml", + "action" => [config::class, "load_config"], + "help" => "spécifier un fichier de configuration", + ], + ["group", + ["-g", "--profile", "--app-profile", + "args" => 1, "argsdesc" => "PROFILE", + "action" => [app::class, "set_profile"], + "help" => "spécifier le profil d'exécution", + ], + ["-P", "--prod", "action" => [app::class, "set_profile", ref_profiles::PROD]], + ["-T", "--test", "action" => [app::class, "set_profile", ref_profiles::TEST]], + ["--devel", "action" => [app::class, "set_profile", ref_profiles::DEVEL]], + ], + ]; + + const VERBOSITY_SECTION = [ + "title" => "NIVEAU D'INFORMATION", + "show" => false, + ["group", + ["-V", "--verbosity", + "args" => "verbosity", "argsdesc" => "silent|quiet|verbose|debug", + "action" => [con::class, "set_verbosity"], + "help" => "Spécifier le niveau d'informations affiché sur la console", + ], + ["-q", "--quiet", "action" => [con::class, "set_verbosity", "quiet"]], + ["-v", "--verbose", "action" => [con::class, "set_verbosity", "verbose"]], + ["-D", "--debug", "action" => [con::class, "set_verbosity", "debug"]], + ], + ["group", + ["--color", + "action" => [con::class, "set_color", true], + "help" => "Afficher (resp. ne pas afficher) la sortie en couleur par défaut", + ], + ["--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"]], + ], + ]; + + const ARGS = [ + "sections" => [ + self::PROFILE_SECTION, + self::VERBOSITY_SECTION, + ], + ]; + + protected function getArgsParser(): AbstractArgsParser { + return new SimpleArgsParser(static::ARGS); + } + + /** @throws ArgsException */ + function parseArgs(array $args=null): void { + $this->getArgsParser()->parse($this, $args); + } + + const PROFILE_COLORS = [ + ref_profiles::PROD => "@r", + ref_profiles::TEST => "@g", + ref_profiles::DEVEL => "@w", + ]; + const DEFAULT_PROFILE_COLOR = "y"; + + /** retourner le profil courant en couleur */ + static function get_profile(?string $profile=null): string { + if ($profile === null) $profile = app::get_profile(); + foreach (static::PROFILE_COLORS as $text => $color) { + if (strpos($profile, $text) !== false) { + return $color? "$profile": $profile; + } + } + $color = static::DEFAULT_PROFILE_COLOR; + return $color? "$profile": $profile; + } + + protected ?array $args = null; + + abstract function main(); + + static function runfile(): RunFile { + return app::with(static::class)->getRunfile(); + } +} diff --git a/php/src/app/cli/include-launcher.php b/php/src/app/cli/include-launcher.php index 0958cb7..d0d04b5 100644 --- a/php/src/app/cli/include-launcher.php +++ b/php/src/app/cli/include-launcher.php @@ -3,7 +3,7 @@ # les constantes suivantes doivent être définies AVANT de chager ce script: # - NULIB_APP_app_params : paramètres du projet -use nulib\app; +use nulib\app\app; use nulib\os\path; if ($argc <= 1) die("invalid arguments"); diff --git a/php/src/app/config.php b/php/src/app/config.php new file mode 100644 index 0000000..6fd270a --- /dev/null +++ b/php/src/app/config.php @@ -0,0 +1,56 @@ +addConfigurator($configurators); + } + + # certains types de configurations sont normalisés + /** ne configurer que le minimum pour que l'application puisse s'initialiser */ + const CONFIGURE_INITIAL_ONLY = ["include" => "initial"]; + /** ne configurer que les routes */ + const CONFIGURE_ROUTES_ONLY = ["include" => "routes"]; + /** configurer uniquement ce qui ne nécessite pas d'avoir une session */ + const CONFIGURE_NO_SESSION = ["exclude" => "session"]; + + static function configure(?array $params=null): void { + self::$config->configure($params); + } + + static final function add($config, string ...$profiles): void { self::$config->addConfig($config, $profiles); } + static final function load_config($file): void { + $ext = path::ext($file); + if ($ext === ".yml" || $ext === ".yaml") { + $config = new YamlConfig($file); + } elseif ($ext === ".json") { + $config = new JsonConfig($file); + } else { + throw exceptions::invalid_value($file, "config file"); + } + self::add($config); + } + + static final function get(string $pkey, $default=null, ?string $profile=null) { return self::$config->getValue($pkey, $default, $profile); } + static final function k(string $pkey, $default=null) { return self::$config->getValue("app.$pkey", $default); } + static final function db(string $pkey, $default=null) { return self::$config->getValue("dbs.$pkey", $default); } + static final function m(string $pkey, $default=null) { return self::$config->getValue("msgs.$pkey", $default); } + static final function l(string $pkey, $default=null) { return self::$config->getValue("mails.$pkey", $default); } +} + +new class extends config { + function __construct() { + self::$config = new ConfigManager(); + } +}; \ No newline at end of file diff --git a/php/src/app/config/ArrayConfig.php b/php/src/app/config/ArrayConfig.php new file mode 100644 index 0000000..6a02c8e --- /dev/null +++ b/php/src/app/config/ArrayConfig.php @@ -0,0 +1,50 @@ +APP(); break; + case "dbs": $default = $this->DBS(); break; + case "msgs": $default = $this->MSGS(); break; + case "mails": $default = $this->MAILS(); break; + default: $default = []; + } + $config[$key] ??= $default; + } + $this->config = $config; + } + + protected array $config; + + function has(string $pkey, string $profile): bool { + return cl::phas($this->config, $pkey); + } + + function get(string $pkey, string $profile) { + return cl::pget($this->config, $pkey); + } + + function set(string $pkey, $value, string $profile): void { + cl::pset($this->config, $pkey, $value); + } +} diff --git a/php/src/app/config/ConfigManager.php b/php/src/app/config/ConfigManager.php new file mode 100644 index 0000000..d2b2ec2 --- /dev/null +++ b/php/src/app/config/ConfigManager.php @@ -0,0 +1,148 @@ +configurators, cl::with($configurators)); + } + + protected array $configured = []; + + /** + * configurer les objets et les classes qui ne l'ont pas encore été. la liste + * des objets et des classes à configurer est fournie en appelant la méthode + * {@link addConfigurator()} + * + * par défaut, la configuration se fait en appelant toutes les méthodes + * publiques des objets et toutes les méthodes statiques des classes qui + * commencent par 'configure', e.g 'configureThis()' ou 'configure_db()', + * si elles n'ont pas déjà été appelées + * + * Il est possible de modifier la liste des méthodes appelées avec le tableau + * $params, qui doit être conforme au schema de {@link func::CALL_ALL_SCHEMA} + */ + function configure(?array $params=null): void { + $params["prefix"] ??= "configure"; + foreach ($this->configurators as $key => $configurator) { + $configured =& $this->configured[$key]; + /** @var func[] $methods */ + $methods = func::get_all($configurator, $params); + foreach ($methods as $method) { + $name = $method->getName() ?? "(no name)"; + $done = $configured[$name] ?? false; + if (!$done) { + $method->invoke(); + $configured[$name] = true; + } + } + } + } + + ############################################################################# + + protected $cache = []; + + protected function resetCache(): void { + $this->cache = []; + } + + protected function cacheHas(string $pkey, string $profile) { + return array_key_exists("$profile.$pkey", $this->cache); + } + + protected function cacheGet(string $pkey, string $profile) { + return cl::get($this->cache, "$profile.$pkey"); + } + + protected function cacheSet(string $pkey, $value, string $profile): void { + $this->cache["$profile.$pkey"] = $value; + } + + protected array $profileConfigs = []; + + /** + * Ajouter une configuration valide pour le(s) profil(s) spécifié(s) + * + * $config est un objet ou une classe qui définit une ou plusieurs des + * constantes APP, DBS, MSGS, MAILS + * + * si !$inProfiles, la configuration est valide dans tous les profils + */ + function addConfig($config, ?array $inProfiles=null): void { + if (is_string($config)) { + $c = new ReflectionClass($config); + if ($c->implementsInterface(IConfig::class)) { + $config = $c->newInstance(); + } else { + $config = []; + foreach (IConfig::CONFIG_KEYS as $key) { + $config[$key] = cl::with($c->getConstant(strtoupper($key))); + } + $config = new ArrayConfig($config); + } + } elseif (is_array($config)) { + $config = new ArrayConfig($config); + } elseif (!($config instanceof IConfig)) { + throw exceptions::invalid_type($config, "config", ["array", IConfig::class]); + } + + if (!$inProfiles) $inProfiles = [IConfig::PROFILE_ALL]; + foreach ($inProfiles as $profile) { + $this->profileConfigs[$profile][] = $config; + } + + $this->resetCache(); + } + + function _getValue(string $pkey, $default, string $inProfile) { + $profiles = [$inProfile]; + if ($inProfile !== IConfig::PROFILE_ALL) $profiles[] = IConfig::PROFILE_ALL; + foreach ($profiles as $profile) { + /** @var IConfig[] $configs */ + $configs = $this->profileConfigs[$profile] ?? []; + foreach (array_reverse($configs) as $config) { + if ($config->has($pkey, $profile)) { + return $config->get($pkey, $profile); + } + } + } + return $default; + } + + /** + * obtenir la valeur au chemin de clé $pkey dans le profil spécifié + * + * le $inProfile===null, prendre le profil par défaut. + */ + function getValue(string $pkey, $default=null, ?string $inProfile=null) { + $inProfile ??= app::get_profile(); + + if ($this->cacheHas($pkey, $inProfile)) { + return $this->cacheGet($pkey, $inProfile); + } + + $value = $this->_getValue($pkey, $default, $inProfile); + $this->cacheSet($pkey, $value, $inProfile); + return $value; + } + + function setValue(string $pkey, $value, ?string $inProfile=null): void { + $inProfile ??= app::get_profile(); + /** @var IConfig[] $configs */ + $configs =& $this->profileConfigs[$inProfile]; + if ($configs === null) $key = 0; + else $key = array_key_last($configs); + $configs[$key] ??= new ArrayConfig([]); + $configs[$key]->set($pkey, $value, $inProfile); + } +} diff --git a/php/src/app/config/EnvConfig.php b/php/src/app/config/EnvConfig.php new file mode 100644 index 0000000..b07bf47 --- /dev/null +++ b/php/src/app/config/EnvConfig.php @@ -0,0 +1,112 @@ + "mysql", "name" => "mysql:host=authdb;dbname=auth;charset=utf8", + * "user" => "auth_int", "pass" => "auth" ] + * situé au chemin de clé dbs.auth dans le profil prod, on peut par exemple + * définir les variables suivantes: + * CONFIG_prod_dbs__auth__type="mysql" + * CONFIG_prod_dbs__auth__name="mysql:host=authdb;dbname=auth;charset=utf8" + * CONFIG_prod_dbs__auth__user="auth_int" + * CONFIG_prod_dbs__auth__pass="auth" + * ou alternativement: + * JSON_CONFIG_prod_dbs__auth='{"type":"mysql","name":"mysql:host=authdb;dbname=auth;charset=utf8","user":"auth_int","pass":"auth"}' + * + * Les préfixes supportés sont, dans l'ordre de précédence: + * - JSON_FILE_CONFIG -- une valeur au format JSON inscrite dans un fichier + * - JSON_CONFIG -- une valeur au format JSON + * - FILE_CONFIG -- une valeur inscrite dans un fichier + * - CONFIG -- une valeur scalaire + */ +class EnvConfig implements IConfig{ + protected ?array $profileConfigs = null; + + /** analyser $name et retourner [$pkey, $profile] */ + private static function parse_pkey_profile($name): array { + $i = strpos($name, "_"); + if ($i === false) return [false, false]; + $profile = substr($name, 0, $i); + if ($profile === "ALL") $profile = IConfig::PROFILE_ALL; + $name = substr($name, $i + 1); + $pkey = str_replace("__", ".", $name); + return [$pkey, $profile]; + } + + function loadEnvConfig(): void { + if ($this->profileConfigs !== null) return; + $json_files = []; + $jsons = []; + $files = []; + $vars = []; + foreach (getenv() as $name => $value) { + if (str::starts_with("JSON_FILE_CONFIG_", $name)) { + $json_files[str::without_prefix("JSON_FILE_CONFIG_", $name)] = $value; + } elseif (str::starts_with("JSON_CONFIG_", $name)) { + $jsons[str::without_prefix("JSON_CONFIG_", $name)] = $value; + } elseif (str::starts_with("FILE_CONFIG_", $name)) { + $files[str::without_prefix("FILE_CONFIG_", $name)] = $value; + } elseif (str::starts_with("CONFIG_", $name)) { + $vars[str::without_prefix("CONFIG_", $name)] = $value; + } + } + $profileConfigs = []; + foreach ($json_files as $name => $file) { + [$pkey, $profile] = self::parse_pkey_profile($name); + $value = json::load($file); + cl::pset($profileConfigs, "$profile.$pkey", $value); + } + foreach ($jsons as $name => $json) { + [$pkey, $profile] = self::parse_pkey_profile($name); + $value = json::decode($json); + cl::pset($profileConfigs, "$profile.$pkey", $value); + } + foreach ($files as $name => $file) { + [$pkey, $profile] = self::parse_pkey_profile($name); + $value = file::reader($file)->getContents(); + cl::pset($profileConfigs, "$profile.$pkey", $value); + } + foreach ($vars as $name => $value) { + [$pkey, $profile] = self::parse_pkey_profile($name); + cl::pset($profileConfigs, "$profile.$pkey", $value); + } + $this->profileConfigs = $profileConfigs; + } + + function has(string $pkey, string $profile): bool { + $this->loadEnvConfig(); + $config = $this->profileConfigs[$profile] ?? null; + return cl::phas($config, $pkey); + } + + function get(string $pkey, string $profile) { + $this->loadEnvConfig(); + $config = $this->profileConfigs[$profile] ?? null; + return cl::pget($config, $pkey); + } + + function set(string $pkey, $value, string $profile): void { + $this->loadEnvConfig(); + $config =& $this->profileConfigs[$profile]; + cl::pset($config, $pkey, $value); + } +} diff --git a/php/src/app/config/IConfig.php b/php/src/app/config/IConfig.php new file mode 100644 index 0000000..dcba89f --- /dev/null +++ b/php/src/app/config/IConfig.php @@ -0,0 +1,24 @@ + profil effectif + * + * ce mapping est utilisé quand il faut calculer le profil courant s'il n'a + * pas été spécifié par l'utilisateur. il permet de faire correspondre le + * profil courant de l'application avec le profil effectif à sélectionner + */ + const PROFILE_MAP = null; + + function __construct(?array $params=null) { + $this->isAppProfile = $params["app"] ?? false; + $this->profiles = static::PROFILES; + $this->productionModes = static::PRODUCTION_MODES; + $this->profileMap = static::PROFILE_MAP; + $name = $params["name"] ?? static::NAME; + if ($name === null) { + $this->configKey = null; + $this->envKeys = ["APP_PROFILE"]; + } else { + $configKey = "${name}_profile"; + $envKey = strtoupper($configKey); + if ($this->isAppProfile) { + $this->configKey = null; + $this->envKeys = [$envKey, "APP_PROFILE"]; + } else { + $this->configKey = $configKey; + $this->envKeys = [$envKey]; + } + } + $this->defaultProfile = $params["default_profile"] ?? null; + $profile = $params["profile"] ?? null; + $productionMode = $params["production_mode"] ?? null; + $productionMode ??= $this->productionModes[$profile] ?? false; + $this->profile = $profile; + $this->productionMode = $productionMode; + } + + /** + * @var bool cet objet est-il utilisé pour gérer le profil de l'application? + */ + protected bool $isAppProfile; + + protected ?array $profiles; + + protected ?array $productionModes; + + protected ?array $profileMap; + + protected function mapProfile(?string $profile): ?string { + return $this->profileMap[$profile] ?? $profile; + } + + protected ?string $configKey; + + function getConfigProfile(): ?string { + if ($this->configKey === null) return null; + return config::k($this->configKey); + } + + protected array $envKeys; + + function getEnvProfile(): ?string { + foreach ($this->envKeys as $envKey) { + $profile = getenv($envKey); + if ($profile !== false) return $profile; + } + return null; + } + + protected ?string $defaultProfile; + + function getDefaultProfile(): ?string { + return $this->defaultProfile; + } + + function setDefaultProfile(?string $profile): void { + $this->defaultProfile = $profile; + } + + protected ?string $profile; + + protected bool $productionMode; + + protected function resolveProfile(): void { + $profile ??= $this->getenvProfile(); + $profile ??= $this->getConfigProfile(); + $profile ??= $this->getDefaultProfile(); + if ($this->isAppProfile) { + $profile ??= $this->profiles[0] ?? ref_profiles::PROD; + } else { + $profile ??= $this->mapProfile(app::get_profile()); + } + $this->profile = $profile; + $this->productionMode = $this->productionModes[$profile] ?? false; + } + + function getProfile(?bool &$productionMode=null): string { + if ($this->profile === null) $this->resolveProfile(); + $productionMode = $this->productionMode; + return $this->profile; + } + + function isProductionMode(): bool { + return $this->productionMode; + } + + function setProfile(?string $profile=null, ?bool $productionMode=null): void { + if ($profile === null) $this->profile = null; + $profile ??= $this->getProfile($productionMode); + $productionMode ??= $this->productionModes[$profile] ?? false; + $this->profile = $profile; + $this->productionMode = $productionMode; + } +} diff --git a/php/src/app/config/YamlConfig.php b/php/src/app/config/YamlConfig.php new file mode 100644 index 0000000..e248c9c --- /dev/null +++ b/php/src/app/config/YamlConfig.php @@ -0,0 +1,13 @@ +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(); +} diff --git a/php/src/cache/CacheFile.php b/php/src/cache/CacheFile.php new file mode 100644 index 0000000..ea738d7 --- /dev/null +++ b/php/src/cache/CacheFile.php @@ -0,0 +1,356 @@ +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); + } +} diff --git a/php/src/cache/CacheManager.php b/php/src/cache/CacheManager.php new file mode 100644 index 0000000..2b134c9 --- /dev/null +++ b/php/src/cache/CacheManager.php @@ -0,0 +1,68 @@ +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; + } +} diff --git a/php/src/cache/CursorCacheData.php b/php/src/cache/CursorCacheData.php new file mode 100644 index 0000000..c0b402d --- /dev/null +++ b/php/src/cache/CursorCacheData.php @@ -0,0 +1,40 @@ +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); + } +} diff --git a/php/src/cache/CursorChannel.php b/php/src/cache/CursorChannel.php new file mode 100644 index 0000000..1af52b1 --- /dev/null +++ b/php/src/cache/CursorChannel.php @@ -0,0 +1,127 @@ +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; + } + } +} diff --git a/php/src/cache/DataCacheData.php b/php/src/cache/DataCacheData.php new file mode 100644 index 0000000..fa85ce6 --- /dev/null +++ b/php/src/cache/DataCacheData.php @@ -0,0 +1,62 @@ +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); + } +} diff --git a/php/src/cache/TODO.md b/php/src/cache/TODO.md new file mode 100644 index 0000000..e8c41c0 --- /dev/null +++ b/php/src/cache/TODO.md @@ -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 \ No newline at end of file diff --git a/php/src/cache/cache.php b/php/src/cache/cache.php new file mode 100644 index 0000000..ed599fa --- /dev/null +++ b/php/src/cache/cache.php @@ -0,0 +1,93 @@ +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); + } +} diff --git a/php/src/cl.php b/php/src/cl.php index f0919ea..2e57a87 100644 --- a/php/src/cl.php +++ b/php/src/cl.php @@ -848,7 +848,7 @@ class cl { static final function any_not_same(?array $array, $value): bool { return self::any_if($array, cv::Fnot_same($value)); } #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - + static final function filter_if(?array $array, callable $cond): ?array { if ($array === null) return null; $filtered = []; @@ -923,4 +923,48 @@ class cl { A::usort($array, $keys, $assoc); return $array; } + + ############################################################################# + + /** + * Extraire d'un tableau les clés séquentielles et les clés associatives + * + * Retourner une liste [$list, $assoc] où $list est un tableau avec uniquement + * les valeurs des clés séquentielles et $assoc est un tableau avec uniquement + * les valeurs des clés associatives. S'il n'existe aucune clé séquentielle + * (resp. aucune clé associative), $list (resp. $assoc) vaut null. + * + * Par exemple: split_assoc(["a", "b" => "c"]) retourne [["a"], ["b" => "c"]] + */ + static final function split_assoc(?array $array): array { + $list = null; + $assoc = null; + if ($array !== null) { + $i = 0; + foreach ($array as $key => $value) { + if ($key === $i) { + $list[] = $value; + $i++; + } else { + $assoc[$key] = $value; + } + } + } + return [$list, $assoc]; + } + + /** + * Joindre en un seul tableau un tableau avec des clés séquentielles et un + * tableau avec des clés associatives. + * + * Si $list_first==true, les clés séquentielles arrivent d'abord, ensuite les + * clés associatives. Sinon, ce sont les clés associatives qui arrivent d'abord + */ + static final function merge_assoc(?array &$array, ?array $list, ?array $assoc, bool $list_first=false): void { + if ($list === null && $assoc === null) $array = []; + elseif ($list === null) $array = $assoc; + elseif ($assoc === null) $array = $list; + elseif ($list_first) $array = array_merge($list, $assoc); + else $array = array_merge($assoc, $list); + } } diff --git a/php/src/cv.php b/php/src/cv.php index ac3dbac..89b81ac 100644 --- a/php/src/cv.php +++ b/php/src/cv.php @@ -29,7 +29,7 @@ class cv { static final function t($value): bool { return $value || $value === "0"; } - + /** tester si $value est fausse (cela n'inclue pas la chaine "0") */ static final function f($value): bool { return !$value && $value !== "0"; @@ -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 */ static final function check_bool($value): ?bool { return is_bool($value)? $value: null; @@ -192,11 +198,11 @@ class cv { * * 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; $key = is_string($value)? $value : null; - if ($index === null && $key === null && $throw_exception) { - throw ValueException::invalid_kind($value, "key", $prefix); + if ($index === null && $key === null && $throwException) { + throw exceptions::invalid_type($value, $kind, "key"); } else { return [$index, $key]; } @@ -208,12 +214,12 @@ class cv { * * @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; $scalar = !is_bool($value) && is_scalar($value)? $value : null; $array = is_array($value)? $value : null; - if ($bool === null && $scalar === null && $array === null && $throw_exception) { - throw ValueException::invalid_kind($value, "value", $prefix); + if ($bool === null && $scalar === null && $array === null && $throwException) { + throw exceptions::invalid_type($value, $kind, ["bool", "scalar", "array"]); } else { return [$bool, $scalar, $array]; } diff --git a/php/src/db/Capacitor.php b/php/src/db/Capacitor.php index 8fb2403..1ed0db7 100644 --- a/php/src/db/Capacitor.php +++ b/php/src/db/Capacitor.php @@ -1,10 +1,9 @@ subChannels[] = $channel; } else { - throw ValueException::invalid_type($channel, CapacitorChannel::class); + throw exceptions::invalid_type($channel, "channel", CapacitorChannel::class); } } } diff --git a/php/src/db/CapacitorStorage.php b/php/src/db/CapacitorStorage.php index dbc7f87..b812408 100644 --- a/php/src/db/CapacitorStorage.php +++ b/php/src/db/CapacitorStorage.php @@ -5,8 +5,8 @@ use nulib\A; use nulib\cl; use nulib\cv; use nulib\db\_private\_migration; +use nulib\exceptions; use nulib\php\func; -use nulib\ValueException; use Traversable; /** @@ -17,7 +17,7 @@ abstract class CapacitorStorage { abstract function db(): IDatabase; function ensureLive(): self { - $this->db()->ensure(); + $this->db()->ensureLive(); return $this; } @@ -596,7 +596,7 @@ abstract class CapacitorStorage { * 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 ValueException::null("filter"); + if ($filter === null) throw exceptions::null_value("filter"); $this->_create($channel); $this->verifixFilter($channel, $filter); $raw = $this->db()->one(cl::merge([ diff --git a/php/src/db/IDatabase.php b/php/src/db/IDatabase.php index 32a0013..5ca2e54 100644 --- a/php/src/db/IDatabase.php +++ b/php/src/db/IDatabase.php @@ -17,7 +17,7 @@ interface IDatabase extends ITransactor { * transactions en cours sont perdues. cette méthode est donc prévue pour * vérifier la validité de la connexion avant de lancer une transaction */ - function ensure(): self; + function ensureLive(): self; /** * - si c'est un insert, retourner l'identifiant autogénéré de la ligne diff --git a/php/src/db/TODO.md b/php/src/db/TODO.md index c7004a6..1f03e6d 100644 --- a/php/src/db/TODO.md +++ b/php/src/db/TODO.md @@ -1,7 +1,29 @@ # db/Capacitor -* charge() permet de spécifier la clé associée avec la valeur chargée, et - discharge() retourne les valeurs avec la clé primaire -* chargeAll() (ou peut-être chargeFrom()) permet de charger depuis un iterable +charge() permet de spécifier la clé associée avec la valeur chargée, et +discharge() retourne les valeurs avec la clé primaire + +--- + +chargeAll() (ou peut-être chargeFrom()) permet de charger depuis un iterable + +--- + +rendre obsolète la classe Capacitor: ne garder que CapacitorChannel et +CapacitorStorage + +--- + +constante de classe AUTO_MIGRATE valant par défaut null + +false: ne jamais faire de migration: assumer que la table existe avec les bonnes +colonnes + +true: toujours chercher à faire la migration + +null: calculer la valeur en fonction du profil courant: true pour devel, false +sinon + + -*- coding: utf-8 mode: markdown -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8:noeol:binary \ No newline at end of file diff --git a/php/src/db/_private/_base.php b/php/src/db/_private/_base.php index 2ca42f9..8325bae 100644 --- a/php/src/db/_private/_base.php +++ b/php/src/db/_private/_base.php @@ -1,14 +1,14 @@ "create", "type" => "ddl"]; @@ -28,7 +28,7 @@ abstract class _base extends _common { $sql = _generic::parse($sql, $bindings); $meta = ["isa" => "generic", "type" => null]; } else { - throw ValueException::invalid_kind($sql, "query"); + throw exceptions::invalid_value($sql, "cette requête sql"); } } else { if (!is_string($sql)) $sql = strval($sql); diff --git a/php/src/db/_private/_common.php b/php/src/db/_private/_common.php index 575a53b..69b93ab 100644 --- a/php/src/db/_private/_common.php +++ b/php/src/db/_private/_common.php @@ -2,8 +2,8 @@ namespace nulib\db\_private; use nulib\cl; +use nulib\exceptions; use nulib\str; -use nulib\ValueException; class _common { 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 { self::consume(';\s*', $tmpsql); if ($tmpsql) { - throw new ValueException("unexpected value at end: $usersql"); + throw exceptions::invalid_value($usersql, "cette requête sql"); } } } diff --git a/php/src/db/_private/_insert.php b/php/src/db/_private/_insert.php index eb54980..a4fe118 100644 --- a/php/src/db/_private/_insert.php +++ b/php/src/db/_private/_insert.php @@ -2,7 +2,7 @@ namespace nulib\db\_private; use nulib\cl; -use nulib\ValueException; +use nulib\exceptions; class _insert extends _common { const SCHEMA = [ @@ -44,7 +44,7 @@ class _insert extends _common { } elseif ($into !== null) { $sql[] = $into; } 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 diff --git a/php/src/db/_private/_select.php b/php/src/db/_private/_select.php index 0dc0f0b..4fdc3ff 100644 --- a/php/src/db/_private/_select.php +++ b/php/src/db/_private/_select.php @@ -2,8 +2,8 @@ namespace nulib\db\_private; use nulib\cl; +use nulib\exceptions; use nulib\str; -use nulib\ValueException; class _select extends _common { const SCHEMA = [ @@ -101,7 +101,7 @@ class _select extends _common { $sql[] = "from"; $sql[] = $from; } else { - throw new ValueException("expected table name: $usersql"); + throw exceptions::invalid_value($usersql, "cette requête sql", "il faut spécifier la table"); } ## where diff --git a/php/src/db/mysql/Mysql.php b/php/src/db/mysql/Mysql.php index f0a0e75..52b93d6 100644 --- a/php/src/db/mysql/Mysql.php +++ b/php/src/db/mysql/Mysql.php @@ -6,11 +6,22 @@ use nulib\db\pdo\Pdo; class Mysql extends Pdo { 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 { $mysql->db->setAttribute(\PDO::MYSQL_ATTR_USE_BUFFERED_QUERY, false); } const CONFIG_unbufferedQueries = [self::class, "config_unbufferedQueries"]; + const DEFAULT_CONFIG = [ + ...parent::DEFAULT_CONFIG, + self::CONFIG_setTimeout, + ]; + function getDbname(): ?string { $url = $this->dbconn["name"] ?? null; if ($url !== null && preg_match('/^mysql(?::|.*;)dbname=([^;]+)/i', $url, $ms)) { diff --git a/php/src/db/pdo/Pdo.php b/php/src/db/pdo/Pdo.php index 10da1c6..a12be17 100644 --- a/php/src/db/pdo/Pdo.php +++ b/php/src/db/pdo/Pdo.php @@ -6,8 +6,8 @@ use nulib\db\_private\_config; use nulib\db\_private\Tvalues; use nulib\db\IDatabase; use nulib\db\ITransactor; +use nulib\exceptions; use nulib\php\func; -use nulib\ValueException; class Pdo implements IDatabase { use Tvalues; @@ -28,6 +28,7 @@ class Pdo implements IDatabase { "options" => $pdo->options, "config" => $pdo->config, "migration" => $pdo->migration, + "autocheck" => $pdo->autocheck, ], $params)); } else { return new static($pdo, $params); @@ -41,7 +42,7 @@ class Pdo implements IDatabase { const CONFIG_errmodeException_lowerCase = [self::class, "config_errmodeException_lowerCase"]; protected const OPTIONS = [ - \PDO::ATTR_PERSISTENT => true, + \PDO::ATTR_PERSISTENT => false, ]; protected const DEFAULT_CONFIG = [ @@ -52,6 +53,10 @@ class Pdo implements IDatabase { protected const MIGRATION = null; + protected const AUTOCHECK = true; + + protected const AUTOOPEN = true; + const dbconn_SCHEMA = [ "name" => "string", "user" => "?string", @@ -64,7 +69,8 @@ class Pdo implements IDatabase { "replace_config" => ["?array|callable"], "config" => ["?array|callable"], "migration" => ["?array|string|callable"], - "auto_open" => ["bool", true], + "autocheck" => ["bool", self::AUTOCHECK], + "autoopen" => ["bool", self::AUTOOPEN], ]; function __construct($dbconn=null, ?array $params=null) { @@ -96,8 +102,8 @@ class Pdo implements IDatabase { # migrations $this->migration = $params["migration"] ?? static::MIGRATION; # - $defaultAutoOpen = self::params_SCHEMA["auto_open"][1]; - if ($params["auto_open"] ?? $defaultAutoOpen) { + $this->autocheck = $params["autocheck"] ?? static::AUTOCHECK; + if ($params["autoopen"] ?? static::AUTOOPEN) { $this->open(); } } @@ -113,6 +119,8 @@ class Pdo implements IDatabase { /** @var array|string|callable */ protected $migration; + protected bool $autocheck; + protected ?\PDO $db = null; function getSql($query, ?array $params=null): string { @@ -163,7 +171,7 @@ class Pdo implements IDatabase { const SQL_CHECK_LIVE = "select 1"; - function ensure(): self { + function ensureLive(): self { try { $this->_query(static::SQL_CHECK_LIVE); } catch (\PDOException $e) { @@ -195,7 +203,7 @@ class Pdo implements IDatabase { $this->transactors[] = $transactor; $transactor->willUpdate(); } else { - throw ValueException::invalid_type($transactor, ITransactor::class); + throw exceptions::invalid_type($transactor, "transactor", ITransactor::class); } } return $this; @@ -206,6 +214,9 @@ class Pdo implements IDatabase { } 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(); if ($this->transactors !== null) { foreach ($this->transactors as $transactor) { diff --git a/php/src/db/pgsql/Pgsql.php b/php/src/db/pgsql/Pgsql.php index ca9b7e9..72e2ef8 100644 --- a/php/src/db/pgsql/Pgsql.php +++ b/php/src/db/pgsql/Pgsql.php @@ -6,8 +6,8 @@ use nulib\db\_private\_config; use nulib\db\_private\Tvalues; use nulib\db\IDatabase; use nulib\db\ITransactor; +use nulib\exceptions; use nulib\php\func; -use nulib\ValueException; class Pgsql implements IDatabase { use Tvalues; @@ -34,7 +34,6 @@ class Pgsql implements IDatabase { } } - protected const OPTIONS = [ # XXX désactiver les connexions persistantes par défaut # 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; + protected const AUTOCHECK = true; + + protected const AUTOOPEN = true; + const params_SCHEMA = [ "dbconn" => ["array"], "options" => ["?array|callable"], "replace_config" => ["?array|callable"], "config" => ["?array|callable"], "migration" => ["?array|string|callable"], - "auto_open" => ["bool", true], + "autocheck" => ["bool", self::AUTOCHECK], + "autoopen" => ["bool", self::AUTOOPEN], ]; const dbconn_SCHEMA = [ @@ -113,8 +117,8 @@ class Pgsql implements IDatabase { # migrations $this->migration = $params["migration"] ?? static::MIGRATION; # - $defaultAutoOpen = self::params_SCHEMA["auto_open"][1]; - if ($params["auto_open"] ?? $defaultAutoOpen) { + $this->autocheck = $params["autocheck"] ?? static::AUTOCHECK; + if ($params["autoopen"] ?? static::AUTOOPEN) { $this->open(); } } @@ -130,6 +134,8 @@ class Pgsql implements IDatabase { /** @var array|string|callable */ protected $migration; + protected bool $autocheck; + /** @var resource */ protected $db = null; @@ -209,7 +215,7 @@ class Pgsql implements IDatabase { const SQL_CHECK_LIVE = "select 1"; - function ensure(): self { + function ensureLive(): self { try { $this->_query(static::SQL_CHECK_LIVE); } catch (\PDOException $e) { @@ -247,7 +253,7 @@ class Pgsql implements IDatabase { $this->transactors[] = $transactor; $transactor->willUpdate(); } else { - throw ValueException::invalid_type($transactor, ITransactor::class); + throw exceptions::invalid_type($transactor, "transactor", ITransactor::class); } } return $this; @@ -267,6 +273,9 @@ class Pgsql implements IDatabase { } 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"); if ($this->transactors !== null) { foreach ($this->transactors as $transactor) { diff --git a/php/src/db/sqlite/Sqlite.php b/php/src/db/sqlite/Sqlite.php index 1d52f2c..7876c35 100644 --- a/php/src/db/sqlite/Sqlite.php +++ b/php/src/db/sqlite/Sqlite.php @@ -7,8 +7,8 @@ use nulib\db\_private\_config; use nulib\db\_private\Tvalues; use nulib\db\IDatabase; use nulib\db\ITransactor; +use nulib\exceptions; use nulib\php\func; -use nulib\ValueException; use SQLite3; use SQLite3Result; use SQLite3Stmt; @@ -80,6 +80,10 @@ class Sqlite implements IDatabase { const MIGRATION = null; + protected const AUTOCHECK = true; + + protected const AUTOOPEN = true; + const params_SCHEMA = [ "file" => ["string", ""], "flags" => ["int", SQLITE3_OPEN_READWRITE + SQLITE3_OPEN_CREATE], @@ -88,7 +92,8 @@ class Sqlite implements IDatabase { "replace_config" => ["?array|callable"], "config" => ["?array|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) { @@ -117,9 +122,9 @@ class Sqlite implements IDatabase { # migrations $this->migration = $params["migration"] ?? static::MIGRATION; # - $defaultAutoOpen = self::params_SCHEMA["auto_open"][1]; $this->inTransaction = false; - if ($params["auto_open"] ?? $defaultAutoOpen) { + $this->autocheck = $params["autocheck"] ?? static::AUTOCHECK; + if ($params["autoopen"] ?? static::AUTOOPEN) { $this->open(); } } @@ -147,6 +152,8 @@ class Sqlite implements IDatabase { /** @var array|string|callable */ protected $migration; + protected bool $autocheck; + /** @var SQLite3 */ protected $db; @@ -208,7 +215,7 @@ class Sqlite implements IDatabase { const SQL_CHECK_LIVE = "select 1"; - function ensure(): self { + function ensureLive(): self { try { $this->_query(static::SQL_CHECK_LIVE); } catch (\PDOException $e) { @@ -247,7 +254,7 @@ class Sqlite implements IDatabase { $this->transactors[] = $transactor; $transactor->willUpdate(); } else { - throw ValueException::invalid_type($transactor, ITransactor::class); + throw exceptions::invalid_type($transactor, "transactor", ITransactor::class); } } return $this; @@ -259,6 +266,9 @@ class Sqlite implements IDatabase { } 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->inTransaction = true; if ($this->transactors !== null) { diff --git a/php/src/exceptions.php b/php/src/exceptions.php new file mode 100644 index 0000000..a6b1e88 --- /dev/null +++ b/php/src/exceptions.php @@ -0,0 +1,253 @@ +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); + } +} diff --git a/php/src/file.php b/php/src/file.php index a8175d4..1045293 100644 --- a/php/src/file.php +++ b/php/src/file.php @@ -57,9 +57,9 @@ class file { } return $file; } - - static function writer($output, ?string $mode="w+b", ?callable $func=null): FileWriter { - $file = new FileWriter(self::fix_dash($output), $mode); + + static function writer($output, ?callable $func=null): FileWriter { + $file = new FileWriter(self::fix_dash($output), "w+b"); if ($func !== null) { try { $func($file); diff --git a/php/src/file/SharedFile.php b/php/src/file/SharedFile.php index c14001e..70456e5 100644 --- a/php/src/file/SharedFile.php +++ b/php/src/file/SharedFile.php @@ -1,7 +1,7 @@ fd = $fd; $this->close = $close; $this->throwOnError = $throwOnError ?? static::THROW_ON_ERROR; diff --git a/php/src/file/csv/CsvReader.php b/php/src/file/csv/CsvReader.php index 317fda0..3dbdd39 100644 --- a/php/src/file/csv/CsvReader.php +++ b/php/src/file/csv/CsvReader.php @@ -2,7 +2,6 @@ namespace nulib\file\csv; use nulib\file; -use nulib\file\_IFile; use nulib\file\FileReader; use nulib\file\IReader; use nulib\file\tab\AbstractReader; diff --git a/php/src/file/csv/csv_flavours.php b/php/src/file/csv/csv_flavours.php index 4bc7bd9..bb00150 100644 --- a/php/src/file/csv/csv_flavours.php +++ b/php/src/file/csv/csv_flavours.php @@ -2,7 +2,7 @@ namespace nulib\file\csv; use nulib\cl; -use nulib\ref\file\csv\ref_csv; +use nulib\ref\ref_csv; use nulib\str; class csv_flavours { @@ -17,13 +17,13 @@ class csv_flavours { "dumb," => ref_csv::DUMB_OO_FLAVOUR, "dumb" => ref_csv::DUMB_FLAVOUR, ]; - + const ENCODINGS = [ ref_csv::OO_FLAVOUR => ref_csv::OO_ENCODING, ref_csv::XL_FLAVOUR => ref_csv::XL_ENCODING, ref_csv::DUMB_FLAVOUR => ref_csv::DUMB_ENCODING, ]; - + static final function verifix(?string $flavour): ?string { if ($flavour === null) return null; $lflavour = strtolower($flavour); @@ -41,7 +41,7 @@ class csv_flavours { elseif ($flavour == ref_csv::XL_FLAVOUR) return ref_csv::MSEXCEL; else return $flavour; } - + static final function get_params(string $flavour): array { return [$flavour[0], $flavour[1], $flavour[2]]; } diff --git a/php/src/file/tab/TAbstractBuilder.php b/php/src/file/tab/TAbstractBuilder.php index f12e1e0..f43e604 100644 --- a/php/src/file/tab/TAbstractBuilder.php +++ b/php/src/file/tab/TAbstractBuilder.php @@ -2,10 +2,10 @@ namespace nulib\file\tab; use nulib\cl; +use nulib\exceptions; use nulib\file\csv\CsvBuilder; use nulib\file\web\Upload; use nulib\os\path; -use nulib\ValueException; trait TAbstractBuilder { /** @param Upload|string|array $builder */ @@ -32,7 +32,7 @@ trait TAbstractBuilder { } elseif (is_array($builder)) { $params = cl::merge($builder, $params); } elseif ($builder !== null) { - throw ValueException::invalid_type($builder, self::class); + throw exceptions::invalid_type($builder, "builder", self::class); } $output = $params["output"] ?? null; diff --git a/php/src/file/tab/TAbstractReader.php b/php/src/file/tab/TAbstractReader.php index f6037c7..23ab697 100644 --- a/php/src/file/tab/TAbstractReader.php +++ b/php/src/file/tab/TAbstractReader.php @@ -2,10 +2,10 @@ namespace nulib\file\tab; use nulib\cl; +use nulib\exceptions; use nulib\file\csv\CsvReader; use nulib\file\web\Upload; use nulib\os\path; -use nulib\ValueException; trait TAbstractReader { /** @param Upload|string|array $reader */ @@ -31,7 +31,7 @@ trait TAbstractReader { } elseif (is_array($reader)) { $params = cl::merge($reader, $params); } elseif ($reader !== null) { - throw ValueException::invalid_type($reader, self::class); + throw exceptions::invalid_type($reader, "reader", self::class); } $input = $params["input"] ?? null; diff --git a/php/src/mail/MailTemplate.php b/php/src/mail/MailTemplate.php new file mode 100644 index 0000000..59e3bae --- /dev/null +++ b/php/src/mail/MailTemplate.php @@ -0,0 +1,119 @@ + "string", + "body" => "string", + "exprs" => "array", + ]; + + function __construct(array $mail) { + $tsubject = $mail["subject"] ?? null; + $tbody = $mail["body"] ?? null; + $texprs = $mail["exprs"] ?? []; + + $this->el = new ExpressionLanguage(); + $this->subject = cv::not_null($tsubject, "subject"); + $this->body = cv::not_null($tbody, "body"); + $exprs = []; + # 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)) { + foreach ($mss as $ms) { + $key = $ms[0]; + $expr = str_replace("'", "\\'", $ms[1]); + $expr = "_helper.value('$expr')"; + $exprs[$key] = $expr; + } + } + $index = 0; + foreach ($texprs as $key => $expr) { + $prefix = null; + $orig = $expr; + if (preg_match('/^\[([^]]*)]/', $expr, $ms)) { + # un préfixe spécifié de la forme [prefix]expr permet de reconnaitre les + # formes spéciales de expr (+, *, .) qui sont précédées de prefix + # exemple: [https://]+app.url permettra d'utiliser un texte markdown + # de la forme qui est correctement reconnu comme un + # url + $prefix = $ms[1]; + $expr = substr($expr, strlen($ms[0])); + } + $mapKey = false; + if (str::del_prefix($expr, "+")) { + # config + $mapKey = "$prefix+$expr"; + $expr = str_replace("'", "\\'", $expr); + $expr = "_helper.config('$expr')"; + } elseif (str::del_prefix($expr, "*")) { + # session + $mapKey = "$prefix*$expr"; + $expr = str_replace("'", "\\'", $expr); + $expr = "_helper.session('$expr')"; + } elseif (str::del_prefix($expr, ".")) { + # session + $mapKey = "$prefix.$expr"; + $expr = str_replace("'", "\\'", $expr); + $expr = "_helper.value('$expr')"; + } elseif ($prefix !== null) { + # sinon remettre le préfixe + $expr = $orig; + } + + if ($key === $index) { + $index++; + if ($mapKey !== false) { + $exprs[$mapKey] = $expr; + } else { + # clé normale: la correspondance est en minuscule + $exprs[$expr] = strtolower($expr); + } + } else { + $exprs[$key] = $expr; + } + } + uksort($exprs, function ($a, $b) { + return -cv::complen($a, $b); + }); + $this->exprs = $exprs; + } + + /** @var ExpressionLanguage */ + protected $el; + + protected $subject; + + protected $body; + + protected $exprs; + + protected function _eval(string $template, ?array $data): string { + if ($data === null) return $template; + $el = $this->el; + foreach ($this->exprs as $key => $expr) { + $value = $el->evaluate($expr, $data); + if (is_array($value)) $value = str::join(" ", $value); + elseif (!is_string($value)) $value = strval($value); + $template = str_replace($key, $value, $template); + } + return $template; + } + + function eval(?array $data, $convertMd=true): array { + if ($data !== null) { + $data["_helper"] = new MailTemplateHelper($data); + } + $subject = $this->_eval($this->subject, $data); + $body = $this->body; + if ($convertMd) $body = mdc::convert($body); + $body = $this->_eval($body, $data); + return [ + "subject" => $subject, + "body" => $body, + ]; + } +} diff --git a/php/src/mail/MailTemplateHelper.php b/php/src/mail/MailTemplateHelper.php new file mode 100644 index 0000000..37d68d9 --- /dev/null +++ b/php/src/mail/MailTemplateHelper.php @@ -0,0 +1,24 @@ +data = $data; + } + + function value(string $pkey) { + return cl::pget($this->data, $pkey); + } + + function config(string $pkey) { + return config::get($pkey); + } + + function session(string $pkey) { + return session::pget($pkey); + } +} diff --git a/php/src/mail/MailerException.php b/php/src/mail/MailerException.php new file mode 100644 index 0000000..79e9261 --- /dev/null +++ b/php/src/mail/MailerException.php @@ -0,0 +1,7 @@ + ["string", "smtp"], + "debug" => ["int", SMTP::DEBUG_OFF], + "host" => ["?string", "smtp.univ.run"], + "port" => ["?int", 25], + "auth" => "?bool", + "username" => "?string", + "password" => "?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 { + $params = self::resolve_params($params); + + $mailer = new PHPMailer($exceptions); + $mailer->setLanguage("fr"); + $mailer->CharSet = PHPMailer::CHARSET_UTF8; + # backend + $backend = $params["backend"] ?? "smtp"; + switch ($backend) { + case "smtp": + # host, port + $host = $params["host"] ?? null; + $port = $params["port"] ?? 25; + if ($host === null) { + throw exceptions::null_value("host"); + } + msg::debug("new PHPMailer using SMTP to $host:$port"); + $mailer->isSMTP(); + $mailer->Host = $host; + $mailer->Port = $port; + break; + case "phpmail": + msg::debug("new PHPMailer using PHPmail"); + $mailer->isMail(); + break; + case "sendmail": + msg::debug("new PHPMailer using sendmail"); + $mailer->isSendmail(); + break; + default: + throw exceptions::forbidden_value($backend, "backend", ["smtp", "phpmail", "sendmail"]); + } + # debug + $debug = $params["debug"] ?? SMTP::DEBUG_OFF; + if (is_int($debug)) { + if ($debug < SMTP::DEBUG_OFF) $debug = SMTP::DEBUG_OFF; + elseif ($debug > SMTP::DEBUG_LOWLEVEL) $debug = SMTP::DEBUG_LOWLEVEL; + } elseif (!self::is_bool($debug)) { + throw exceptions::invalid_type($debug, "debug", ["int", "bool"]); + } + $mailer->SMTPDebug = $debug; + # auth, username, password + $username = $params["username"] ?? null; + $username ??= cv::vn(getenv("NULIB_MAIL_USERNAME")); + $password = $params["password"] ?? null; + $password ??= cv::vn(getenv("NULIB_MAIL_PASSWORD")); + $auth = $params["auth"] ?? null; + $auth ??= cv::vn(getenv("NULIB_MAIL_AUTH")); + $auth ??= $username !== null && $password !== null; + $mailer->SMTPAuth = self::get_bool($auth); + $mailer->Username = $username; + $mailer->Password = $password; + # secure + $secure = $params["secure"] ?? null; + $secure ??= cv::vn(getenv("NULIB_MAIL_SECURE")); + $secure ??= false; + if (self::is_bool($secure)) { + if (!$secure) { + $mailer->SMTPSecure = ""; + $mailer->SMTPAutoTLS = false; + } + } else { + switch ($secure) { + case PHPMailer::ENCRYPTION_SMTPS: + case PHPMailer::ENCRYPTION_STARTTLS: + $mailer->SMTPSecure = $secure; + break; + default: + throw exceptions::forbidden_value($secure, "secure", [ + PHPMailer::ENCRYPTION_SMTPS, + PHPMailer::ENCRYPTION_STARTTLS, + ]); + } + } + + return $mailer; + } + + static function build($to, string $subject, string $body, $cc=null, $bcc=null, ?string $from=null, ?PHPMailer $mailer=null): PHPMailer { + if ($mailer === null) $mailer = self::get(); + $mailer->clearAllRecipients(); + + if ($from === null) $from = static::FROM; + $mailer->setFrom($from); + foreach (cl::with($to) as $tos) { + foreach (preg_split('/\s*[,;]\s*/', trim($tos)) as $to) { + $mailer->addAddress($to); + } + } + foreach (cl::with($cc) as $ccs) { + foreach (preg_split('/\s*[,;]\s*/', trim($ccs)) as $cc) { + $mailer->addCC($cc); + } + } + foreach (cl::with($bcc) as $bccs) { + foreach (preg_split('/\s*[,;]\s*/', trim($bccs)) as $bcc) { + $mailer->addBCC($bcc); + } + } + $mailer->isHTML(); + $mailer->Subject = $subject; + $mailer->Body = $body; + return $mailer; + } + + static function _send(PHPMailer $mailer): void { + $tos = []; + foreach ($mailer->getToAddresses() as $to) { + $tos[] = $to[0]; + } + $tos = str::join(",", $tos); + msg::debug("Sending to $tos"); + if (!$mailer->send()) { + throw new MailerException("erreur d'envoi du mail", $mailer->ErrorInfo); + } + } + + static function send($to, string $subject, string $body, $cc=null, $bcc=null, ?string $from=null, ?PHPMailer $mailer=null): void { + self::_send(self::build($to, $subject, $body, $cc, $bcc, $from, $mailer)); + } + + static function tsend(array $template, array $data, $to, $cc=null, $bcc=null, ?string $from=null): void { + $template = new MailTemplate($template); + $mail = $template->eval($data); + self::send($to, $mail["subject"], $mail["body"], $cc, $bcc, $from); + } +} diff --git a/php/src/mail/mdc.php b/php/src/mail/mdc.php new file mode 100644 index 0000000..204264e --- /dev/null +++ b/php/src/mail/mdc.php @@ -0,0 +1,22 @@ + false, + ]); + } + return self::$mdc; + } + + static function convert(string $text): string { + return self::mdc()->convert($text); + } +} diff --git a/php/src/os/sh.php b/php/src/os/sh.php index a18f012..802ca36 100644 --- a/php/src/os/sh.php +++ b/php/src/os/sh.php @@ -1,7 +1,7 @@ string|list] + ne pas faire cette transformation si le tableau est associatif + * un trait Tlogger permet de spécifier le cas échéant comment mettre en forme + une donnée structurée --> il permet de calculer les valeurs de user_message + et tech_message + -*- coding: utf-8 mode: markdown -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8:noeol:binary \ No newline at end of file diff --git a/php/src/output/_TMessenger.php b/php/src/output/_TMessenger.php new file mode 100644 index 0000000..912dc0a --- /dev/null +++ b/php/src/output/_TMessenger.php @@ -0,0 +1,67 @@ +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"]); + } + } +} diff --git a/php/src/output/_messenger.php b/php/src/output/_messenger.php index 1226c24..bf5d4ca 100644 --- a/php/src/output/_messenger.php +++ b/php/src/output/_messenger.php @@ -1,44 +1,16 @@ clone($params); } - static final function __callStatic($name, $args) { - $name = str::us2camel($name); - call_user_func_array([static::get(), $name], $args); - } - ############################################################################# const DEBUG = IMessenger::DEBUG; diff --git a/php/src/output/con.php b/php/src/output/con.php new file mode 100644 index 0000000..6937328 --- /dev/null +++ b/php/src/output/con.php @@ -0,0 +1,25 @@ +resetParams([ + "color" => $color, + ]); + } +} diff --git a/php/src/output/console.php b/php/src/output/console.php deleted file mode 100644 index 0ef8c81..0000000 --- a/php/src/output/console.php +++ /dev/null @@ -1,28 +0,0 @@ -isEmpty()) { + $msg->addMessenger(new LogMessenger([ + "min_level" => msg::MINOR, + ])); + } + return $msg; + } + + static function set_output(string $logfile): void { + self::ensure_log()->resetParams([ + "output" => $logfile, + ]); } } diff --git a/php/src/output/msg.php b/php/src/output/msg.php index d180d18..9bd4eee 100644 --- a/php/src/output/msg.php +++ b/php/src/output/msg.php @@ -1,67 +1,18 @@ 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(); diff --git a/php/src/output/say.php b/php/src/output/say.php index 9d8b6d0..f5d5583 100644 --- a/php/src/output/say.php +++ b/php/src/output/say.php @@ -1,28 +1,17 @@ $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); + } + } + } + } +} diff --git a/php/src/output/std/ConsoleMessenger.php b/php/src/output/std/ConsoleMessenger.php new file mode 100644 index 0000000..1cb8bcf --- /dev/null +++ b/php/src/output/std/ConsoleMessenger.php @@ -0,0 +1,473 @@ + $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(); + } +} diff --git a/php/src/output/std/LogMessenger.php b/php/src/output/std/LogMessenger.php new file mode 100644 index 0000000..3bf3391 --- /dev/null +++ b/php/src/output/std/LogMessenger.php @@ -0,0 +1,350 @@ +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(); + } + } +} diff --git a/php/src/output/std/NullMessenger.php b/php/src/output/std/NullMessenger.php new file mode 100644 index 0000000..0218ebb --- /dev/null +++ b/php/src/output/std/NullMessenger.php @@ -0,0 +1,61 @@ +msgs = []; foreach ($msgs as $msg) { if ($msg !== null) $this->msgs[] = $msg; } } - /** @var IMessenger[] */ - protected $msgs; + /** @var _IMessenger[] */ + 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 { $clone = clone $this; foreach ($clone->msgs as &$msg) { @@ -30,92 +41,180 @@ class ProxyMessenger implements IMessenger { }; unset($msg); 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 { - $useFunc = false; foreach ($this->msgs as $msg) { $msg->section($content, null, $level); - if ($msg instanceof _IMessenger) $useFunc = true; } - if ($useFunc && $func !== null) { + if ($func !== null) { try { $func($this); } finally { - /** @var _IMessenger $msg */ - foreach ($this->msgs as $msg) { - $msg->_endSection(); - } + $this->section__afterFunc(); } } } - function title($content, ?callable $func=null, ?int $level=null): void { - $useFunc = false; - $untils = []; - foreach ($this->msgs as $msg) { + + function title__getMarks(): array { + $marks = []; + foreach ($this->msgs as $key => $msg) { if ($msg instanceof _IMessenger) { - $useFunc = true; - $untils[] = $msg->_getTitleMark(); + $marks[$key] = $msg->title__getMarks(); } + } + 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); } - if ($useFunc && $func !== null) { + if ($func !== null) { try { + $this->title__beforeFunc($marks); $func($this); } finally { - /** @var _IMessenger $msg */ - $index = 0; - foreach ($this->msgs as $msg) { - if ($msg instanceof _IMessenger) { - $msg->_endTitle($untils[$index++]); - } - } + $this->title__afterFunc($marks); } } } - 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 { - $useFunc = false; - $untils = []; + + function desc($content, ?int $level=null): void { foreach ($this->msgs as $msg) { + $msg->desc($content, $level); + } + } + + function action__getMarks(): array { + $marks = []; + foreach ($this->msgs as $key => $msg) { if ($msg instanceof _IMessenger) { - $useFunc = true; - $untils[] = $msg->_getActionMark(); + $marks[$key] = $msg->action__getMarks(); } + } + 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); } - if ($useFunc && $func !== null) { + if ($func !== null) { try { + $result = null; + $this->action__beforeFunc($marks); $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 { - /** @var _IMessenger $msg */ - $index = 0; - foreach ($this->msgs as $msg) { - $msg->_endAction($untils[$index++]); - } + $this->action__afterFunc($marks, $result); } } } - 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 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); } } + + 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 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); + } + } } diff --git a/php/src/output/std/StdMessenger.php b/php/src/output/std/StdMessenger.php deleted file mode 100644 index 3892a0d..0000000 --- a/php/src/output/std/StdMessenger.php +++ /dev/null @@ -1,722 +0,0 @@ - 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!", "===", "=", "=", "==="], - "title" => [false, "TITLE!", null, "T", "", "==="], - "desc" => ["DESC!", ">", ""], - "error" => ["CRIT.ERROR!", "E!", ""], - "warning" => ["CRIT.WARNING!", "W!", ""], - "note" => ["ATTENTION!", "N!", ""], - "info" => ["IMPORTANT!", "N!", ""], - "step" => ["*", ".", ""], - "print" => [null, null, null], - ], - self::NORMAL => [ - "section" => [true, "SECTION:", "---", "-", "-", "---"], - "title" => [false, "TITLE:", null, "T", "", "---"], - "desc" => ["DESC:", ">", ""], - "error" => ["ERROR:", "E", ""], - "warning" => ["WARNING:", "W", ""], - "note" => ["NOTE:", "N", ""], - "info" => ["INFO:", "I", ""], - "step" => ["*", ".", ""], - "print" => [null, null, null], - ], - self::MINOR => [ - "section" => [true, "section", null, ">>", "<<", null], - "title" => [false, "title", null, "t", "", null], - "desc" => ["desc", ">", ""], - "error" => ["error", "E", ""], - "warning" => ["warning", "W", ""], - "note" => ["note", "N", ""], - "info" => ["info", "I", ""], - "step" => ["*", ".", ""], - "print" => [null, null, null], - ], - self::DEBUG => [ - "section" => [true, "section", null, ">>", "<<", null], - "title" => [false, "title", null, "t", "", null], - "desc" => ["desc", ">", ""], - "error" => ["debugE", "e", ""], - "warning" => ["debugW", "w", ""], - "note" => ["debugN", "i", ""], - "info" => ["debug", "D", ""], - "step" => ["*", ".", ""], - "print" => [null, null, null], - ], - ]; - - const RESULT_PREFIXES = [ - "failure" => ["(FAILURE)", ""], - "success" => ["(SUCCESS)", ""], - "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); - $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(); - } - } -} diff --git a/php/src/output/std/StdOutput.php b/php/src/output/std/StdOutput.php index c30c466..945b9b3 100644 --- a/php/src/output/std/StdOutput.php +++ b/php/src/output/std/StdOutput.php @@ -79,12 +79,12 @@ class StdOutput { } function resetParams(?array $params=null): void { - $output = cl::get($params, "output"); + $output = $params["output"] ?? null; $maskErrors = null; - $color = cl::get($params, "color"); - $filterTags = cl::get($params, "filter_tags"); - $indent = cl::get($params, "indent"); - $flush = cl::get($params, "flush"); + $color = $params["color"] ?? null; + $filterTags = $params["filter_tags"] ?? null; + $indent = $params["indent"] ?? null; + $flush = $params["flush"] ?? null; if ($output instanceof Stream) $output = $output->getResource(); if ($output !== null) { @@ -105,14 +105,14 @@ class StdOutput { else $message = "$output: open error"; throw new Exception($message); } - if ($flush === null) $flush = true; + $flush ??= true; } else { $outf = $output; } $this->outf = $outf; $this->maskErrors = $maskErrors; - if ($color === null) $color = stream_isatty($outf); - if ($flush === null) $flush = false; + $color ??= stream_isatty($outf); + $flush ??= false; } if ($color !== null) $this->color = boolval($color); if ($filterTags !== null) $this->filterTags = boolval($filterTags); @@ -124,23 +124,23 @@ class StdOutput { protected $outf; /** @var bool faut-il masquer les erreurs d'écriture? */ - protected $maskErrors; + protected ?bool $maskErrors; /** @var bool faut-il autoriser la sortie en couleur? */ - protected $color; + protected bool $color = false; function isColor(): bool { return $this->color; } /** @var bool faut-il enlever les tags dans la sortie? */ - protected $filterTags; + protected bool $filterTags = true; /** @var string indentation unitaire */ - protected $indent; + protected string $indent = " "; /** @var bool faut-il flush le fichier après l'écriture de chaque ligne */ - protected $flush; + protected bool $flush = true; function isatty(): bool { return stream_isatty($this->outf); @@ -167,6 +167,7 @@ class StdOutput { $text .= "m"; return $text; } + function filterContent(string $text): string { # couleur au début $text = preg_replace_callback('/]*)>/', [self::class, "replace_colors"], $text); @@ -178,6 +179,7 @@ class StdOutput { } return $text; } + function filterColors(string $text): string { return preg_replace('/\x1B\[.*?m/', "", $text); } diff --git a/php/src/output/std/_IMessenger.php b/php/src/output/std/_IMessenger.php index 9b54b59..86d84cb 100644 --- a/php/src/output/std/_IMessenger.php +++ b/php/src/output/std/_IMessenger.php @@ -7,13 +7,86 @@ use nulib\output\IMessenger; * Interface _IMessenger: méthodes privées de 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!", "===", "=", "=", "==="], + "title" => [false, "TITLE!", null, "T", "", "==="], + "desc" => ["DESC!", ">", ""], + "error" => ["CRIT.ERROR!", "E!", ""], + "warning" => ["CRIT.WARNING!", "W!", ""], + "note" => ["ATTENTION!", "N!", ""], + "info" => ["IMPORTANT!", "N!", ""], + "step" => ["*", ".", ""], + "print" => [null, null, null], + ], + self::NORMAL => [ + "section" => [true, "SECTION:", "---", "-", "-", "---"], + "title" => [false, "TITLE:", null, "T", "", "---"], + "desc" => ["DESC:", ">", ""], + "error" => ["ERROR:", "E", ""], + "warning" => ["WARNING:", "W", ""], + "note" => ["NOTE:", "N", ""], + "info" => ["INFO:", "I", ""], + "step" => ["*", ".", ""], + "print" => [null, null, null], + ], + self::MINOR => [ + "section" => [true, "section", null, ">>", "<<", null], + "title" => [false, "title", null, "t", "", null], + "desc" => ["desc", ">", ""], + "error" => ["error", "E", ""], + "warning" => ["warning", "W", ""], + "note" => ["note", "N", ""], + "info" => ["info", "I", ""], + "step" => ["*", ".", ""], + "print" => [null, null, null], + ], + self::DEBUG => [ + "section" => [true, "section", null, ">>", "<<", null], + "title" => [false, "title", null, "t", "", null], + "desc" => ["desc", ">", ""], + "error" => ["debugE", "e", ""], + "warning" => ["debugW", "w", ""], + "note" => ["debugN", "i", ""], + "info" => ["debug", "D", ""], + "step" => ["*", ".", ""], + "print" => [null, null, null], + ], + ]; + + const RESULT_PREFIXES = [ + "failure" => ["(FAILURE)", ""], + "success" => ["(SUCCESS)", ""], + "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; } diff --git a/php/src/output/web.php b/php/src/output/web.php new file mode 100644 index 0000000..c3b6e4b --- /dev/null +++ b/php/src/output/web.php @@ -0,0 +1,15 @@ +getMethods() as $m) { + if (($m->getModifiers() & $mask) != $expected) continue; + $name = $m->getName(); + if (substr($name, 0, $length) != $prefix) continue; + if (!self::match_name($name, $includes, $excludes)) continue; + $method = [$class_or_object, $name]; + $methods[] = self::with($method, $args); + } + return $methods; + } + + /** + * Appeler toutes les méthodes publiques de $object_or_class et retourner un + * tableau [$method_name => $return_value] des valeurs de retour. + */ + static function call_all($class_or_object, ?array $params=null) { + $methods = self::get_all($class_or_object, $params); + $values = []; + foreach ($methods as $method) { + $values[$method->getName()] = $method->invoke(); + } + return $values; + } + ############################################################################# protected function __construct(int $type, $func, ?array $args=null, bool $bound=false, ?string $reason=null) { @@ -598,6 +680,10 @@ class func { protected ?array $func; + function getName(): ?string { + return $this->func[1] ?? null; + } + protected bool $bound; protected ?string $reason; @@ -687,7 +773,7 @@ class func { if (is_object($object) && !($this->flags & self::FLAG_STATIC)) { if (is_object($c)) $c = get_class($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->bound = true; diff --git a/php/src/php/time/Date.php b/php/src/php/time/Date.php index c681e68..69a14c8 100644 --- a/php/src/php/time/Date.php +++ b/php/src/php/time/Date.php @@ -1,7 +1,7 @@ setTime(0, 0); + protected function fix(DateTimeInterface $datetime): \DateTimeInterface { + return $datetime->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 { diff --git a/php/src/php/time/DateTime.php b/php/src/php/time/DateTime.php index 438b614..264e945 100644 --- a/php/src/php/time/DateTime.php +++ b/php/src/php/time/DateTime.php @@ -1,10 +1,10 @@ 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"; - } +class DateTime extends \DateTimeImmutable { + use _TDateTime; 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 - * retourner l'année à 4 chiffres. + * $datetime est une spécification de date, avec ou sans fuseau horaire * - * 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 + * 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 * - * 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' + * 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 */ - 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"; - } - - function __construct($datetime="now", DateTimeZone $timezone=null, ?bool $forceLocalTimezone=null) { - $forceLocalTimezone ??= $timezone === null; - if ($forceLocalTimezone) { - $setTimezone = $timezone; + 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); + if ($datetime instanceof DateTimeImmutable) { + $datetime = \DateTime::createFromImmutable($datetime); + } elseif ($datetime instanceof \DateTime) { + $datetime = clone $datetime; + #XXX sous PHP 8, remplacer les deux commandes ci-dessus par + # DateTime::createFromInterface } elseif (is_int($datetime)) { - parent::__construct("now", $timezone); - $this->setTimestamp($datetime); + $timestamp = $datetime; + $datetime = new \DateTime("now", $timezone); + $datetime->setTimestamp($timestamp); } elseif (is_string($datetime)) { $Y = $H = $Z = null; - if (preg_match(self::DMY_PATTERN, $datetime, $ms)) { + if (preg_match(_utils::DMY_PATTERN, $datetime, $ms)) { $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")); $m = intval($ms[2]); $d = intval($ms[1]); - } elseif (preg_match(self::YMD_PATTERN, $datetime, $ms)) { + } elseif (preg_match(_utils::YMD_PATTERN, $datetime, $ms)) { $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); $m = intval($ms[2]); $d = intval($ms[3]); - } elseif (preg_match(self::DMYHIS_PATTERN, $datetime, $ms)) { + } elseif (preg_match(_utils::DMYHIS_PATTERN, $datetime, $ms)) { $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")); $m = intval($ms[2]); $d = intval($ms[1]); $H = intval($ms[4]); $M = intval($ms[5]); $S = intval($ms[6] ?? 0); - } elseif (preg_match(self::YMDHISZ_PATTERN, $datetime, $ms)) { + } elseif (preg_match(_utils::YMDHISZ_PATTERN, $datetime, $ms)) { $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); $m = intval($ms[2]); $d = intval($ms[3]); @@ -281,73 +107,61 @@ class DateTime extends \DateTime { if ($Y !== null) { if ($H === null) $datetime = sprintf("%04d-%02d-%02d", $Y, $m, $d); else $datetime = sprintf("%04d-%02d-%02d %02d:%02d:%02d", $Y, $m, $d, $H, $M, $S); - if ($Z !== null) $timezone ??= new DateTimeZone(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) { - $setTimezone = $timezone; - $timezone = null; + } 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(self::fix_z($Z)); - parent::__construct($datetime, $timezone); + if ($Z !== null) $timezone = new DateTimeZone(_utils::fix_z($Z)); + $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"); } - - if ($forceLocalTimezone) { - $setTimezone ??= new DateTimeZone(date_default_timezone_get()); - $this->setTimezone($setTimezone); - } } - function clone(): self { - return clone $this; + protected function fix(DateTimeInterface $datetime): DateTimeInterface { + return $datetime; } - 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, 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); + /** @return MutableDateTime|self */ + function clone(bool $mutable=false): DateTimeInterface { + if ($mutable) return new MutableDateTime($this); + else return clone $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 wrapStartOfDay(): self { - $this->setTime(0, 0); - return $this; + function getStartOfDay(): self { + return new static($this->setTime(0, 0)); } /** * 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 wrapEndOfDay(): self { - $this->setTime(23, 59, 59, 999999); - return $this; + function getEndOfDay(): self { + return new static($this->setTime(23, 59, 59, 999999)); } function getPrevDay(int $nbDays=1, bool $skipWeekend=false): self { 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 { @@ -355,35 +169,6 @@ class DateTime extends \DateTime { $wday = $this->wday; if ($wday > 5) $nbDays = 8 - $this->wday; } - return static::with($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"); + return new static($this->add(new \DateInterval("P${nbDays}D"))); } } diff --git a/php/src/php/time/Delay.php b/php/src/php/time/Delay.php index e773efa..15aa96f 100644 --- a/php/src/php/time/Delay.php +++ b/php/src/php/time/Delay.php @@ -3,7 +3,6 @@ namespace nulib\php\time; use DateTimeInterface; use InvalidArgumentException; -use nulib\ValueException; /** * Class Delay: une durée jusqu'à un moment destination. le moment destination @@ -31,6 +30,11 @@ class Delay { 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 */ const DEFAULTS = [ "w" => [0, 5], @@ -40,8 +44,7 @@ class Delay { "s" => [1, 0], ]; - static function compute_dest(int $x, string $u, ?int $y, ?DateTimeInterface $from): array { - $dest = DateTime::with($from)->clone(); + protected static function compute_dest(int $x, string $u, ?int $y, MutableDateTime $dest): array { $yu = null; switch ($u) { case "w": @@ -90,10 +93,10 @@ class Delay { } function __construct($delay, ?DateTimeInterface $from=null) { - if ($from === null) $from = new DateTime(); - if ($delay === "INF") { - $dest = DateTime::with($from)->clone(); - $dest->add(new DateInterval("P9999Y")); + $from = MutableDateTime::with($from)->clone(true); + if ($delay === null || $delay === "INF") { + # $dest === null signifie un délai infini + $dest = null; $repr = "INF"; } elseif (is_int($delay)) { [$dest, $repr] = self::compute_dest($delay, "s", null, $from); @@ -117,37 +120,53 @@ class Delay { } function __clone() { - $this->dest = clone $this->dest; + if ($this->dest !== null) { + $this->dest = clone $this->dest; + } } 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 { - [$this->dest, $this->repr] = $data; + [$dest, $this->repr] = $data; + if ($dest !== null) $dest = $dest->clone(true); + $this->dest = $dest; } - /** @var DateTime */ - protected $dest; + protected ?MutableDateTime $dest; 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) { - if (is_numeric($duration) && $duration < 0) { - $this->dest->sub(DateInterval::with(-$duration)); - } else { - $this->dest->add(DateInterval::with($duration)); + function addDuration($duration): self { + if ($this->dest !== null) { + if (is_numeric($duration) && $duration < 0) { + $this->dest->sub(DateInterval::with(-$duration)); + } else { + $this->dest->add(DateInterval::with($duration)); + } } + return $this; } - function subDuration($duration) { - if (is_numeric($duration) && $duration < 0) { - $this->dest->add(DateInterval::with(-$duration)); - } else { - $this->dest->sub(DateInterval::with($duration)); + function subDuration($duration): self { + if ($this->dest !== null) { + if (is_numeric($duration) && $duration < 0) { + $this->dest->add(DateInterval::with(-$duration)); + } else { + $this->dest->sub(DateInterval::with($duration)); + } } + return $this; } /** @var string */ @@ -157,23 +176,20 @@ class Delay { 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. * * l'intervalle est négatif si le délai n'est pas écoulé, positif sinon */ 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; } } diff --git a/php/src/php/time/Elapsed.php b/php/src/php/time/Elapsed.php index 37f22c6..1d3f49a 100644 --- a/php/src/php/time/Elapsed.php +++ b/php/src/php/time/Elapsed.php @@ -1,6 +1,8 @@ getTimestamp() - $start->getTimestamp(); 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(); $seconds = $now->getTimestamp() - $start->getTimestamp(); 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(); $seconds = $now->getTimestamp() - $start->getTimestamp(); return (new self($seconds, $resolution))->formatDelay(); diff --git a/php/src/php/time/MutableDate.php b/php/src/php/time/MutableDate.php new file mode 100644 index 0000000..1f01428 --- /dev/null +++ b/php/src/php/time/MutableDate.php @@ -0,0 +1,24 @@ +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); + } +} diff --git a/php/src/php/time/MutableDateTime.php b/php/src/php/time/MutableDateTime.php new file mode 100644 index 0000000..1083444 --- /dev/null +++ b/php/src/php/time/MutableDateTime.php @@ -0,0 +1,180 @@ +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); + } +} diff --git a/php/src/php/time/TODO.md b/php/src/php/time/TODO.md index edf17b0..ffa6b2e 100644 --- a/php/src/php/time/TODO.md +++ b/php/src/php/time/TODO.md @@ -1,10 +1,3 @@ # 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 \ No newline at end of file diff --git a/php/src/php/time/_TDateTime.php b/php/src/php/time/_TDateTime.php new file mode 100644 index 0000000..755ec31 --- /dev/null +++ b/php/src/php/time/_TDateTime.php @@ -0,0 +1,103 @@ +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); + } +} diff --git a/php/src/php/time/_utils.php b/php/src/php/time/_utils.php new file mode 100644 index 0000000..aeec99e --- /dev/null +++ b/php/src/php/time/_utils.php @@ -0,0 +1,147 @@ +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]; + } +} diff --git a/php/src/php/types/varray.php b/php/src/php/types/varray.php new file mode 100644 index 0000000..3a6b416 --- /dev/null +++ b/php/src/php/types/varray.php @@ -0,0 +1,24 @@ + [null, null, "tableau 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" => [null, null, "tableau à merger à celui-ci avant de calculer la liste effective des options", - # si merge_arrays et merge sont spécifiés tous les deux, "merge" est mergé après "merge_arrays" + "merges" => ["?array", null, "liste de tableaux contenant des paramètres et des options par défaut"], + "merge" => ["?array", null, "tableau contenant des paramètres et des options par défaut", + # si merges et merge sont spécifiés tous les deux, "merge" est mergé après "merges" ], + "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"], "name" => [null, null, "nom du programme, utilisé pour l'affichage de l'aide"], "purpose" => [null, null, "courte description de l'objet de ce programme"], @@ -51,34 +51,34 @@ class ref_args { ]; const DEF_SCHEMA = [ - "set_defaults" => [null, null, "tableau contenant des paramètres par défaut"], - "merge_arrays" => [null, null, "liste de tableaux à merger à celui-ci"], - "merge" => [null, null, "tableau à merger à celui-ci", - # si merge_arrays et merge sont spécifiés tous les deux, "merge" est mergé après "merge_arrays" + "merges" => ["array", null, "liste de tableaux contenant des paramètres et des options par défaut"], + "merge" => ["array", null, "tableau contenant des paramètres et des options par défaut", + # si merges et merge sont spécifiés tous les deux, "merge" est mergé après "merges" ], - "kind" => [null, null, "type de définition: 'option' ou 'command'"], - "arg" => [null, null, "type de l'argument attendu par l'option"], - "args" => [null, null, "type des arguments attendus par l'option", + "merge_after" => ["array", null, "tableau contenant des paramètres et des options supplémentaires"], + "extends" => ["string", null, "option que cette définition étend"], + "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é ], - "argsdesc" => [null, 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"], - "action" => [null, null, "fonction à appeler quand cette option est utilisée", + "argsdesc" => ["?string", null, "description textuelle des arguments, utilisé pour l'affichage de l'aide"], + "type" => ["schema", null, "type dans lequel convertir les arguments avant de les fournir à l'utilisateur"], + "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) ], - "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 ], - "property" => [null, null, "comme name mais force l'utilisation d'une propriété"], - "key" => [null, 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"], + "property" => ["?string", null, "comme name mais force l'utilisation d'une propriété"], + "key" => ["?key", null, "comme name mais force l'utilisation d'une clé"], "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"], - "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"]; diff --git a/php/src/ref/ref_cache.php b/php/src/ref/ref_cache.php new file mode 100644 index 0000000..d1dbf0a --- /dev/null +++ b/php/src/ref/ref_cache.php @@ -0,0 +1,11 @@ + + + true, + self::TEST => true, + ]; +} diff --git a/php/src/ref/schema/ref_schema.php b/php/src/ref/ref_schema.php similarity index 99% rename from php/src/ref/schema/ref_schema.php rename to php/src/ref/ref_schema.php index 37bea48..b1cda6d 100644 --- a/php/src/ref/schema/ref_schema.php +++ b/php/src/ref/ref_schema.php @@ -1,5 +1,5 @@ "bool", + "integer" => "int", + "flt" => "float", "double" => "float", "dbl" => "float", + "function" => "func", "callable" => "func", + ]; +} diff --git a/php/src/ref/schema/ref_types.php b/php/src/ref/schema/ref_types.php deleted file mode 100644 index d7ce1d4..0000000 --- a/php/src/ref/schema/ref_types.php +++ /dev/null @@ -1,11 +0,0 @@ - "bool", - "integer" => "int", - "flt" => "float", "double" => "float", "dbl" => "float", - "func" => "callable", "function" => "callable", - ]; -} diff --git a/php/src/str.php b/php/src/str.php index 6e09c51..0614ad1 100644 --- a/php/src/str.php +++ b/php/src/str.php @@ -110,7 +110,7 @@ class str { if ($s === null) return null; else return ucfirst($s); } - + static final function upperw(?string $s, ?string $delimiters=null): ?string { if ($s === null) return null; if ($delimiters !== null) return ucwords($s, $delimiters); @@ -392,6 +392,18 @@ class str { return implode($glue, $pieces); } + /** + * indenter chaque ligne de $text + */ + static function indent(?string $text, string $indent=" "): ?string { + if ($text === null) return null; + $indented = []; + foreach (explode("\n", $text) as $line) { + $indented[] = "$indent$line"; + } + return implode("\n", $indented); + } + const CAMEL_PATTERN0 = '/([A-Z0-9]+)$/A'; const CAMEL_PATTERN1 = '/([A-Z0-9]+)[A-Z]/A'; const CAMEL_PATTERN2 = '/([A-Z]?[^A-Z]+)/A'; @@ -426,7 +438,7 @@ class str { } elseif (preg_match(self::CAMEL_PATTERN2, $camel, $ms, PREG_OFFSET_CAPTURE)) { # préfixe en minuscule } else { - throw ValueException::invalid_kind($camel, "camel string"); + throw exceptions::invalid_value($camel, "camel string"); } $parts[] = strtolower($ms[1][0]); $index = intval($ms[1][1]) + strlen($ms[1][0]); diff --git a/php/src/text/Word.php b/php/src/text/Word.php index a91b03b..e5889d1 100644 --- a/php/src/text/Word.php +++ b/php/src/text/Word.php @@ -28,17 +28,23 @@ use nulib\ValueException; */ class Word { /** @var bool le mot est-il féminin? */ - private $fem; + private ?bool $fem; + /** @var string article "aucun", "aucune" */ + private ?string $aucun; + /** @var string article "un", "une" */ + private ?string $un; /** @var string article "le", "la", "l'" */ - private $le; + private ?string $le; /** @var string article "ce", "cet", "cette" */ - private $ce; + private ?string $ce; + /** @var string article "de", "d'" */ + private ?string $de; /** @var string article "du", "de la", "de l'" */ - private $du; + private ?string $du; /** @var string article "au", "à la", "à l'" */ - private $au; + private ?string $au; /** @var string le mot sans article */ - private $w; + private string $w; function __construct(string $spec, bool $adjective=false) { if (preg_match('/^f([eé]m(inin)?)?\s*:\s*/iu', $spec, $ms)) { @@ -57,28 +63,40 @@ class Word { $fem = null; } if (preg_match('/^l\'\s*/i', $spec, $ms) && $fem !== null) { + $aucun = $fem? "aucune ": "aucun "; + $un = $fem? "une ": "un "; $le = "l'"; $ce = "cet "; + $de = "d'"; $du = "de l'"; $au = "à l'"; $spec = substr($spec, strlen($ms[0])); } elseif (preg_match('/^la\s+/i', $spec, $ms)) { $fem = true; + $aucun = "aucune "; + $un = "une "; $le = "la "; $ce = "cette "; + $de = "de "; $du = "de la "; $au = "à la "; $spec = substr($spec, strlen($ms[0])); } elseif (preg_match('/^le\s+/i', $spec, $ms)) { $fem = false; + $aucun = "aucun "; + $un = "un "; $le = "le "; $ce = "ce "; + $de = "de "; $du = "du "; $au = "au "; $spec = substr($spec, strlen($ms[0])); } else { + $aucun = null; + $un = null; $le = null; $ce = null; + $de = null; $du = null; $au = null; } @@ -86,18 +104,29 @@ class Word { # si c'est un nom, il faut l'article et le genre if ($fem === null) { throw new ValueException("Vous devez spécifier le genre du nom"); - } elseif ($le === null || $du === null || $au === null) { + } elseif ($le === null) { throw new ValueException("Vous devez spécifier l'article du nom"); } } $this->fem = $fem; + $this->aucun = $aucun; + $this->un = $un; $this->le = $le; $this->ce = $ce; + $this->de = $de; $this->du = $du; $this->au = $au; $this->w = $spec; } + function isMasculin(): bool { + return !$this->fem; + } + + function isFeminin(): bool { + return $this->fem; + } + /** * retourner le mot sans article * @@ -168,15 +197,25 @@ class Word { return "$amount/$max ".$this->w($amount); } + function pronom(): string { + return $this->fem? "elle": "il"; + } + + function articleAucun(): ?string { + return $this->un; + } + + function articleUn(): ?string { + return $this->un; + } + /** retourner le mot avec l'article indéfini et la quantité */ function un(int $amount=1): string { $abs_amount = abs($amount); if ($abs_amount == 0) { - $aucun = $this->fem? "aucune ": "aucun "; - return $aucun.$this->w($amount); + return $this->aucun.$this->w($amount); } elseif ($abs_amount == 1) { - $un = $this->fem? "une ": "un "; - return $un.$this->w($amount); + return $this->un.$this->w($amount); } else { return "les $amount ".$this->w($amount); } @@ -193,6 +232,10 @@ class Word { } } + function articleLe(): ?string { + return $this->le; + } + function le(int $amount=1): string { $abs_amount = abs($amount); if ($abs_amount == 0) { @@ -214,6 +257,10 @@ class Word { } } + function articleCe(): string { + return $this->ce; + } + function ce(int $amount=1): string { $abs_amount = abs($amount); if ($abs_amount == 0) { @@ -235,6 +282,18 @@ class Word { } } + function articleDe(): string { + return $this->de; + } + + function _de(int $amount=1): string { + return $this->de.$this->w($amount); + } + + function articleDu(): string { + return $this->du; + } + function du(int $amount=1): string { $abs_amount = abs($amount); if ($abs_amount == 0) { @@ -256,6 +315,10 @@ class Word { } } + function articleAu(): string { + return $this->au; + } + function au(int $amount=1): string { $abs_amount = abs($amount); if ($abs_amount == 0) { diff --git a/php/src/txt.php b/php/src/txt.php index 2c5ef53..68a2794 100644 --- a/php/src/txt.php +++ b/php/src/txt.php @@ -105,7 +105,7 @@ class txt { if ($s === null) return null; return mb_strtoupper(mb_substr($s, 0, 1)).mb_substr($s, 1); } - + static final function upperw(?string $s, ?string $delimiters=null): ?string { if ($s === null) return null; if ($delimiters === null) $delimiters = " _-\t\r\n\f\v"; diff --git a/php/src/web/curl/CurlException.php b/php/src/web/curl/CurlException.php index 53fda92..52e87a0 100644 --- a/php/src/web/curl/CurlException.php +++ b/php/src/web/curl/CurlException.php @@ -5,9 +5,8 @@ use nulib\UserException; use Throwable; class CurlException extends UserException { - function __construct($ch, ?string $message=null, $code=0, ?Throwable $previous=null) { - if ($message === null) $message = "(unknown error)"; - $userMessage = $message; + function __construct($ch, $userMessage=null, $code=0, ?Throwable $previous=null) { + $userMessage ??= "erreur curl inconnue"; $techMessage = null; if ($ch !== null) { $parts = []; @@ -17,6 +16,7 @@ class CurlException extends UserException { if ($error != "") $parts[] = "error: $error"; if ($parts) $techMessage = implode(", ", $parts); } - parent::__construct($userMessage, $techMessage, $code, $previous); + parent::__construct($userMessage, $code, $previous); + $this->setTechMessage($techMessage); } } diff --git a/php/src/web/curl/curl.php b/php/src/web/curl/curl.php index 34f0677..fcc7530 100644 --- a/php/src/web/curl/curl.php +++ b/php/src/web/curl/curl.php @@ -12,11 +12,11 @@ class curl { if (!isset($curlOptions[CURLOPT_RETURNTRANSFER])) $curlOptions[CURLOPT_RETURNTRANSFER] = true; $extractHeaders = isset($curlOptions[CURLOPT_HEADER]) && $curlOptions[CURLOPT_HEADER]; $ch = curl_init(); - if ($ch === false) throw new CurlException(null, "init"); + if ($ch === false) throw new CurlException(null, "erreur curl lors de l'initialisation"); curl_setopt_array($ch, $curlOptions); try { $result = curl_exec($ch); - if ($result === false) throw new CurlException($ch); + if ($result === false) throw new CurlException($ch, "erreur curl lors du téléchargement"); if ($extractHeaders) { $info = curl_getinfo($ch); $headersSize = $info["header_size"]; diff --git a/php/src/web/params/F.php b/php/src/web/params/F.php index 18ed828..987b1bc 100644 --- a/php/src/web/params/F.php +++ b/php/src/web/params/F.php @@ -17,7 +17,7 @@ class F { } /** obtenir le paramètre $name en cherchant dans $_POST puis $_GET */ - static final function get($name, $default=null, bool $trim=false) { + static final function get($name, $default=null, bool $trim=false): ?string { if ($name === null || $name === false) $value = $default; elseif (array_key_exists($name, $_POST)) $value = $_POST[$name]; elseif (array_key_exists($name, $_GET)) $value = $_GET[$name]; @@ -47,6 +47,10 @@ class F { )); } + /** + * calculer la liste de tous les paramètres qui ont été passés. ensuite, + * fusionner le tableau $merge s'il est spécifié + */ static final function merge(?array $merge=null): array { $params = []; foreach (self::get_names() as $name) { @@ -56,7 +60,7 @@ class F { } /** - * retourner une liste des paramètres qui ont été passés, en les sélectionnant + * calculer une liste des paramètres qui ont été passés, en les sélectionnant * selon le contenu de $includes et $excludes. ensuite, fusionner le tableau * $merge s'il est spécifié * diff --git a/php/src/web/params/G.php b/php/src/web/params/G.php index ee37af2..2948712 100644 --- a/php/src/web/params/G.php +++ b/php/src/web/params/G.php @@ -15,7 +15,7 @@ class G { } /** obtenir le paramètre $name */ - static final function get($name, $default=null, bool $trim=false) { + static final function get($name, $default=null, bool $trim=false): ?string { $value = cl::get($_GET, $name, $default); if ($trim) $value = str::trim($value); return $value; @@ -26,7 +26,11 @@ class G { $_GET[$name] = $value; } - static final function xselect(?array $includes, ?array $excludes=null, ?array $merge=null): array { + static final function merge(?array $merge=null): array { + return cl::merge($_GET, $merge); + } + + static final function select(?array $includes, ?array $excludes=null, ?array $merge=null): array { return cl::merge(cl::xselect($_GET, $includes, $excludes), $merge); } } diff --git a/php/src/web/params/P.php b/php/src/web/params/P.php index 04faeec..eddb151 100644 --- a/php/src/web/params/P.php +++ b/php/src/web/params/P.php @@ -15,7 +15,7 @@ class P { } /** obtenir le paramètre $name */ - static final function get($name, $default=null, bool $trim=false) { + static final function get($name, $default=null, bool $trim=false): ?string { $value = cl::get($_POST, $name, $default); if ($trim) $value = str::trim($value); return $value; @@ -30,4 +30,12 @@ class P { static final function raw(): string { return file_get_contents("php://input"); } + + static final function merge(?array $merge=null): array { + return cl::merge($_POST, $merge); + } + + static final function select(?array $includes, ?array $excludes=null, ?array $merge=null): array { + return cl::merge(cl::xselect($_POST, $includes, $excludes), $merge); + } } diff --git a/php/src/web/params/R.php b/php/src/web/params/R.php index 9c1aae6..b7d38fa 100644 --- a/php/src/web/params/R.php +++ b/php/src/web/params/R.php @@ -15,7 +15,7 @@ class R { } /** obtenir le paramètre $name */ - static final function get($name, $default=null, bool $trim=false) { + static final function get($name, $default=null, bool $trim=false): ?string { $value = cl::get($_REQUEST, $name, $default); if ($trim) $value = str::trim($value); return $value; diff --git a/php/src/web/session.php b/php/src/web/session.php new file mode 100644 index 0000000..f475858 --- /dev/null +++ b/php/src/web/session.php @@ -0,0 +1,244 @@ + $duration, + ]); + self::$started_once = true; + + $creation_time = self::get(self::SESSION_CREATION_TIME, false); + if (!$creation_time) { + # création initiale + self::set(self::SESSION_CREATION_TIME, time()); + return true; + } elseif ($canSetCookies) { + # étendre la durée du cookie + $params = session_get_cookie_params(); + setcookie(session_name(), session_id(), time() + $duration, $params["path"], $params["domain"], $params["secure"], $params["httponly"]); + } + } + return false; + } + + /** + * enregistrer la session, la fermer et libérer son verrou. + * + * cette fonction peut être appelée avant une opération longue si on n'a plus + * besoin de la session. + * + * retourn true si la session a été fermée, false sinon. + */ + static final function close(): bool { + if (self::started()) { + session_write_close(); + return true; + } + return false; + } + + /** + * vider la session de toutes ses variables ($unsetOnly==true) ou la détruire + * ($unsetOnly==false). en cas de destruction de la session, supprimer aussi + * le cookie de session + * + * si $unsetOnly==true, refaire la variable SESSION_CREATION_TIME + */ + static final function destroy(bool $unsetOnly=false, bool $clearCookie=true): void { + self::start(); + if ($unsetOnly) { + session_unset(); + self::set(self::SESSION_CREATION_TIME, time()); + } else { + $canSetCookies = !headers_sent(); + if ($clearCookie && $canSetCookies && ini_get("session.use_cookies")) { + $params = session_get_cookie_params(); + setcookie(session_name(), '', time() - 42000, $params["path"], $params["domain"], $params["secure"], $params["httponly"]); + } + session_destroy(); + } + } + + /** + * Vider la session de toutes les clés spécifiées dans $keys qui ne sont pas + * mentionnées dans $keeps + * + * Si $keys vaut null, toutes les clés sont supprimées comme avec destroy(true) + * notamment, cela signifie que la variable SESSION_CREATION_TIME est refaite + */ + static final function unset_keys(?array $keys, ?array $keeps=null): void { + $updateSessionCreationTime = false; + if ($keys === null) { + $keys = array_keys($_SESSION); + $updateSessionCreationTime = true; + } + if ($keeps !== null) $keys = array_diff($keys, $keeps); + foreach ($keys as $key) { + unset($_SESSION[$key]); + } + if ($updateSessionCreationTime) { + self::set(self::SESSION_CREATION_TIME, time()); + } + } + + /** vérifier si la session est démarrée et si la clé spécifiée existe. */ + static final function has($key): bool { + if ($key === null || $key === false) return false; + return isset($_SESSION) && array_key_exists($key, $_SESSION); + } + + /** obtenir la valeur associée à la clé spécifiée si la session est démarrée. */ + static final function get(string $key, $default=null) { + if (!isset($_SESSION)) return $default; + return cl::get($_SESSION, $key, $default); + } + + /** + * mettre à jour la valeur d'une variable de session. + * + * ne pas chercher à savoir si la session est démarrée ou non + */ + static final function set(string $key, $value): void { + $_SESSION[$key] = $value; + } + + /** + * comme {@link set()} mais rouvrir automatiquement la session si nécessaire, + * à condition qu'elle aie déjà été ouverte une fois + */ + static final function setx(string $key, $value): void { + $close = !self::started() && self::started_once(); + if ($close) self::start(); + self::set($key, $value); + if ($close) self::close(); + } + + /** + * supprimer une variable de session. + * + * ne pas chercher à savoir si la session est démarrée ou non + */ + static final function del(string $key): void { + unset($_SESSION[$key]); + } + + /** + * comme {@link del()} mais rouvrir automatiquement la session si nécessaire, + * à condition qu'elle aie déjà été ouverte une fois + */ + static final function delx(string $key): void { + $close = !self::started() && self::started_once(); + if ($close) self::start(); + self::del($key); + if ($close) self::close(); + } + + /** vérifier si chemin de clé spécifié existe dans la session. */ + static final function phas($pkey): bool { + return isset($_SESSION) && cl::phas($_SESSION, $pkey); + } + + /** obtenir la valeur associée au chemin de clé spécifié si la session est démarrée. */ + static final function pget($pkey, $default=null) { + return isset($_SESSION) && cl::pget($_SESSION, $pkey, $default); + } + + /** + * mettre à jour la valeur correspondant au chemin de clé spécifié. + * + * ne pas chercher à savoir si la session est démarrée ou non + */ + static final function pset($pkey, $value): void { + cl::pset($_SESSION, $pkey, $value); + } + + /** + * comme {@link pset()} mais rouvrir automatiquement la session si nécessaire, + * à condition qu'elle aie déjà été ouverte une fois + */ + static final function psetx($pkey, $value): void { + $close = !self::started() && self::started_once(); + if ($close) self::start(); + self::pset($pkey, $value); + if ($close) self::close(); + } + + /** + * supprimer la variable au chemin de clé spécifié. + * + * ne pas chercher à savoir si la session est démarrée ou non + */ + static final function pdel($pkey): void { + cl::pdel($_SESSION, $pkey); + } + + /** + * comme {@link pdel()} mais rouvrir automatiquement la session si nécessaire, + * à condition qu'elle aie déjà été ouverte une fois + */ + static final function pdelx(string $key): void { + $close = !self::started() && self::started_once(); + if ($close) self::start(); + self::pdel($key); + if ($close) self::close(); + } +} diff --git a/php/tbin/.gitignore b/php/tbin/.gitignore index f3b938a..e20f16c 100644 --- a/php/tbin/.gitignore +++ b/php/tbin/.gitignore @@ -1 +1,4 @@ +/devel/ /*.db +/*.cache +/*.log diff --git a/php/tbin/cachectl.php b/php/tbin/cachectl.php new file mode 100755 index 0000000..4afa7a5 --- /dev/null +++ b/php/tbin/cachectl.php @@ -0,0 +1,7 @@ +#!/usr/bin/php + "SUBJECT BODY -t RECIPIENT", + "merge" => parent::ARGS, + ["-F", "--from", "args" => 1, "name" => "from", "argsdesc" => "MAILFROM"], + ["-t", "--to", "args" => 1, "action" => "--add", "name" => "to", "argsdesc" => "RECIPIENT"], + ["-c", "--cc", "args" => 1, "action" => "--add", "name" => "cc", "argsdesc" => "RECIPIENT"], + ["-b", "--bcc", "args" => 1, "action" => "--add", "name" => "bcc", "argsdesc" => "RECIPIENT"], + ["args" => 2, "name" => "args"], + ]; + + protected $to, $cc, $bcc, $from; + + function main() { + $subject = cv::not_null($this->args[0] ?? null, "subject"); + $body = cv::not_null($this->args[1] ?? null, "body"); + mailer::send($this->to, $subject, $body, $this->cc, $this->bcc, $this->from); + } +}); diff --git a/php/tbin/steam-train.php b/php/tbin/steam-train.php new file mode 100755 index 0000000..cba3bcf --- /dev/null +++ b/php/tbin/steam-train.php @@ -0,0 +1,14 @@ +#!/usr/bin/php + "../vendor/autoload.php", + "bindir" => "../vendor/bin", + ]; +} +SteamTrainApp::run(); diff --git a/php/tbin/test-application.php b/php/tbin/test-application.php new file mode 100755 index 0000000..a196a27 --- /dev/null +++ b/php/tbin/test-application.php @@ -0,0 +1,78 @@ +#!/usr/bin/php + "tester la gestion des arguments", + "usage" => "-A|-a|-b", + + "merge" => parent::ARGS, + "sections" => [ + [ + "title" => "Section X", + "show" => false, + ["group", + ["-X:", "--setx", "args" => "int", "name" => "x", + "help" => "spécifier x", + ], + ["--setx10", "name" => "x", "value" => 10], + ["--setx20", "name" => "x", "value" => 20], + ], + ["-x", "--incx", "name" => "x"], + ["-y", "--decx", "name" => "x", "inverse" => true], + ], + ], + ["group", + ["-A:", "--seta", "args" => "int", "name" => "a", + "help" => "spécifier a", + ], + ["--seta10", "name" => "a", "value" => 10], + ["--seta20", "name" => "a", "value" => 20], + ], + ["-a", "--inca", "name" => "a", + "help" => "incrémenter a", + ], + ["-b", "--deca", "name" => "a", "inverse" => true, + "help" => "décrémenter a", + ], + ["-D::", "--override", + "help" => "++remplace celui de la section principale", + ], + ["-1:first", "--one", "help" => "un argument"], + ["-2:first,second", "--two", "help" => "deux arguments"], + ["-3", "args" => ""], + //["args" => [["value", "value"]], "name" => "args"], + //["args" => ["value", ["value"]], "name" => "args"], + //["args" => ["value", "value"], "name" => "args"], + ]; + + private ?int $a = null; + private ?int $x = null; + private ?string $override = null; + private ?string $one = null; + private ?array $two = null; + + function main() { + $profile = app::get_profile($productionMode); + $profile = self::get_profile($profile); + $productionMode = $productionMode? "production": "development"; + msg::info("profile=$profile ($productionMode)"); + $debug = app::is_debug()? "DEBUG": "non"; + msg::info("debug=$debug"); + + msg::info([ + "variables:", + "\na=", var_export($this->a, true), + "\nx=", var_export($this->x, true), + "\noverride=", var_export($this->override, true), + "\none=", var_export($this->one, true), + "\ntwo=", var_export($this->two, true), + "\nargs=", var_export($this->args, true), + ]); + } +}); diff --git a/php/tbin/test-cache.php b/php/tbin/test-cache.php new file mode 100755 index 0000000..ebc91cd --- /dev/null +++ b/php/tbin/test-cache.php @@ -0,0 +1,82 @@ +#!/usr/bin/php +get()); + if ($dumpInfos) { + yaml::dump($cache->getInfos()); + } +} + +//system("rm -f *.cache .*.cache"); + +$what = [ + "null", + "one", + "two", + "three", +]; +$duration = 10; + +if (in_array("null", $what)) { + $null = new CacheFile("null", null, [ + "duration" => $duration, + ]); + show("null", $null); +} + +if (in_array("one", $what)) { + $one = new class("one", null, [ + "duration" => $duration, + ]) extends CacheFile { + protected function compute() { + return 1; + } + }; + show("one", $one); +} + +if (in_array("two", $what)) { + $two = new CacheFile("two", new DataCacheData(null, function () { + return 2; + }), [ + "duration" => $duration, + ]); + show("two", $two); +} + +if (in_array("three", $what)) { + $data31 = new DataCacheData("data31name", function () { + return 31; + }); + + $data32 = new DataCacheData(null, function () { + return 32; + }); + + $three = new CacheFile("three", [ + "data31" => $data31, + $data31, # name=data31name + "data32" => $data32, + $data32, # name="" + ]); + Txx("three.0=", $three->get("data31")); + Txx("three.1=", $three->get("data31name")); + Txx("three.2=", $three->get("data32")); + Txx("three.3=", $three->get("")); +} diff --git a/php/tbin/test-exceptions.php b/php/tbin/test-exceptions.php new file mode 100755 index 0000000..ad17757 --- /dev/null +++ b/php/tbin/test-exceptions.php @@ -0,0 +1,65 @@ +#!/usr/bin/php + "tester l'affichage des exception", + + "merge" => parent::ARGS, + ]; + + function fart(): void { + throw new RuntimeException("fart"); + } + + function prout(): void { + try { + $this->fart(); + } catch (Exception $e) { + throw new RuntimeException("prout", $e->getCode(), $e); + } + } + + function main() { + try { + throw new Exception("exception normale"); + } catch (Exception $e) { + msg::info("summary: ". exceptions::get_summary($e)); + msg::error($e); + } + try { + try { + $this->prout(); + } catch (Exception $e) { + throw new Exception("exception normale", $e->getCode(), $e); + } + } catch (Exception $e) { + msg::info("summary: ". exceptions::get_summary($e)); + msg::error($e); + } + try { + throw exceptions::invalid_value("valeur", $kind) + ->setTechMessage("message technique"); + } catch (Exception $e) { + msg::info("summary: ". exceptions::get_summary($e)); + msg::error($e); + } + try { + try { + $this->prout(); + } catch (Exception $e) { + throw exceptions::invalid_value("valeur", $kind, null, $e) + ->setTechMessage("message technique"); + } + } catch (Exception $e) { + msg::info("summary: ". exceptions::get_summary($e)); + msg::error($e); + } + } +}); diff --git a/php/tbin/test-mail.php b/php/tbin/test-mail.php new file mode 100644 index 0000000..d37b25b --- /dev/null +++ b/php/tbin/test-mail.php @@ -0,0 +1,19 @@ + "test de mail", + "body" => << "moi même", +]; +mailer::tsend($template, $data, "jephte.clain@gmail.com"); diff --git a/php/tbin/test_mysql.php b/php/tbin/test-mysql.php similarity index 90% rename from php/tbin/test_mysql.php rename to php/tbin/test-mysql.php index e2eb555..043d924 100644 --- a/php/tbin/test_mysql.php +++ b/php/tbin/test-mysql.php @@ -7,9 +7,9 @@ use nulib\db\CapacitorChannel; use nulib\db\mysql\Mysql; use nulib\db\mysql\MysqlStorage; use nulib\output\msg; -use nulib\output\std\StdMessenger; +use nulib\output\std\ConsoleMessenger; -msg::set_messenger_class(StdMessenger::class); +msg::set_messenger_class(ConsoleMessenger::class); $db = new Mysql([ "type" => "mysql", diff --git a/php/tbin/test-output-forever.php b/php/tbin/test-output-forever.php new file mode 100755 index 0000000..6bee7b2 --- /dev/null +++ b/php/tbin/test-output-forever.php @@ -0,0 +1,18 @@ +#!/usr/bin/php + "output-forever.log", +])); + +$index = 1; +while (true) { + msg::info("info $index"); + $index++; + sleep(1); +} diff --git a/php/tbin/test-output.php b/php/tbin/test-output.php new file mode 100755 index 0000000..78aa8d0 --- /dev/null +++ b/php/tbin/test-output.php @@ -0,0 +1,458 @@ +#!/usr/bin/php +title("title"); + sleep(5); + $msg->info("info"); + sleep(5); + $msg->info("info"); + $msg->end(); + + echo date("Y-m-d\\TH:i:s.u")."\n"; + $msg->action("action"); + sleep(5); + $msg->info("info"); + sleep(5); + $msg->info("info"); + $msg->adone(); + + echo date("Y-m-d\\TH:i:s.u")."\n"; + $msg->action("action"); + sleep(5); + $msg->asuccess(); + + echo date("Y-m-d\\TH:i:s.u")."\n"; + $msg->action("action"); + $msg->asuccess("plouf1"); + + echo date("Y-m-d\\TH:i:s.u")."\n"; + $msg->action("action"); + sleep(5); + $msg->asuccess("plouf2"); +} + +if ($titles) { + $msg->title("title0"); + $msg->desc("desc0"); + $msg->title("title1"); + $msg->desc("desc1"); + $msg->print("print under title1"); + $msg->end(); + $msg->print("print under title0"); + $msg->end(); + $msg->print("print out of title"); +} + +if ($maxTitleLevel) { + $msg->info("test maxTitleLevel"); + $msg->title("1first", function(IMessenger $msg) { + $msg->info("1one"); + $msg->end(); + $msg->info("1two"); + $msg->end(); + $msg->info("1three"); + }); + $msg->info("0one"); + $msg->end(); + $msg->info("0two"); + $msg->end(); + $msg->info("0three"); + + $msg->title("2first", function(IMessenger $msg) { + $msg->title("3second", function(IMessenger $msg) { + $msg->title("4third", function(IMessenger $msg) { + $msg->info("4one"); + $msg->end(); + $msg->info("4two"); + $msg->end(); + $msg->info("4three"); + }); + $msg->info("3four"); + $msg->end(); + $msg->info("3five"); + $msg->end(); + $msg->info("3six"); + }); + $msg->info("2seven"); + $msg->end(); + $msg->info("2eight"); + $msg->end(); + $msg->info("2nine"); + }); + $msg->info("1one"); + $msg->end(); + $msg->info("1two"); + $msg->end(); + $msg->info("1three"); +} + +if ($actions) { + $msg->desc("action avec step"); + $msg->action("action avec step"); + $msg->step("step"); + $msg->asuccess("action success"); + + $msg->action("action avec step"); + $msg->step("step"); + $msg->afailure("action failure"); + + $msg->action("action avec step"); + $msg->step("step"); + $msg->adone("action neutral"); + + $msg->desc("actions sans step"); + $msg->action("action sans step"); + $msg->asuccess("action success"); + + $msg->action("action sans step"); + $msg->afailure("action failure"); + + $msg->action("action sans step"); + $msg->adone("action neutral"); + + $msg->desc("actions imbriquées"); + $msg->action("action0"); + $msg->action("action1"); + $msg->action("action2"); + $msg->asuccess("action2 success"); + $msg->asuccess("action1 success"); + $msg->asuccess("action0 success"); + + $msg->desc("action avec step, sans messages"); + $msg->action("action avec step, sans messages, success"); + $msg->step("step"); + $msg->asuccess(); + + $msg->action("action avec step, sans messages, failure"); + $msg->step("step"); + $msg->afailure(); + + $msg->action("action avec step, sans messages, done"); + $msg->step("step"); + $msg->adone(); + + $msg->desc("action sans step, sans messages"); + $msg->action("action sans step, sans messages, success"); + $msg->asuccess(); + + $msg->action("action sans step, sans messages, failure"); + $msg->afailure(); + + $msg->action("action sans step, sans messages, done"); + $msg->adone(); + + $msg->desc("actions imbriquées, sans messages"); + $msg->action("action0"); + $msg->action("action1"); + $msg->action("action2"); + $msg->asuccess(); + $msg->asuccess(); + $msg->asuccess(); +} + +if ($maxActionLevel) { + $msg->info("test maxActionLevel"); + $msg->action("first1", function (IMessenger $msg) { + $msg->info("one"); + $msg->end(); + $msg->info("two"); + $msg->end(); + $msg->info("three"); + }); + $msg->action("first2", function (IMessenger $msg) { + $msg->info("one"); + $msg->adone(); + $msg->info("two"); + $msg->adone(); + $msg->info("three"); + }); + $msg->action("first3", function (IMessenger $msg) { + $msg->action("second", function (IMessenger $msg) { + $msg->action("third", function (IMessenger $msg) { + $msg->info("one"); + $msg->end(); + $msg->info("two"); + $msg->end(); + $msg->info("three"); + }); + $msg->info("four"); + $msg->end(); + $msg->info("five"); + $msg->end(); + $msg->info("six"); + }); + $msg->info("seven"); + $msg->end(); + $msg->info("eight"); + $msg->end(); + $msg->info("nine"); + }); + $msg->info("ten"); + $msg->end(); + $msg->info("eleven"); + $msg->end(); + $msg->info("twelve"); + + $msg->action("first4", function (IMessenger $msg) { + $msg->action("second", function (IMessenger $msg) { + $msg->action("third", function (IMessenger $msg) { + $msg->info("one"); + $msg->adone(); + $msg->info("two"); + $msg->adone(); + $msg->info("three"); + }); + $msg->info("four"); + $msg->adone(); + $msg->info("five"); + $msg->adone(); + $msg->info("six"); + }); + $msg->info("seven"); + $msg->adone(); + $msg->info("eight"); + $msg->adone(); + $msg->info("nine"); + }); + $msg->info("ten"); + $msg->adone(); + $msg->info("eleven"); + $msg->adone(); + $msg->info("twelve"); +} + +if ($levels) { + $msg->info("info"); + $msg->note("note"); + $msg->warning("warning"); + $msg->error("error"); +} + +if ($complete) { + $msg->section("section", function (IMessenger $msg) { + $msg->title("title", function (IMessenger $msg) { + $msg->desc("desc"); + $msg->print("print"); + + $msg->desc("action avec step"); + $msg->action("action avec step", function (IMessenger $msg) { + $msg->step("step"); + $msg->asuccess("action success"); + }); + + $msg->action("action avec step", function (IMessenger $msg) { + $msg->step("step"); + $msg->afailure("action failure"); + }); + + $msg->action("action avec step", function (IMessenger $msg) { + $msg->step("step"); + $msg->adone("action done"); + }); + + $msg->desc("actions sans step"); + $msg->action("action sans step", function (IMessenger $msg) { + $msg->asuccess("action success"); + }); + + $msg->action("action sans step", function (IMessenger $msg) { + $msg->afailure("action failure"); + }); + + $msg->action("action sans step", function (IMessenger $msg) { + $msg->adone("action done"); + }); + + $msg->desc("actions imbriquées"); + $msg->action("action0", function (IMessenger $msg) { + $msg->action("action1", function (IMessenger $msg) { + $msg->action("action2", function (IMessenger $msg) { + $msg->asuccess("action2 success"); + }); + $msg->asuccess("action1 success"); + }); + $msg->asuccess("action0 success"); + }); + + $msg->desc("action avec step, sans messages"); + $msg->action("action avec step, sans messages, success", function (IMessenger $msg) { + $msg->step("step"); + $msg->asuccess(); + }); + + $msg->action("action avec step, sans messages, failure", function (IMessenger $msg) { + $msg->step("step"); + $msg->afailure(); + }); + + $msg->action("action avec step, sans messages, done", function (IMessenger $msg) { + $msg->step("step"); + $msg->adone(); + }); + + $msg->desc("action sans step, sans messages"); + $msg->action("action sans step, sans messages, success", function (IMessenger $msg) { + $msg->asuccess(); + }); + + $msg->action("action sans step, sans messages, failure", function (IMessenger $msg) { + $msg->afailure(); + }); + + $msg->action("action sans step, sans messages, done", function (IMessenger $msg) { + $msg->adone(); + }); + + $msg->desc("actions imbriquées, sans messages"); + $msg->action("action0", function (IMessenger $msg) { + $msg->action("action1", function (IMessenger $msg) { + $msg->action("action2", function (IMessenger $msg) { + $msg->asuccess(); + }); + $msg->asuccess(); + }); + $msg->asuccess(); + }); + + $msg->desc("action avec step, avec code de retour"); + $msg->action("action avec step, avec code de retour true", function (IMessenger $msg) { + $msg->step("step"); + return true; + }); + + $msg->action("action avec step, avec code de retour false", function (IMessenger $msg) { + $msg->step("step"); + return false; + }); + + $msg->action("action avec step, avec code de retour autre", function (IMessenger $msg) { + $msg->step("step"); + return "autre"; + }); + + $msg->action("action avec step, avec code de retour null", function (IMessenger $msg) { + $msg->step("step"); + }); + + $msg->desc("action sans step, avec code de retour"); + $msg->action("action sans step, avec code de retour true", function (IMessenger $msg) { + return true; + }); + + $msg->action("action sans step, avec code de retour false", function (IMessenger $msg) { + return false; + }); + + $msg->action("action sans step, avec code de retour autre", function (IMessenger $msg) { + return "autre"; + }); + + # ici, il n'y aura pas de message du tout + $msg->action("action sans step, avec code de retour null", function (IMessenger $msg) { + }); + + $msg->info("info"); + $msg->note("note"); + $msg->warning("warning"); + $msg->error("error"); + }); + }); +} + +if ($multilines) { + $msg->section("multi-line\nsection", function (IMessenger $msg) { + $msg->title("multi-line\ntitle"); + $msg->title("another\ntitle"); + + $msg->print("multi-line\nprint"); + $msg->info("multi-line\ninfo"); + $msg->action("multi-line\naction"); + $msg->asuccess(); + $msg->action("multi-line\naction"); + $msg->step("multi-line\nstep"); + $msg->afailure(); + $msg->action("multi-line\naction"); + $msg->step("multi-line\nstep"); + $msg->asuccess("multi-line\nsuccess"); + $msg->action("multi-line\naction"); + $msg->step("multi-line\nstep"); + $msg->adone("multi-line\ndone"); + + $msg->end(); + $msg->end(); + }); +} + +if ($exceptions) { + $msg->section("Exceptions", function (IMessenger $msg) { + $e = new Exception("message"); + $u1 = new UserException("userMessage"); + $u2 = (new UserException("userMessage"))->setTechMessage("techMessage"); + $msg->title("avec message", function (IMessenger $msg) use ($e, $u1, $u2) { + $msg->info(["exception", $e]); + $msg->info(["userException1", $u1]); + $msg->info(["userException2", $u2]); + }); + $msg->title("sans message", function (IMessenger $msg) use ($e, $u1, $u2) { + $msg->info($e); + $msg->info($u1); + $msg->info($u2); + }); + }); +} diff --git a/php/tbin/test-output1.php b/php/tbin/test-output1.php new file mode 100755 index 0000000..8a2f78e --- /dev/null +++ b/php/tbin/test-output1.php @@ -0,0 +1,56 @@ +#!/usr/bin/php + parent::ARGS, + + ["-c", "--con", "name" => "use", "value" => self::CON], + ["-l", "--log", "name" => "use", "value" => self::LOG], + ["-m", "--msg", "name" => "use", "value" => self::MSG], + ]; + + protected int $use = self::MSG; + + function main() { + switch ($this->use) { + case self::MSG: + $msg = new ProxyMessenger(); + $msg->addMessenger(con::get()); + $msg->addMessenger(new LogMessenger()); + break; + case self::CON: + $msg = con::get(); + break; + case self::LOG: + $msg = new LogMessenger(); + break; + } + $msg->info("test d'information"); + $msg->action("attente de 2 secondes", function (IMessenger $msg) { + sleep(1); + $msg->asuccess("1 seconde"); + sleep(1); + $msg->asuccess("1 seconde"); + }); + $msg->action("attente de 2 secondes", function (IMessenger $msg) { + sleep(1); + $msg->info("1 seconde"); + sleep(1); + $msg->info("1 seconde"); + }); + $msg->info("fin de test-appctl"); + } +}); diff --git a/php/tbin/test_pgsql.php b/php/tbin/test-pgsql.php similarity index 100% rename from php/tbin/test_pgsql.php rename to php/tbin/test-pgsql.php diff --git a/php/tbin/test_sqlite.php b/php/tbin/test-sqlite.php similarity index 100% rename from php/tbin/test_sqlite.php rename to php/tbin/test-sqlite.php diff --git a/php/tests/app/appTest.php b/php/tests/app/appTest.php new file mode 100644 index 0000000..fe19a9a --- /dev/null +++ b/php/tests/app/appTest.php @@ -0,0 +1,141 @@ + $projdir, + "vendor" => [ + "bindir" => "$projdir/vendor/bin", + "autoload" => "$projdir/vendor/autoload.php", + ], + "projcode" => "nulib-base", + "cwd" => $cwd, + "datadir" => "$projdir/devel", + "etcdir" => "$projdir/devel/etc", + "vardir" => "$projdir/devel/var", + "logdir" => "$projdir/devel/log", + "profile" => "devel", + "facts" => null, + "debug" => null, + "appgroup" => null, + "name" => "my-application1", + "title" => null, + ], $app1->getParams()); + + $app2 = myapp::with(MyApplication2::class, $app1); + self::assertSame([ + "projdir" => $projdir, + "vendor" => [ + "bindir" => "$projdir/vendor/bin", + "autoload" => "$projdir/vendor/autoload.php", + ], + "projcode" => "nulib-base", + "cwd" => $cwd, + "datadir" => "$projdir/devel", + "etcdir" => "$projdir/devel/etc", + "vardir" => "$projdir/devel/var", + "logdir" => "$projdir/devel/log", + "profile" => "devel", + "facts" => null, + "debug" => null, + "appgroup" => null, + "name" => "my-application2", + "title" => null, + ], $app2->getParams()); + } + + function testInit() { + $projdir = config::get_projdir(); + $cwd = getcwd(); + + myapp::reset(); + myapp::init(MyApplication1::class); + self::assertSame([ + "projdir" => $projdir, + "vendor" => [ + "bindir" => "$projdir/vendor/bin", + "autoload" => "$projdir/vendor/autoload.php", + ], + "projcode" => "nulib-base", + "cwd" => $cwd, + "datadir" => "$projdir/devel", + "etcdir" => "$projdir/devel/etc", + "vardir" => "$projdir/devel/var", + "logdir" => "$projdir/devel/log", + "profile" => "devel", + "facts" => null, + "debug" => null, + "appgroup" => null, + "name" => "my-application1", + "title" => null, + ], myapp::get()->getParams()); + + myapp::init(MyApplication2::class); + self::assertSame([ + "projdir" => $projdir, + "vendor" => [ + "bindir" => "$projdir/vendor/bin", + "autoload" => "$projdir/vendor/autoload.php", + ], + "projcode" => "nulib-base", + "cwd" => $cwd, + "datadir" => "$projdir/devel", + "etcdir" => "$projdir/devel/etc", + "vardir" => "$projdir/devel/var", + "logdir" => "$projdir/devel/log", + "profile" => "devel", + "facts" => null, + "debug" => null, + "appgroup" => null, + "name" => "my-application2", + "title" => null, + ], myapp::get()->getParams()); + } + } +} + +namespace nulib\app\impl { + + use nulib\app\app; + use nulib\app\cli\Application; + use nulib\os\path; + + class config { + const PROJDIR = __DIR__.'/../../..'; + + static function get_projdir(): string { + return path::abspath(self::PROJDIR); + } + } + + class myapp extends app { + static function reset(): void { + self::$app = null; + } + } + + class MyApplication1 extends Application { + const PROJDIR = config::PROJDIR; + + function main() { + } + } + class MyApplication2 extends Application { + const PROJDIR = null; + + function main() { + } + } +} diff --git a/php/tests/app/args/AodefTest.php b/php/tests/app/args/AodefTest.php new file mode 100644 index 0000000..db6b6d1 --- /dev/null +++ b/php/tests/app/args/AodefTest.php @@ -0,0 +1,172 @@ +setup1(); + $aodef->setup2(); + #var_export($aodef->debugInfos()); #XXX + self::assertSame($options, $aodef->getOptions()); + self::assertSame($haveShortOptions, $aodef->haveShortOptions, "haveShortOptions"); + self::assertSame($haveLongOptions, $aodef->haveLongOptions, "haveLongOptions"); + self::assertSame($isCommand, $aodef->isCommand, "isCommand"); + self::assertSame($haveArgs, $aodef->haveArgs, "haveArgs"); + self::assertSame($minArgs, $aodef->minArgs, "minArgs"); + self::assertSame($maxArgs, $aodef->maxArgs, "maxArgs"); + self::assertSame($argsdesc, $aodef->argsdesc, "argsdesc"); + } + + function testArgsNone() { + $aodef = new Aodef(["-o"]); + self::assertArg($aodef, + ["-o"], + true, false, false, + false, 0, 0, ""); + + $aodef = new Aodef(["--longo"]); + self::assertArg($aodef, + ["--longo"], + false, true, false, + false, 0, 0, ""); + + $aodef = new Aodef(["-o", "--longo"]); + self::assertArg($aodef, + ["-o", "--longo"], + true, true, false, + false, 0, 0, ""); + } + + function testArgsMandatory() { + $aodef = new Aodef(["-o:", "--longo"]); + self::assertArg($aodef, + ["-o", "--longo"], + true, true, false, + true, 1, 1, "VALUE"); + + $aodef = new Aodef(["-a:", "-b:"]); + self::assertArg($aodef, + ["-a", "-b"], + true, false, false, + true, 1, 1, "VALUE"); + + $aodef = new Aodef(["-a:", "-b::"]); + self::assertArg($aodef, + ["-a", "-b"], + true, false, false, + true, 1, 1, "VALUE"); + + $aodef = new Aodef(["-a::", "-b:"]); + self::assertArg($aodef, + ["-a", "-b"], + true, false, false, + true, 1, 1, "VALUE"); + + $aodef = new Aodef(["-o", "--longo", "args" => true]); + self::assertArg($aodef, + ["-o", "--longo"], + true, true, false, + true, 1, 1, "VALUE"); + + $aodef = new Aodef(["-o", "--longo", "args" => 1]); + self::assertArg($aodef, + ["-o", "--longo"], + true, true, false, + true, 1, 1, "VALUE"); + + $aodef = new Aodef(["-o", "--longo", "args" => "value"]); + self::assertArg($aodef, + ["-o", "--longo"], + true, true, false, + true, 1, 1, "VALUE"); + + $aodef = new Aodef(["-o", "--longo", "args" => ["value"]]); + self::assertArg($aodef, + ["-o", "--longo"], + true, true, false, + true, 1, 1, "VALUE"); + } + + function testArgsOptional() { + $aodef = new Aodef(["-o::", "--longo"]); + self::assertArg($aodef, + ["-o", "--longo"], + true, true, false, + true, 0, 1, "[VALUE]"); + + $aodef = new Aodef(["-o", "--longo", "args" => [["value"]]]); + self::assertArg($aodef, + ["-o", "--longo"], + true, true, false, + true, 0, 1, "[VALUE]"); + + $aodef = new Aodef(["-o", "--longo", "args" => [[null]]]); + self::assertArg($aodef, + ["-o", "--longo"], + true, true, false, + true, 0, PHP_INT_MAX, "[VALUEs...]"); + + $aodef = new Aodef(["-o", "--longo", "args" => ["value", null]]); + self::assertArg($aodef, + ["-o", "--longo"], + true, true, false, + true, 1, PHP_INT_MAX, "VALUE [VALUEs...]"); + + $aodef = new Aodef(["-o", "--longo", "args" => "*"]); + self::assertArg($aodef, + ["-o", "--longo"], + true, true, false, + true, 0, PHP_INT_MAX, "[VALUEs...]"); + + $aodef = new Aodef(["-o", "--longo", "args" => "+"]); + self::assertArg($aodef, + ["-o", "--longo"], + true, true, false, + true, 1, PHP_INT_MAX, "VALUE [VALUEs...]"); + } + + function testMerge() { + $BASE = ["-o:", "--longo"]; + + $aodef = new Aodef([ + "merge" => $BASE, + "add" => ["-a", "--longa"], + "remove" => ["-o", "--longo"], + ]); + self::assertArg($aodef, + ["-a", "--longa"], + true, true, false, + false, 0, 0, ""); + + $aodef = new Aodef([ + "merge" => $BASE, + "add" => ["-a", "--longa"], + "remove" => ["-o", "--longo"], + "-x", + ]); + self::assertArg($aodef, + ["-a", "--longa", "-x"], + true, true, false, + false, 0, 0, ""); + } + + function testArgsdesc() { + $aodef = new Aodef(["-o:value", "--longo"]); + self::assertArg($aodef, + ["-o", "--longo"], + true, true, false, + true, 1, 1, "VALUE"); + + $aodef = new Aodef(["-o:file,suffix", "--longo"]); + self::assertArg($aodef, + ["-o", "--longo"], + true, true, false, + true, 2, 2, "FILE SUFFIX"); + } +} diff --git a/php/tests/app/args/AolistTest.php b/php/tests/app/args/AolistTest.php new file mode 100644 index 0000000..acf0402 --- /dev/null +++ b/php/tests/app/args/AolistTest.php @@ -0,0 +1,60 @@ + "value", + ["--opt"], + ["group", + ["--gopt1"], + ["--gopt2"], + ], + "sections" => [ + [ + ["--s0opt"], + ["group", + ["--s0gopt1"], + ["--s0gopt2"], + ], + ], + "ns" => [ + ["--nsopt"], + ["group", + ["--nsgopt1"], + ["--nsgopt2"], + ], + ], + ], + ]) extends Aolist {}; + + echo "$aolist\n"; + self::assertTrue(true); + } +} diff --git a/php/tests/app/args/SimpleAolistTest.php b/php/tests/app/args/SimpleAolistTest.php new file mode 100644 index 0000000..750e401 --- /dev/null +++ b/php/tests/app/args/SimpleAolistTest.php @@ -0,0 +1,75 @@ + [ + ["-o", "--longo"], + ], + ]); + echo "$aolist\n"; #XXX + + $aolist = new SimpleAolist([ + ["-o", "--longo"], + ["-o", "--longx"], + ]); + echo "$aolist\n"; #XXX + + $aolist = new SimpleAolist([ + ["-o", "--longo"], + ["-o"], + ["--longo"], + ]); + echo "$aolist\n"; #XXX + + self::assertTrue(true); + } + + function testExtends() { + $ARGS0 = [ + ["-o:", "--longo", + "name" => "desto", + "help" => "help longo" + ], + ["-a:", "--longa", + "name" => "desta", + "help" => "help longa" + ], + ]; + $ARGS = [ + "merge" => $ARGS0, + ["extends" => "-a", + "remove" => ["--longa"], + "add" => ["--desta"], + "help" => "help desta" + ], + ]; + //$aolist0 = new SimpleArgDefs($ARGS0); + //echo "$aolist0\n"; #XXX + $aolist = new SimpleAolist($ARGS); + echo "$aolist\n"; #XXX + + self::assertTrue(true); + } + + function testRemainingArgs() { + $aolist = new SimpleAolist([]); + echo "$aolist\n"; #XXX + + $aolist = new SimpleAolist([ + ["name" => "args"], + ]); + echo "$aolist\n"; #XXX + + $aolist = new SimpleAolist([ + ["args" => 2, "name" => "args"], + ]); + echo "$aolist\n"; #XXX + + self::assertTrue(true); + } +} diff --git a/php/tests/app/args/SimpleArgsParserTest.php b/php/tests/app/args/SimpleArgsParserTest.php new file mode 100644 index 0000000..de46776 --- /dev/null +++ b/php/tests/app/args/SimpleArgsParserTest.php @@ -0,0 +1,198 @@ + [["value", "value"]]], + ["--mo12:", "args" => ["value", ["value"]]], + ["--mo22:", "args" => ["value", "value"]], + ]; + const NORMALIZE_TESTS = [ + [], ["--"], + ["--"], ["--"], + ["--", "--"], ["--", "--"], + ["-aa"], ["-a", "-a", "--"], + ["a", "b"], ["--", "a", "b"], + ["-a", "--ma", "x", "a", "--ma=y", "b"], ["-a", "--mandatory", "x", "--mandatory", "y", "--", "a", "b"], + ["-mx", "-m", "y"], ["-m", "x", "-m", "y", "--"], + ["-ox", "-o", "y"], ["-ox", "-o", "--", "y"], + ["-a", "--", "-a", "-c"], ["-a", "--", "-a", "-c"], + + # -a et -b doivent être considérés comme arguments, -n comme option + ["--mo02"], ["--mo02", "--", "--"], + ["--mo02", "-a"], ["--mo02", "-a", "--", "--"], + ["--mo02", "--"], ["--mo02", "--", "--"], + ["--mo02", "--", "-n"], ["--mo02", "--", "-n", "--"], + ["--mo02", "--", "--", "-b"], ["--mo02", "--", "--", "-b"], + # + ["--mo02", "-a"], ["--mo02", "-a", "--", "--"], + ["--mo02", "-a", "-a"], ["--mo02", "-a", "-a", "--"], + ["--mo02", "-a", "--"], ["--mo02", "-a", "--", "--"], + ["--mo02", "-a", "--", "-n"], ["--mo02", "-a", "--", "-n", "--"], + ["--mo02", "-a", "--", "--", "-b"], ["--mo02", "-a", "--", "--", "-b"], + + [ + "--mo02", "--", + "--mo02", "x", "--", + "--mo02", "x", "y", + "--mo12", "x", "--", + "--mo12", "x", "y", + "--mo22", "x", "y", + "z", + ], [ + "--mo02", "--", + "--mo02", "x", "--", + "--mo02", "x", "y", + "--mo12", "x", "--", + "--mo12", "x", "y", + "--mo22", "x", "y", + "--", + "z", + ], + ]; + + function testNormalize() { + $parser = new SimpleArgsParser(self::NORMALIZE_ARGS); + $count = count(self::NORMALIZE_TESTS); + for ($i = 0; $i < $count; $i += 2) { + $args = self::NORMALIZE_TESTS[$i]; + $expected = self::NORMALIZE_TESTS[$i + 1]; + $normalized = $parser->normalize($args); + self::assertSame($expected, $normalized + , "for ".var_export($args, true) + .", normalized is ".var_export($normalized, true) + ); + } + } + + function testArgsNone() { + $parser = new SimpleArgsParser([ + ["-z"], + ["-a"], + ["-b"], + ["-c",], + ["-d", "value" => 42], + ]); + + $dest = []; $parser->parse($dest, ["-a", "-bb", "-ccc", "-dddd"]); + self::assertSame(null, $dest["z"] ?? null); + self::assertSame(1, $dest["a"] ?? null); + self::assertSame(2, $dest["b"] ?? null); + self::assertSame(3, $dest["c"] ?? null); + self::assertSame(42, $dest["d"] ?? null); + + self::assertTrue(true); + } + + function testArgsMandatory() { + $parser = new SimpleArgsParser([ + ["-z:"], + ["-a:"], + ["-b:"], + ["-c:", "value" => 42], + ]); + + $dest = []; $parser->parse($dest, [ + "-a", + "-bb", + "-c", + "-c15", + "-c30", + "-c45", + ]); + self::assertSame(null, $dest["z"] ?? null); + self::assertSame("-bb", $dest["a"] ?? null); + self::assertSame(null, $dest["b"] ?? null); + self::assertSame("45", $dest["c"] ?? null); + + self::assertTrue(true); + } + + function testArgsOptional() { + $parser = new SimpleArgsParser([ + ["-z::"], + ["-a::"], + ["-b::"], + ["-c::", "value" => 42], + ["-d::", "value" => 42], + ]); + + $dest = []; $parser->parse($dest, [ + "-a", + "-bb", + "-c", + "-d15", + "-d30", + ]); + self::assertSame(null, $dest["z"] ?? null); + self::assertSame(null, $dest["a"] ?? null); + self::assertSame("b", $dest["b"] ?? null); + self::assertSame(42, $dest["c"] ?? null); + self::assertSame("30", $dest["d"] ?? null); + + self::assertTrue(true); + } + + function testRemains() { + $parser = new SimpleArgsParser([]); + $dest = []; $parser->parse($dest, ["x", "y"]); + self::assertSame(["x", "y"], $dest["args"] ?? null); + } + + function test() { + $parser = new SimpleArgsParser([ + ["-n", "--none"], + ["-m:", "--mandatory"], + ["-o::", "--optional"], + ["--mo02:", "args" => [["value", "value"]]], + ["--mo12:", "args" => ["value", ["value"]]], + ["--mo22:", "args" => ["value", "value"]], + ]); + $parser->parse($dest, [ + "--mo02", "--", + "--mo02", "x", "--", + "--mo02", "x", "y", + "--mo12", "x", "--", + "--mo12", "x", "y", + "--mo22", "x", "y", + "z", + ]); + + self::assertTrue(true); + } + + function testAutono() { + $parser = new SimpleArgsParser([ + ["-a", "--plouf"], + ["-b", "--no-plouf"], + ]); + $dest = []; + $parser->parse($dest, ["-aabb"]); + self::assertSame(["plouf" => 0, "args" => []], $dest); + + $parser = new SimpleArgsParser([ + ["-a", "--plouf", "value" => true], + ["-b", "--no-plouf", "value" => false], + ]); + $dest = ["plouf" => null]; + $parser->parse($dest, []); + self::assertSame(["plouf" => null, "args" => []], $dest); + $dest = ["plouf" => null]; + $parser->parse($dest, ["-a"]); + self::assertSame(["plouf" => true, "args" => []], $dest); + $dest = ["plouf" => null]; + $parser->parse($dest, ["-b"]); + self::assertSame(["plouf" => false, "args" => []], $dest); + } +} diff --git a/php/tests/app/argsTest.php b/php/tests/app/argsTest.php index c0a894c..3404d23 100644 --- a/php/tests/app/argsTest.php +++ b/php/tests/app/argsTest.php @@ -3,7 +3,6 @@ namespace nulib\app; use nulib\tests\TestCase; -use nulib\app\args; class argsTest extends TestCase { function testFrom_array() { diff --git a/php/tests/app/config/ConfigManagerTest.php b/php/tests/app/config/ConfigManagerTest.php new file mode 100644 index 0000000..415a6bd --- /dev/null +++ b/php/tests/app/config/ConfigManagerTest.php @@ -0,0 +1,124 @@ +addConfigurator(config1::class); + $config->configure(); + self::assertSame([ + "config1::static configure1", + ], impl\result::$configured); + + result::reset(); + $config->addConfigurator(config1::class); + $config->configure(); + $config->configure(); + $config->configure(); + self::assertSame([ + "config1::static configure1", + ], impl\result::$configured); + + result::reset(); + $config->addConfigurator(new config1()); + $config->configure(); + self::assertSame([ + "config1::static configure1", + "config1::configure2", + ], impl\result::$configured); + + result::reset(); + $config->addConfigurator(new config1()); + $config->configure(["include" => "2"]); + self::assertSame([ + "config1::configure2", + ], impl\result::$configured); + $config->configure(["include" => "1"]); + self::assertSame([ + "config1::configure2", + "config1::static configure1", + ], impl\result::$configured); + + result::reset(); + $config->addConfigurator([ + config1::class, + new config2(), + ]); + $config->configure(); + self::assertSame([ + "config1::static configure1", + "config2::static configure1", + "config2::configure2", + ], impl\result::$configured); + } + + function testConfig() { + $config = new ConfigManager(); + + $config->addConfig([ + "app" => [ + "var" => "array", + ] + ]); + self::assertSame("array", $config->getValue("app.var")); + + $config->addConfig(new ArrayConfig([ + "app" => [ + "var" => "instance", + ] + ])); + self::assertSame("instance", $config->getValue("app.var")); + + $config->addConfig(config1::class); + self::assertSame("class1", $config->getValue("app.var")); + + $config->addConfig(config2::class); + self::assertSame("class2", $config->getValue("app.var")); + } + } +} + +namespace nulib\app\config\impl { + class result { + static array $configured = []; + + static function reset() { + self::$configured = []; + } + } + + class config1 { + const APP = [ + "var" => "class1", + ]; + + static function configure1() { + result::$configured[] = "config1::static configure1"; + } + + function configure2() { + result::$configured[] = "config1::configure2"; + } + } + + class config2 { + const APP = [ + "var" => "class2", + ]; + + static function configure1() { + result::$configured[] = "config2::static configure1"; + } + + function configure2() { + result::$configured[] = "config2::configure2"; + } + } +} diff --git a/php/tests/cache/CursorChannelTest.php b/php/tests/cache/CursorChannelTest.php new file mode 100644 index 0000000..70e7343 --- /dev/null +++ b/php/tests/cache/CursorChannelTest.php @@ -0,0 +1,38 @@ + ["a" => "un", "b" => "deux"], + "eng" => ["a" => "one", "b" => "two"], + ["a" => 1, "b" => 2], + ]; + + function testUsage() { + $channel = CursorChannel::with("numbers", self::DATA, self::$storage); + $count = 0; + foreach ($channel as $key => $item) { + msg::info("one: $key => {$item["a"]}"); + $count++; + } + self::assertSame(3, $count); + } + + function testAddColumns() { + $channel = (new class("numbers") extends CursorChannel { + const NAME = "numbersac"; + const TABLE_NAME = self::NAME; + const ADD_COLUMNS = [ + "a" => "varchar(30)", + ]; + })->initStorage(self::$storage)->rechargeAll(self::DATA); + $count = 0; + foreach ($channel as $key => $item) { + msg::info("one: $key => {$item["a"]}"); + $count++; + } + self::assertSame(3, $count); + } +} diff --git a/php/tests/cache/SourceDb.php b/php/tests/cache/SourceDb.php new file mode 100644 index 0000000..31dc119 --- /dev/null +++ b/php/tests/cache/SourceDb.php @@ -0,0 +1,22 @@ +exec("insert into source (s, i, b) values (null, null, null)"); + $db->exec("insert into source (s, i, b) values ('false', 0, 0)"); + $db->exec("insert into source (s, i, b) values ('first', 1, 1)"); + $db->exec("insert into source (s, i, b) values ('second', 2, 1)"); + } + + public function __construct() { + parent::__construct(__DIR__."/source.db"); + } +} diff --git a/php/tests/cache/_TestCase.php b/php/tests/cache/_TestCase.php new file mode 100644 index 0000000..a8bfa11 --- /dev/null +++ b/php/tests/cache/_TestCase.php @@ -0,0 +1,23 @@ +close(); + } +} diff --git a/php/tests/cache/cacheTest.php b/php/tests/cache/cacheTest.php new file mode 100644 index 0000000..cdd2e8f --- /dev/null +++ b/php/tests/cache/cacheTest.php @@ -0,0 +1,105 @@ + ["a" => "un", "b" => "deux"], + "eng" => ["a" => "one", "b" => "two"], + ["a" => 1, "b" => 2], + ]; + + function _testRows(iterable $rows, int $expectedCount) { + $count = 0; + foreach ($rows as $key => $row) { + $parts = ["got $key => {"]; + $i = 0; + foreach ($row as $k => $v) { + if ($i++ > 0) $parts[] = ", "; + $parts[] = "$k=$v"; + } + $parts[] = "}"; + msg::info(implode("", $parts)); + $count++; + } + self::assertSame($expectedCount, $count); + } + + function _testGet(string $dataId, int $expectedCount, callable $gencompute) { + msg::section($dataId); + cache::nc(true, true); + + msg::step("premier"); + $rows = cache::get($dataId, $gencompute()); + $this->_testRows($rows, $expectedCount); + msg::step("deuxième"); + $rows = cache::get($dataId, $gencompute()); + $this->_testRows($rows, $expectedCount); + + msg::step("vider le cache"); + cache::nc(true, true); + + msg::step("premier"); + $rows = cache::get($dataId, $gencompute()); + $this->_testRows($rows, $expectedCount); + msg::step("deuxième"); + $rows = cache::get($dataId, $gencompute()); + $this->_testRows($rows, $expectedCount); + } + + function testGetStatic() { + $this->_testGet("getStatic", 3, function () { + return static function () { + msg::note("getdata"); + return self::DATA; + }; + }); + } + + function testGetGenerator() { + $this->_testGet("getGenerator", 3, function () { + return static function () { + msg::note("gendata"); + foreach (self::DATA as $key => $item) { + msg::info("yield $key"); + yield $key => $item; + sleep(2); + } + msg::note("fin gendata"); + }; + }); + } + + function _testAll(string $cursorId, int $expectedCount, callable $gencompute) { + msg::section($cursorId); + cache::nc(true, true); + + msg::step("premier"); + $rows = cache::all($cursorId, $gencompute()); + $this->_testRows($rows, $expectedCount); + msg::step("deuxième"); + $rows = cache::all($cursorId, $gencompute()); + $this->_testRows($rows, $expectedCount); + + msg::step("vider le cache"); + cache::nc(true, true); + + msg::step("premier"); + $rows = cache::all($cursorId, $gencompute()); + $this->_testRows($rows, $expectedCount); + msg::step("deuxième"); + $rows = cache::all($cursorId, $gencompute()); + $this->_testRows($rows, $expectedCount); + } + + function testAllGenerator() { + $this->_testAll("allGenerator", 4, function() { + return static function() { + $db = new SourceDb(); + msg::note("query source"); + yield from $db->all("select * from source"); + }; + }); + } +} diff --git a/php/tests/db/sqlite/ChannelMigrationTest.php b/php/tests/db/sqlite/ChannelMigrationTest.php index fa48e7c..30a80f4 100644 --- a/php/tests/db/sqlite/ChannelMigrationTest.php +++ b/php/tests/db/sqlite/ChannelMigrationTest.php @@ -7,14 +7,14 @@ use nulib\db\sqlite\impl\MyChannelV2; use nulib\db\sqlite\impl\MyChannelV3; use nulib\db\sqlite\impl\MyIndexChannel; use nulib\output\msg; -use nulib\output\std\StdMessenger; +use nulib\output\std\ConsoleMessenger; use nulib\php\time\DateTime; use nulib\tests\TestCase; class ChannelMigrationTest extends TestCase { static function setUpBeforeClass(): void { parent::setUpBeforeClass(); - msg::set_messenger_class(StdMessenger::class); + msg::set_messenger_class(ConsoleMessenger::class); } protected function addData(MyChannel $channel, array $data): void { diff --git a/php/tests/db/sqlite/SqliteStorageTest.php b/php/tests/db/sqlite/SqliteStorageTest.php index 540dfb7..356fcd3 100644 --- a/php/tests/db/sqlite/SqliteStorageTest.php +++ b/php/tests/db/sqlite/SqliteStorageTest.php @@ -1,10 +1,10 @@ "infos pour NOM PRENOM", + "body" => << [ + "PRENOM" => "prenom", + "NOM" => "nom", + "AGE" => "age", + ], + ]; + + $tpl = new MailTemplate($mail); + [ + "subject" => $subject, + "body" => $body, + ] = $tpl->eval([ + "nom" => "Clain", + "prenom" => "Jephté", + "age" => 47, + ]); + self::assertSame("infos pour Clain Jephté", $subject); + self::assertSame("

bonjour Jephté Clain,

\n

vous avez 47 ans

\n", $body); + } +} diff --git a/php/tests/php/funcTest.php b/php/tests/php/funcTest.php index a382660..b942beb 100644 --- a/php/tests/php/funcTest.php +++ b/php/tests/php/funcTest.php @@ -1,11 +1,12 @@ invoke([1, 2]); self::assertInstanceOf(C1::class, $i1); self::assertSame(1, $i1->base); } - + private static function invoke_asserts(): array { $inv_ok = function($func) { return func::with($func)->invoke(); diff --git a/php/tests/php/time/DateTest.php b/php/tests/php/time/DateTest.php index 458e42b..4656fc5 100644 --- a/php/tests/php/time/DateTest.php +++ b/php/tests/php/time/DateTest.php @@ -29,13 +29,15 @@ class DateTest extends TestCase { function testClone() { $date = self::dt("now"); + $clone = $date->clone(true); + self::assertInstanceOf(MutableDate::class, $clone); $clone = $date->clone(); - self::assertInstanceOf(DateTime::class, $clone); + self::assertInstanceOf(Date::class, $clone); } function testConstruct() { - $y = date("Y"); - self::assertSame("05/04/$y", strval(new Date("5/4"))); + $Y = date("Y"); + self::assertSame("05/04/$Y", strval(new Date("5/4"))); self::assertSame("05/04/2024", strval(new Date("5/4/24"))); self::assertSame("05/04/2024", strval(new Date("5/4/2024"))); self::assertSame("05/04/2024", strval(new Date("05/04/2024"))); diff --git a/php/tests/php/time/DateTimeTest.php b/php/tests/php/time/DateTimeTest.php index 67cc9de..41ce15e 100644 --- a/php/tests/php/time/DateTimeTest.php +++ b/php/tests/php/time/DateTimeTest.php @@ -5,12 +5,8 @@ use DateTimeZone; use nulib\tests\TestCase; class DateTimeTest extends TestCase { - protected static function dt(string $datetime): DateTime { - return new DateTime($datetime, new DateTimeZone("Indian/Reunion")); - } - function testDateTime() { - $date = self::dt("2024-04-05 09:15:23"); + $date = new DateTime("2024-04-05 09:15:23"); self::assertEquals("05/04/2024 09:15:23", $date->format()); self::assertEquals("05/04/2024 09:15:23", strval($date)); @@ -31,24 +27,36 @@ class DateTimeTest extends TestCase { } function testDateTimeZ() { - $date = new DateTime("20240405T091523Z"); - self::assertSame("20240405T131523", $date->YmdHMS); - self::assertSame("20240405T131523+04:00", $date->YmdHMSZ); - # comme on spécifie la timezone, la valeur Z est ignorée - $date = new DateTime("20240405T091523Z", new DateTimeZone("Indian/Reunion")); - self::assertSame("20240405T091523", $date->YmdHMS); + $date = new DateTime("20240405T091523"); self::assertSame("20240405T091523+04:00", $date->YmdHMSZ); + + $date = new DateTime("20240405T091523+02:00", null, null); + self::assertSame("20240405T111523+04:00", $date->YmdHMSZ); + $date = new DateTime("20240405T091523+02:00", null, true); + self::assertSame("20240405T111523+04:00", $date->YmdHMSZ); + $date = new DateTime("20240405T091523+02:00", null, false); + self::assertSame("20240405T091523+02:00", $date->YmdHMSZ); + + $newtz = new DateTimeZone("+06:00"); + $date = new DateTime("20240405T091523+02:00", $newtz, null); + self::assertSame("20240405T091523+02:00", $date->YmdHMSZ); + $date = new DateTime("20240405T091523+02:00", $newtz, false); + self::assertSame("20240405T091523+02:00", $date->YmdHMSZ); + $date = new DateTime("20240405T091523+02:00", $newtz, true); + self::assertSame("20240405T131523+06:00", $date->YmdHMSZ); } function testClone() { - $date = self::dt("now"); + $date = new DateTime("now"); + $clone = $date->clone(true); + self::assertInstanceOf(MutableDateTime::class, $clone); $clone = $date->clone(); self::assertInstanceOf(DateTime::class, $clone); } function testConstruct() { - $y = date("Y"); - self::assertSame("05/04/$y 00:00:00", strval(new DateTime("5/4"))); + $Y = date("Y"); + self::assertSame("05/04/$Y 00:00:00", strval(new DateTime("5/4"))); self::assertSame("05/04/2024 00:00:00", strval(new DateTime("5/4/24"))); self::assertSame("05/04/2024 00:00:00", strval(new DateTime("5/4/2024"))); self::assertSame("05/04/2024 00:00:00", strval(new DateTime("05/04/2024"))); @@ -110,4 +118,12 @@ class DateTimeTest extends TestCase { self::assertFalse($b >= $b2); self::assertFalse($b >= $b3); } + + function testSerialize() { + $date = new DateTime(); + $serialized = serialize($date); + echo "serialized: $serialized\n"; + $unserialized = unserialize($serialized); + self::assertEquals($date, $unserialized); + } } diff --git a/php/tests/php/time/DelayTest.php b/php/tests/php/time/DelayTest.php index 132bc4d..b7c1dd2 100644 --- a/php/tests/php/time/DelayTest.php +++ b/php/tests/php/time/DelayTest.php @@ -1,76 +1,71 @@ getDest()); + self::assertEquals(new MutableDateTime("2024-04-05 09:15:33"), $delay->getDest()); $delay = new Delay("10", $from); - self::assertEquals(self::dt("2024-04-05 09:15:33"), $delay->getDest()); + self::assertEquals(new MutableDateTime("2024-04-05 09:15:33"), $delay->getDest()); $delay = new Delay("10s", $from); - self::assertEquals(self::dt("2024-04-05 09:15:33"), $delay->getDest()); + self::assertEquals(new MutableDateTime("2024-04-05 09:15:33"), $delay->getDest()); $delay = new Delay("s", $from); - self::assertEquals(self::dt("2024-04-05 09:15:24"), $delay->getDest()); + self::assertEquals(new MutableDateTime("2024-04-05 09:15:24"), $delay->getDest()); $delay = new Delay("5m", $from); - self::assertEquals(self::dt("2024-04-05 09:20:00"), $delay->getDest()); + self::assertEquals(new MutableDateTime("2024-04-05 09:20:00"), $delay->getDest()); $delay = new Delay("5m0", $from); - self::assertEquals(self::dt("2024-04-05 09:20:00"), $delay->getDest()); + self::assertEquals(new MutableDateTime("2024-04-05 09:20:00"), $delay->getDest()); $delay = new Delay("5m2", $from); - self::assertEquals(self::dt("2024-04-05 09:20:02"), $delay->getDest()); + self::assertEquals(new MutableDateTime("2024-04-05 09:20:02"), $delay->getDest()); $delay = new Delay("m", $from); - self::assertEquals(self::dt("2024-04-05 09:16:00"), $delay->getDest()); + self::assertEquals(new MutableDateTime("2024-04-05 09:16:00"), $delay->getDest()); $delay = new Delay("5h", $from); - self::assertEquals(self::dt("2024-04-05 14:00:00"), $delay->getDest()); + self::assertEquals(new MutableDateTime("2024-04-05 14:00:00"), $delay->getDest()); $delay = new Delay("5h0", $from); - self::assertEquals(self::dt("2024-04-05 14:00:00"), $delay->getDest()); + self::assertEquals(new MutableDateTime("2024-04-05 14:00:00"), $delay->getDest()); $delay = new Delay("5h2", $from); - self::assertEquals(self::dt("2024-04-05 14:02:00"), $delay->getDest()); + self::assertEquals(new MutableDateTime("2024-04-05 14:02:00"), $delay->getDest()); $delay = new Delay("h", $from); - self::assertEquals(self::dt("2024-04-05 10:00:00"), $delay->getDest()); + self::assertEquals(new MutableDateTime("2024-04-05 10:00:00"), $delay->getDest()); $delay = new Delay("5d", $from); - self::assertEquals(self::dt("2024-04-10 05:00:00"), $delay->getDest()); + self::assertEquals(new MutableDateTime("2024-04-10 05:00:00"), $delay->getDest()); $delay = new Delay("5d2", $from); - self::assertEquals(self::dt("2024-04-10 02:00:00"), $delay->getDest()); + self::assertEquals(new MutableDateTime("2024-04-10 02:00:00"), $delay->getDest()); $delay = new Delay("5d0", $from); - self::assertEquals(self::dt("2024-04-10 00:00:00"), $delay->getDest()); + self::assertEquals(new MutableDateTime("2024-04-10 00:00:00"), $delay->getDest()); $delay = new Delay("d", $from); - self::assertEquals(self::dt("2024-04-06 05:00:00"), $delay->getDest()); + self::assertEquals(new MutableDateTime("2024-04-06 05:00:00"), $delay->getDest()); $delay = new Delay("2w", $from); - self::assertEquals(self::dt("2024-04-21 05:00:00"), $delay->getDest()); + self::assertEquals(new MutableDateTime("2024-04-21 05:00:00"), $delay->getDest()); $delay = new Delay("2w2", $from); - self::assertEquals(self::dt("2024-04-21 02:00:00"), $delay->getDest()); + self::assertEquals(new MutableDateTime("2024-04-21 02:00:00"), $delay->getDest()); $delay = new Delay("2w0", $from); - self::assertEquals(self::dt("2024-04-21 00:00:00"), $delay->getDest()); + self::assertEquals(new MutableDateTime("2024-04-21 00:00:00"), $delay->getDest()); $delay = new Delay("w", $from); - self::assertEquals(self::dt("2024-04-07 05:00:00"), $delay->getDest()); + self::assertEquals(new MutableDateTime("2024-04-07 05:00:00"), $delay->getDest()); } function testElapsed() { @@ -80,4 +75,27 @@ class DelayTest extends TestCase { sleep(5); self::assertTrue($delay->isElapsed()); } + + function testSerialize() { + $delay = new Delay(5); + $serialized = serialize($delay); + echo "serialized: $serialized\n"; + $unserialized = unserialize($serialized); + self::assertEquals($delay, $unserialized); + } + + function testInf() { + $delay = new Delay("INF"); + self::assertSame("INF", strval($delay)); + self::assertFalse($delay->isElapsed()); + + $diff = $delay->getDiff(); + self::assertSame("-P1000YT", strval($diff)); + + $serialized = serialize($delay); + self::assertSame('O:20:"nulib\php\time\Delay":2:{i:0;N;i:1;s:3:"INF";}', $serialized); + echo "serialized: $serialized\n"; + $unserialized = unserialize($serialized); + self::assertEquals($delay, $unserialized); + } } diff --git a/php/tests/php/time/MutableDateTest.php b/php/tests/php/time/MutableDateTest.php new file mode 100644 index 0000000..26f425d --- /dev/null +++ b/php/tests/php/time/MutableDateTest.php @@ -0,0 +1,87 @@ +format()); + self::assertSame("05/04/2024", strval($date)); + self::assertSame(2024, $date->year); + self::assertSame(4, $date->month); + self::assertSame(5, $date->day); + self::assertSame(0, $date->hour); + self::assertSame(0, $date->minute); + self::assertSame(0, $date->second); + self::assertSame(5, $date->wday); + self::assertSame(14, $date->wnum); + self::assertSame("+04:00", $date->timezone); + self::assertSame("05/04/2024 00:00:00", $date->datetime); + self::assertSame("05/04/2024", $date->date); + } + + function testClone() { + $date = self::dt("now"); + $clone = $date->clone(true); + self::assertInstanceOf(MutableDate::class, $clone); + $clone = $date->clone(); + self::assertInstanceOf(Date::class, $clone); + } + + function testConstruct() { + $Y = date("Y"); + self::assertSame("05/04/$Y", strval(new MutableDate("5/4"))); + self::assertSame("05/04/2024", strval(new MutableDate("5/4/24"))); + self::assertSame("05/04/2024", strval(new MutableDate("5/4/2024"))); + self::assertSame("05/04/2024", strval(new MutableDate("05/04/2024"))); + self::assertSame("05/04/2024", strval(new MutableDate("20240405"))); + self::assertSame("05/04/2024", strval(new MutableDate("240405"))); + self::assertSame("05/04/2024", strval(new MutableDate("20240405T091523"))); + self::assertSame("05/04/2024", strval(new MutableDate("20240405T091523Z"))); + self::assertSame("05/04/2024", strval(new MutableDate("5/4/2024 9:15:23"))); + self::assertSame("05/04/2024", strval(new MutableDate("5/4/2024 9.15.23"))); + self::assertSame("05/04/2024", strval(new MutableDate("5/4/2024 9:15"))); + self::assertSame("05/04/2024", strval(new MutableDate("5/4/2024 9.15"))); + self::assertSame("05/04/2024", strval(new MutableDate("5/4/2024 9h15"))); + self::assertSame("05/04/2024", strval(new MutableDate("5/4/2024 09:15:23"))); + self::assertSame("05/04/2024", strval(new MutableDate("5/4/2024 09:15"))); + self::assertSame("05/04/2024", strval(new MutableDate("5/4/2024 09h15"))); + } + + function testCompare() { + $a = new MutableDate("10/02/2024"); + $b = new MutableDate("15/02/2024"); + $c = new MutableDate("20/02/2024"); + $a2 = new MutableDate("10/02/2024"); + $b2 = new MutableDate("15/02/2024"); + $c2 = new MutableDate("20/02/2024"); + + self::assertTrue($a == $a2); + self::assertFalse($a === $a2); + self::assertTrue($b == $b2); + self::assertTrue($c == $c2); + + self::assertFalse($a < $a); + self::assertTrue($a < $b); + self::assertTrue($a < $c); + + self::assertTrue($a <= $a); + self::assertTrue($a <= $b); + self::assertTrue($a <= $c); + + self::assertFalse($c > $c); + self::assertTrue($c > $b); + self::assertTrue($c > $a); + + self::assertTrue($c >= $c); + self::assertTrue($c >= $b); + self::assertTrue($c >= $a); + } +} diff --git a/php/tests/php/time/MutableDateTimeTest.php b/php/tests/php/time/MutableDateTimeTest.php new file mode 100644 index 0000000..a83f129 --- /dev/null +++ b/php/tests/php/time/MutableDateTimeTest.php @@ -0,0 +1,121 @@ +format()); + self::assertEquals("05/04/2024 09:15:23", strval($date)); + self::assertSame(2024, $date->year); + self::assertSame(4, $date->month); + self::assertSame(5, $date->day); + self::assertSame(9, $date->hour); + self::assertSame(15, $date->minute); + self::assertSame(23, $date->second); + self::assertSame(5, $date->wday); + self::assertSame(14, $date->wnum); + self::assertEquals("+04:00", $date->timezone); + self::assertSame("05/04/2024 09:15:23", $date->datetime); + self::assertSame("05/04/2024", $date->date); + self::assertSame("20240405", $date->Ymd); + self::assertSame("20240405T091523", $date->YmdHMS); + self::assertSame("20240405T091523+04:00", $date->YmdHMSZ); + } + + function testDateTimeZ() { + $date = new MutableDateTime("20240405T091523"); + self::assertSame("20240405T091523+04:00", $date->YmdHMSZ); + + $date = new MutableDateTime("20240405T091523+02:00", null, null); + self::assertSame("20240405T111523+04:00", $date->YmdHMSZ); + $date = new MutableDateTime("20240405T091523+02:00", null, true); + self::assertSame("20240405T111523+04:00", $date->YmdHMSZ); + $date = new MutableDateTime("20240405T091523+02:00", null, false); + self::assertSame("20240405T091523+02:00", $date->YmdHMSZ); + + $newtz = new DateTimeZone("+06:00"); + $date = new MutableDateTime("20240405T091523+02:00", $newtz, null); + self::assertSame("20240405T091523+02:00", $date->YmdHMSZ); + $date = new MutableDateTime("20240405T091523+02:00", $newtz, false); + self::assertSame("20240405T091523+02:00", $date->YmdHMSZ); + $date = new MutableDateTime("20240405T091523+02:00", $newtz, true); + self::assertSame("20240405T131523+06:00", $date->YmdHMSZ); + } + + function testClone() { + $date = new MutableDateTime("now"); + $clone = $date->clone(true); + self::assertInstanceOf(MutableDateTime::class, $clone); + $clone = $date->clone(); + self::assertInstanceOf(DateTime::class, $clone); + } + + function testConstruct() { + $Y = date("Y"); + self::assertSame("05/04/$Y 00:00:00", strval(new MutableDateTime("5/4"))); + self::assertSame("05/04/2024 00:00:00", strval(new MutableDateTime("5/4/24"))); + self::assertSame("05/04/2024 00:00:00", strval(new MutableDateTime("5/4/2024"))); + self::assertSame("05/04/2024 00:00:00", strval(new MutableDateTime("05/04/2024"))); + self::assertSame("05/04/2024 00:00:00", strval(new MutableDateTime("20240405"))); + self::assertSame("05/04/2024 00:00:00", strval(new MutableDateTime("240405"))); + self::assertSame("05/04/2024 09:15:23", strval(new MutableDateTime("20240405T091523"))); + self::assertSame("05/04/2024 13:15:23", strval(new MutableDateTime("20240405T091523Z"))); + self::assertSame("05/04/2024 09:15:23", strval(new MutableDateTime("5/4/2024 9:15:23"))); + self::assertSame("05/04/2024 09:15:23", strval(new MutableDateTime("5/4/2024 9.15.23"))); + self::assertSame("05/04/2024 09:15:00", strval(new MutableDateTime("5/4/2024 9:15"))); + self::assertSame("05/04/2024 09:15:00", strval(new MutableDateTime("5/4/2024 9.15"))); + self::assertSame("05/04/2024 09:15:00", strval(new MutableDateTime("5/4/2024 9h15"))); + self::assertSame("05/04/2024 09:15:23", strval(new MutableDateTime("5/4/2024 09:15:23"))); + self::assertSame("05/04/2024 09:15:00", strval(new MutableDateTime("5/4/2024 09:15"))); + self::assertSame("05/04/2024 09:15:00", strval(new MutableDateTime("5/4/2024 09h15"))); + } + + function testCompare() { + $a = new MutableDateTime("10/02/2024"); + $a2 = new MutableDateTime("10/02/2024 8:30"); + $a3 = new MutableDateTime("10/02/2024 15:45"); + $b = new MutableDateTime("15/02/2024"); + $b2 = new MutableDateTime("15/02/2024 8:30"); + $b3 = new MutableDateTime("15/02/2024 15:45"); + $x = new MutableDateTime("10/02/2024"); + $x2 = new MutableDateTime("10/02/2024 8:30"); + $x3 = new MutableDateTime("10/02/2024 15:45"); + + self::assertTrue($a == $x); + self::assertFalse($a === $x); + self::assertTrue($a2 == $x2); + self::assertTrue($a3 == $x3); + + self::assertFalse($a < $a); + self::assertTrue($a < $a2); + self::assertTrue($a < $a3); + self::assertTrue($a < $b); + self::assertTrue($a < $b2); + self::assertTrue($a < $b3); + + self::assertTrue($a <= $a); + self::assertTrue($a <= $a2); + self::assertTrue($a <= $a3); + self::assertTrue($a <= $b); + self::assertTrue($a <= $b2); + self::assertTrue($a <= $b3); + + self::assertTrue($b > $a); + self::assertTrue($b > $a2); + self::assertTrue($b > $a3); + self::assertFalse($b > $b); + self::assertFalse($b > $b2); + self::assertFalse($b > $b3); + + self::assertTrue($b >= $a); + self::assertTrue($b >= $a2); + self::assertTrue($b >= $a3); + self::assertTrue($b >= $b); + self::assertFalse($b >= $b2); + self::assertFalse($b >= $b3); + } +} diff --git a/runphp/TODO.md b/runphp/TODO.md new file mode 100644 index 0000000..b896d4a --- /dev/null +++ b/runphp/TODO.md @@ -0,0 +1,6 @@ +# TODO + +* dans `phpwrapper-_wrapper.sh`, corriger le calcul de link à la ligne 61, + parce que le chemin relatif peut être différent que ../bin + +-*- coding: utf-8 mode: markdown -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8:noeol:binary \ No newline at end of file diff --git a/runphp/dot-build.env.dist b/runphp/dot-build.env.dist index 7baed50..1d19470 100644 --- a/runphp/dot-build.env.dist +++ b/runphp/dot-build.env.dist @@ -15,7 +15,7 @@ PRIVAREG= # Ne pas toucher à partir d'ici REGISTRY=pubdocker.univ-reunion.fr/dist -DIST=d12 +DIST=d13 IMAGENAME=nulib/ #DEVUSER_USERENT=user:x:1000:1000:User,,,:/home/user:/bin/bash #DEVUSER_GROUPENT=user:x:1000: diff --git a/runphp/dot-runphp.conf b/runphp/dot-runphp.conf index 0c2f16a..1906caa 100644 --- a/runphp/dot-runphp.conf +++ b/runphp/dot-runphp.conf @@ -4,5 +4,5 @@ RUNPHP= # Si RUNPHP n'est pas défini, les variables suivantes peuvent être définies -#DIST=d12 +#DIST=d13 #REGISTRY=pubdocker.univ-reunion.fr/dist diff --git a/runphp/phpwrapper-.launcher.php b/runphp/phpwrapper-.launcher.php index adabe3d..802d9cd 100644 --- a/runphp/phpwrapper-.launcher.php +++ b/runphp/phpwrapper-.launcher.php @@ -1,12 +1,12 @@ # TODO Faire une copie de ce script dans un répertoire de l'application web -# (dans le répertoire cli_config/ par défaut) et modifier les paramètres si nécessaire +# (dans le répertoire cli/config/ par défaut) et modifier les paramètres si nécessaire #------------------------------------------------------------------------------- __DIR__.'/..', - "appcode" => \app\config\bootstrap::APPCODE, + "projdir" => __DIR__.'/@@CLI2PROJ@@', + "projcode" => \app\config\bootstrap::PROJCODE, ]; -require __DIR__.'/../vendor/nulib/base/php/src/app/cli/include-launcher.php'; +require __DIR__.'/@@CLI2PROJ@@/vendor/nulib/base/php/src/app/cli/include-launcher.php'; diff --git a/runphp/phpwrapper-_bg_launcher.php b/runphp/phpwrapper-_bg_launcher.php index 783576a..86e28aa 100644 --- a/runphp/phpwrapper-_bg_launcher.php +++ b/runphp/phpwrapper-_bg_launcher.php @@ -2,17 +2,17 @@ # (dans le répertoire sbin/ par défaut) et modifier les paramètres si nécessaire #------------------------------------------------------------------------------- __DIR__.'/..', - "appcode" => \app\config\bootstrap::APPCODE, + "projdir" => __DIR__.'/@@SBIN2PROJ@@', + "projcode" => \app\config\bootstrap::PROJCODE, ]); BgLauncherApp::run(); diff --git a/runphp/phpwrapper-_wrapper.sh b/runphp/phpwrapper-_wrapper.sh index 05569e8..5320701 100644 --- a/runphp/phpwrapper-_wrapper.sh +++ b/runphp/phpwrapper-_wrapper.sh @@ -1,5 +1,5 @@ # TODO Faire une copie de ce script dans un répertoire de l'application web -# (dans le répertoire cli_config/ par défaut) et modifier les paramétres si nécessaire +# (dans le répertoire cli/config/ par défaut) et modifier les paramétres si nécessaire #------------------------------------------------------------------------------- #!/bin/bash # -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8 @@ -7,7 +7,7 @@ # Tous les chemins suivants sont relatifs au répertoire qui contient ce script # Chemin relatif de la racine du projet -PROJPATH=.. +PROJPATH=@@CLI2PROJ@@ # Chemin relatif vers le lanceur PHP LAUNCHERPATH=.launcher.php @@ -100,10 +100,9 @@ if [ "$RUNPHP_MODE" == host ]; then args+=( --workdir "$cwd" "$COMPOSE_SERVICE" - exec "$MYNAME" + exec "$0" "$@" ) - cd "$PROJDIR" exec "${args[@]}" fi diff --git a/runphp/runphp b/runphp/runphp index 30f3056..f534d5a 100755 --- a/runphp/runphp +++ b/runphp/runphp @@ -41,7 +41,7 @@ BUILD_FLAVOUR= ## ici # version de debian à utiliser pour l'image -# d12=php8.2, d11=php7.4, d10=php7.3 +# d13=php8.4 d12=php8.2, d11=php7.4, d10=php7.3 DIST= # Nom de base de l'image (sans le registry), e.g prefix/ @@ -91,7 +91,7 @@ if [ -f "$MYDIR/runphp.userconf.local" ]; then source "$MYDIR/runphp.userconf.local" fi -DEFAULT_DIST=d12 +DEFAULT_DIST=d13 if [ -n "$RUNPHP_STANDALONE" ]; then PROJDIR="$RUNPHP_PROJDIR" @@ -536,41 +536,36 @@ OPTIONS done # monter le répertoire qui contient $PROJDIR - mount_composer= + Cwd="$(pwd)" + mount_homes=1 mount_standalone=1 mount_mount=1 + mount_cwd=1 if [ -z "$PROJDIR" -o "${PROJDIR#$HOME/}" != "$PROJDIR" -o "$PROJDIR" == "$HOME" ]; then # bind mount $HOME args+=(-v "$HOME:$HOME${UseRslave:+:rslave}") - if [ -n "$RUNPHP_STANDALONE" -a "${RUNPHP_STANDALONE#$HOME/}" != "$RUNPHP_STANDALONE" ]; then - mount_standalone= - fi - if [ -n "$RUNPHP_MOUNT" -a "${RUNPHP_MOUNT#$HOME/}" != "$RUNPHP_MOUNT" ]; then - mount_mount= - fi - elif [ -n "$PROJDIR" ]; then - # bind mount uniquement le répertoire du projet + [ "${HOME#/home/}" != "$HOME" ] && mount_homes= + [ -n "$RUNPHP_STANDALONE" -a "${RUNPHP_STANDALONE#$HOME/}" != "$RUNPHP_STANDALONE" ] && mount_standalone= + [ -n "$RUNPHP_MOUNT" -a "${RUNPHP_MOUNT#$HOME/}" != "$RUNPHP_MOUNT" ] && mount_mount= + [ "${Cwd#$HOME/}" != "$Cwd" ] && mount_cwd= + elif [ -n "$PROJDIR" -a "${PROJDIR#/home/}" == "$PROJDIR" ]; then + # bind mount le répertoire du projet s'il n'est pas dans /home (qui est + # monté par défaut si $HOME n'est pas monté) args+=(-v "$PROJDIR:$PROJDIR${UseRslave:+:rslave}") - mount_composer=1 - [ "$RUNPHP_STANDALONE" == "$PROJDIR" ] && mount_standalone= - [ "$RUNPHP_MOUNT" == "$PROJDIR" ] && mount_mount= + [ "$RUNPHP_STANDALONE" == "$PROJDIR" -o "${RUNPHP_STANDALONE#$PROJDIR/}" != "$PROJDIR"] && mount_standalone= + [ "$RUNPHP_MOUNT" == "$PROJDIR" -o "${RUNPHP_MOUNT#$PROJDIR/}" != "$PROJDIR" ] && mount_mount= + [ "$Cwd" == "$PROJDIR" -o "${Cwd#$PROJDIR/}" != "$PROJDIR" ] && mount_cwd= fi - if [ -n "$mount_composer" -a -d "$HOME/.composer" ]; then - # monter la configuration de composer - args+=(-v "$HOME/.composer:$HOME/.composer") - fi - if [ -n "$RUNPHP_STANDALONE" -a -n "$mount_standalone" ]; then - args+=(-v "$RUNPHP_STANDALONE:$RUNPHP_STANDALONE") - fi - if [ -n "$RUNPHP_MOUNT" -a -n "$mount_mount" ]; then - args+=(-v "$RUNPHP_MOUNT:$RUNPHP_MOUNT") - fi - args+=(-w "$(pwd)") + [ -n "$mount_homes" ] && args+=(-v "/home:/home${UseRslave:+:rslave}") + [ -n "$RUNPHP_STANDALONE" -a -n "$mount_standalone" ] && args+=(-v "$RUNPHP_STANDALONE:$RUNPHP_STANDALONE${UseRslave:+:rslave}") + [ -n "$RUNPHP_MOUNT" -a -n "$mount_mount" ] && args+=(-v "$RUNPHP_MOUNT:$RUNPHP_MOUNT${UseRslave:+:rslave}") + [ -n "$mount_cwd" ] && args+=(-v "$Cwd:$Cwd${UseRslave:+:rslave}") + args+=(-w "$Cwd") # lancer avec l'utilisateur courant if [ $(id -u) -ne 0 ]; then # si c'est un utilisateur lambda, il faut monter les informations - # nécessaires. composer est déjà monté via $HOME + # nécessaires. composer est déjà monté via $HOME ou /home args+=( -e DEVUSER_USERENT="$(getent passwd "$(id -un)")" -e DEVUSER_GROUPENT="$(getent group "$(id -gn)")" diff --git a/runphp/update-runphp.sh b/runphp/update-runphp.sh index b4555f5..07f30e4 100755 --- a/runphp/update-runphp.sh +++ b/runphp/update-runphp.sh @@ -52,7 +52,7 @@ declare -A PHPWRAPPER_MODES=( ) projdir= -install_phpwrappers=auto +install_phpwrappers=1 args=( "Mettre à jour le script runphp" "[path/to/runphp]" @@ -63,7 +63,6 @@ Copier les fichiers pour un projet de l'université de la Réunion: - le script build est mis à jour - les wrappers PHP pour la gestion des tâches de fond sont mis à jour le cas échéant" - -p,--phpwrappers-install install_phpwrappers=1 "forcer l'installation des wrappers PHP" --np,--no-phpwrappers-install install_phpwrappers= "ne pas installer les wrappers PHP" ) parse_args "$@"; set -- "${args[@]}" @@ -156,23 +155,31 @@ if [ -n "$projdir" ]; then ' fi - sbin_path=sbin - cli_path=cli_config - if [ "$install_phpwrappers" == auto ]; then - if [ ! -f "$PROJDIR/$COMPOSERDIR/composer.json" ]; then - # ce doit être un projet PHP - install_phpwrappers= - elif [ -d "$projdir/cli_config" ]; then - install_phpwrappers=1 - sbin_path=sbin - cli_path=cli_config - elif [ -d "$projdir/_cli" ]; then - install_phpwrappers=1 - sbin_path=sbin - cli_path=_cli - else - install_phpwrappers= - fi + if [ -z "$install_phpwrappers" ]; then + install_phpwrappers= + elif [ ! -f "$PROJDIR/$COMPOSERDIR/composer.json" ]; then + # ce doit être un projet PHP + install_phpwrappers= + elif [ -d "$projdir/cli/config" ]; then + install_phpwrappers=1 + sbin_path=sbin + sbin2proj=.. + cli_path=cli/config + cli2proj=../.. + elif [ -d "$projdir/cli_config" ]; then + install_phpwrappers=1 + sbin_path=sbin + sbin2proj=.. + cli_path=cli_config + cli2proj=.. + elif [ -d "$projdir/_cli" ]; then + install_phpwrappers=1 + sbin_path=sbin + sbin2proj=.. + cli_path=_cli + cli2proj=.. + else + install_phpwrappers= fi if [ -n "$install_phpwrappers" ]; then @@ -193,7 +200,9 @@ if [ -n "$projdir" ]; then mkdir -p "$destdir" tail -n+4 "$MYDIR/$phpwrapper" | sed " s|/@@SBIN@@/|/$sbin_path/| +s|@@SBIN2PROJ@@|$sbin2proj| s|/@@CLI@@/|/$cli_path/| +s|@@CLI2PROJ@@|$cli2proj| " >"$destdir/$destname" [ -n "$mode" ] && chmod "$mode" "$destdir/$destname" done diff --git a/wip/TEMPLATE b/wip/TEMPLATE new file mode 100755 index 0000000..c46b7c5 --- /dev/null +++ b/wip/TEMPLATE @@ -0,0 +1,10 @@ +#!/bin/bash +# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8 +source "$(dirname -- "$0")/../load.sh" || exit 1 + +args=( + "description" + "usage" +) +parse_args "$@"; set -- "${args[@]}" + diff --git a/wip/pman.md b/wip/TODO.md similarity index 95% rename from wip/pman.md rename to wip/TODO.md index 50460ec..d2bc928 100644 --- a/wip/pman.md +++ b/wip/TODO.md @@ -1,13 +1,6 @@ -# pman +# TODO -outil pour gérer les projets PHP -* p, pci, pp, pu: gestion courante git. - ces outils peuvent agir sur un ensemble de projets, notamment tous les - projets dépendants du projet courant -* pver: gestion des versions. - calculer la prochaine version en respectant semver - -## scripts de gestion de projet +## pman définir précisément le rôle des scripts * pdist: créer la branche DIST, basculer dessus, merger MAIN dans DIST @@ -24,4 +17,14 @@ il faudra supprimer * pman: fonctionnalités réparties dans les autres scripts spécialisés * pmer: fonctionnalités réperties dans les autres scripts spécialisés +### divers + +outil pour gérer les projets PHP +* p, pci, pp, pu: gestion courante git. + ces outils peuvent agir sur un ensemble de projets, notamment tous les + projets dépendants du projet courant +* pver: gestion des versions. + calculer la prochaine version en respectant semver + + -*- coding: utf-8 mode: markdown -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8:noeol:binary \ No newline at end of file diff --git a/bin/pman b/wip/pman.orig similarity index 100% rename from bin/pman rename to wip/pman.orig diff --git a/bin/pmer b/wip/pmer.orig similarity index 100% rename from bin/pmer rename to wip/pmer.orig diff --git a/wip/prel.orig b/wip/prel.orig new file mode 100755 index 0000000..2ec3a31 --- /dev/null +++ b/wip/prel.orig @@ -0,0 +1,292 @@ +#!/bin/bash +# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8 +source "$(dirname -- "$0")/../load.sh" || exit 1 +require: git pman pman.conf + +git_cleancheckout_DIRTY="\ +Vous avez des modifications locales. +Enregistrez ces modifications avant de créer une release" + +function show_action() { + local commits + setx commits=_list_commits + if [ -n "$commits" ]; then + if [ $ShowLevel -ge 2 ]; then + { + echo "\ +# Commits à fusionner $SrcBranch --> $DestBranch + +$commits +" + _sd_COLOR=always _show_diff + } | less -eRF + else + einfo "Commits à fusionner $SrcBranch --> $DestBranch" + eecho "$commits" + fi + fi +} + +function ensure_branches() { + [ -n "$SrcBranch" -a -n "$DestBranch" ] || + die "$SrcBranch: Aucune configuration de fusion trouvée pour cette branche" + + array_contains LocalBranches "$SrcBranch" || die "$SrcBranch: branche source introuvable" + array_contains LocalBranches "$DestBranch" || die "$DestBranch: branche destination introuvable" + + Tag="$TAG_PREFIX$Version$TAG_SUFFIX" + local -a tags + setx -a tags=git tag -l "${TAG_PREFIX}*${TAG_SUFFIX}" + if [ -z "$ForceCreate" ]; then + array_contains tags "$Tag" && die "$Tag: le tag correspondant à la version existe déjà" + fi +} + +function create_release_action() { + if [ -n "$ReleaseBranch" ]; then + Version="${ReleaseBranch#$RELEASE}" + Tag="$TAG_PREFIX$Version$TAG_SUFFIX" + merge_release_action "$@"; return $? + elif [ -n "$HotfixBranch" ]; then + Version="${HotfixBranch#$HOTFIX}" + Tag="$TAG_PREFIX$Version$TAG_SUFFIX" + merge_hotfix_action "$@"; return $? + fi + + [ -n "$ManualRelease" ] && ewarn "\ +L'option --no-merge a été forcée puisque ce dépôt ne supporte pas les releases automatiques" + [ -z "$ShouldPush" ] && enote "\ +L'option --no-push a été forcée puisque ce dépôt n'a pas d'origine" + + if [ -z "$Version" -a -n "$CurrentVersion" -a -f VERSION.txt ]; then + Version="$(" + -d:,--chdir:BASEDIR chdir= "répertoire dans lequel se placer avant de lancer les opérations" + -O:,--origin Origin= "++\ +origine à partir de laquelle les branches distantes sont considérées" + -B:,--config-branch ConfigBranch= "++\ +branche à partir de laquelle charger la configuration" + -c:,--config-file:CONFIG ConfigFile= "++\ +fichier de configuration des branches. cette option est prioritaire sur --config-branch +par défaut, utiliser le fichier .pman.conf dans le répertoire du dépôt s'il existe" + -n,--no-push Push= "\ +ne pas pousser les branches vers leur origine après la fusion" + --push Push=1 "++\ +pousser les branches vers leur origine après la fusion. +c'est l'option par défaut" +) +parse_args "$@"; set -- "${args[@]}" + +# charger la configuration +ensure_gitdir "$chdir" +load_branches all +load_config "$MYNAME" +load_branches current + +branch="$1" +if [ -z "$branch" -a ${#FeatureBranches[*]} -eq 1 ]; then + branch="${FeatureBranches[0]}" +fi +[ -n "$branch" ] || die "Vous devez spécifier la branche à créer" +branch="$FEATURE${branch#$FEATURE}" + +resolve_should_push +git_ensure_cleancheckout + +if array_contains AllBranches "$branch"; then + git checkout -q "$branch" +else + # si la branche source n'existe pas, la créer + args=(--origin "$Origin") + if [ -n "$ConfigFile" ]; then args+=(--config-file "$ConfigFile") + elif [ -n "$ConfigBranch" ]; then args+=(--config-branch "$ConfigBranch") + fi + [ -z "$Push" ] && args+=(--no-push) + exec "$MYDIR/pman" "${args[@]}" "$branch" +fi