nur-sery/wip/app/app2.php

551 lines
16 KiB
PHP

<?php
namespace nur\sery\wip\app;
use Closure;
use nur\sery\A;
use nur\sery\cl;
use nur\sery\ExitError;
use nur\sery\os\path;
use nur\sery\os\sh;
use nur\sery\php\func;
use nur\sery\str;
use nur\sery\ValueException;
use nur\sery\wip\app\cli\Application;
#XXX une réécriture de app, qui remplacera app à terme
class app2 {
private static function isa_Application($app): bool {
if (!is_string($app)) return false;
return $app === Application::class || is_subclass_of($app, Application::class);
}
private static function get_params($app): array {
if ($app instanceof self) {
$params = $app->getParams();
} elseif ($app instanceof Application) {
$class = get_class($app);
$params = [
"class" => $class,
"projdir" => $app::PROJDIR,
"vendor" => $app::VENDOR,
"appcode" => $app::APPCODE,
"datadir" => $app::DATADIR,
"etcdir" => $app::ETCDIR,
"vardir" => $app::VARDIR,
"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"),
"appcode" => constant("$app::APPCODE"),
"datadir" => constant("$app::DATADIR"),
"etcdir" => constant("$app::ETCDIR"),
"vardir" => constant("$app::VARDIR"),
"logdir" => constant("$app::LOGDIR"),
"appgroup" => constant("$app::APPGROUP"),
"name" => constant("$app::NAME"),
"title" => constant("$app::TITLE"),
];
} elseif (is_array($app)) {
$params = $app;
} else {
throw ValueException::invalid_type($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",
"appcode",
"cwd",
"datadir",
"etcdir",
"vardir",
"logdir",
"profile",
]));
}
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(): string {
return self::get()->getProfile();
}
static function set_profile(?string $profile=null): void {
self::get()->setProfile($profile);
}
/**
* @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,
"appcode" => $appcode,
"datadir" => $datadir,
"etcdir" => $etcdir,
"vardir" => $vardir,
"logdir" => $logdir,
] = $params;
$cwd = $params["cwd"] ?? null;
$datadirIsDefined = true;
$profile = $params["profile"] ?? null;
} 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"]);
# appcode
$appcode = $params["appcode"] ?? null;
if ($appcode === null) {
$appcode = str::without_suffix("-app", path::basename($projdir));
}
$APPCODE = str_replace("-", "_", strtoupper($appcode));
# cwd
$cwd = $params["cwd"] ?? null;
# datadir
$datadir = getenv("${APPCODE}_DATADIR");
$datadirIsDefined = $datadir !== false;
if ($datadir === false) $datadir = $params["datadir"] ?? null;
if ($datadir === null) $datadir = "devel";
$datadir = path::reljoin($projdir, $datadir);
# etcdir
$etcdir = getenv("${APPCODE}_ETCDIR");
if ($etcdir === false) $etcdir = $params["etcdir"] ?? null;
if ($etcdir === null) $etcdir = "etc";
$etcdir = path::reljoin($datadir, $etcdir);
# vardir
$vardir = getenv("${APPCODE}_VARDIR");
if ($vardir === false) $vardir = $params["vardir"] ?? null;
if ($vardir === null) $vardir = "var";
$vardir = path::reljoin($datadir, $vardir);
# logdir
$logdir = getenv("${APPCODE}_LOGDIR");
if ($logdir === false) $logdir = $params["logdir"] ?? null;
if ($logdir === null) $logdir = "log";
$logdir = path::reljoin($datadir, $logdir);
# profile
$profile = getenv("${APPCODE}_PROFILE");
if ($profile === false) $profile = getenv("APP_PROFILE");
if ($profile === false) $profile = $params["profile"] ?? null;
}
# cwd
$cwd ??= getcwd();
# profile
$profile ??= $datadirIsDefined? "prod": "devel";
$this->projdir = $projdir;
$this->vendor = $vendor;
$this->appcode = $appcode;
$this->cwd = $cwd;
$this->datadir = $datadir;
$this->etcdir = $etcdir;
$this->vardir = $vardir;
$this->logdir = $logdir;
$this->profile = $profile;
# name, title
$appgroup = $params["appgroup"] ?? null;
$name = $params["name"] ?? $params["class"] ?? null;
if ($name === null) {
$name = $appcode;
} else {
# si $name est une classe, enlever le package et normaliser i.e
# my\package\MyApplication --> my-application
$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 $appcode;
function getAppcode(): string {
return $this->appcode;
}
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 $logdir;
function getLogdir(): string {
return $this->logdir;
}
protected string $profile;
function getProfile(): string {
return $this->profile;
}
function setProfile(?string $profile): void {
$profile ??= $this->profile;
$this->profile = $profile;
}
/**
* @param ?string|false $profile
*
* false === pas de profil
* null === profil par défaut
*/
function withProfile(string $file, $profile): string {
if ($profile !== false) {
if ($profile === null) $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 ValueException::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,
"appcode" => $this->appcode,
"cwd" => $this->cwd,
"datadir" => $this->datadir,
"etcdir" => $this->etcdir,
"vardir" => $this->vardir,
"logdir" => $this->logdir,
"profile" => $this->profile,
"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 {
if ($name === null) $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 {
if ($name === null) $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 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;
}
$file = $this->fencedJoin($this->logdir, $this->appgroup, $name);
$file = $this->withProfile($file, $profile);
sh::mkdirof($file);
return $file;
}
/**
* 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 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 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);
# ne pas spécifier $name pour avoir le fichier par défaut sans profil
$logfile = $this->getLogfile("$name.console.log", 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) return;
$enabled = boolval(self::$bgapplication_enabled->invoke());
if (!$forceEnabled && !$enabled) {
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);
}
}