nur-sery/nur_src/b/ui/AbstractMessenger.php

612 lines
19 KiB
PHP

<?php
namespace nur\b\ui;
use Exception;
use nur\A;
use nur\b\ExceptionShadow;
use nur\b\io\IWriter;
use nur\b\params\Parametrable;
use nur\b\params\Tparametrable;
use nur\b\UserException;
use nur\c;
use nur\config;
use nur\data\types\md_utils;
use nur\data\types\Metadata;
use nur\func;
use nur\SL;
use nur\writer;
use Throwable;
abstract class AbstractMessenger extends Parametrable implements IMessenger {
use Tparametrable;
const PRINT_LEVELS = [
self::KEY_USER => [
self::LEVEL_NORMAL,
config::DEVEL => self::LEVEL_MINOR,
],
self::KEY_TECH => [
self::LEVEL_NORMAL,
config::DEVEL => self::LEVEL_MINOR,
],
self::KEY_EXCEPTION => [
self::LEVEL_NEVER,
config::DEVEL => self::LEVEL_MINOR,
],
];
const LOG_LEVELS = [
self::KEY_USER => [
self::LEVEL_NORMAL,
config::DEVEL => self::LEVEL_MINOR,
],
self::KEY_TECH => [
self::LEVEL_NORMAL,
config::DEVEL => self::LEVEL_MINOR,
],
self::KEY_EXCEPTION => [
self::LEVEL_NORMAL,
config::DEVEL => self::LEVEL_MINOR,
],
];
const TYPE_LEVELS = [
IMessenger::TYPE_INFO,
config::DEVEL => IMessenger::TYPE_DEBUG,
];
const DATE_FORMAT = 'Y-m-d\TH:i:s.u';
function __construct(?array $params=null) {
self::set_parametrable_params_defaults($params, [
"print_levels" => static::PRINT_LEVELS,
"log_levels" => static::LOG_LEVELS,
"type_levels" => static::TYPE_LEVELS,
"date_format" => static::DATE_FORMAT,
]);
parent::__construct($params);
}
const PARAMETRABLE_PARAMS_SCHEMA = [
"print_levels" => ["array", null, "niveaux par défaut pour chaque catégorie de message"],
"log_levels" => ["array", null, "niveaux par défaut pour chaque catégorie de message"],
"type_levels" => ["array", null, "niveaux par défaut des types de message"],
"log_output" => [null, null, "destination des messages de logs"],
"display_log" => ["bool", false, "faut-il afficher les logs?"],
"add_date" => ["?bool", null, "faut-il dater les messages?"],
"date_format" => ["string", null, "format de la date"]
];
/** @var array */
protected $ppPrintLevels;
function pp_setPrintLevels(array $levels): void {
A::merge_nn($this->ppPrintLevels, $levels);
}
/** @var array */
protected $ppLogLevels;
function pp_setLogLevels(array $levels): void {
A::merge_nn($this->ppLogLevels, $levels);
}
/** @var array */
protected $ppTypeLevels;
/** @var IWriter */
protected $ppLogOutput;
/** @var bool */
protected $ppDisplayLog;
/** @var bool */
protected $ppAddDate;
/** @var ?string */
protected $ppDateFormat;
/** @var IWriter */
protected $logOutput;
protected function afterSetParametrableParams(array $modifiedKeys, ?Metadata $md=null): void {
if (self::was_parametrable_param_modified($modifiedKeys, "log_output")) {
$this->logOutput = $this->ppLogOutput !== null? writer::with($this->ppLogOutput): null;
if (!self::was_parametrable_param_modified($modifiedKeys, "add_date")) {
$this->ppAddDate = $this->logOutput !== null;
}
}
}
function setLevels(?array $printLevels, ?array $logLevels=null, $typeLevels=null): IMessenger {
$this->setParametrableParams(SL::filter_z([
"print_levels" => $printLevels,
"log_levels" => $logLevels,
"type_levels" => $typeLevels,
]));
return $this;
}
protected function _getLevel(string $key, array $levels): int {
$level = A::get($levels, $key, self::LEVEL_NORMAL);
if (is_int($level)) return $level;
$levels = A::with($level);
$level = A::get($levels, config::get_profile());
if ($level === null) $level = A::get($levels, 0);
if ($level === null) return self::LEVEL_NORMAL;
return $level;
}
function getPrintLevel(string $key): int {
return $this->_getLevel($key, $this->ppPrintLevels);
}
function getLogLevel(string $key): int {
return $this->_getLevel($key, $this->ppLogLevels);
}
function getTypeLevel(): int {
$levels = $this->ppTypeLevels;
$level = A::get($levels, config::get_profile());
if ($level === null) $level = A::get($levels, 0);
if ($level === null) return IMessenger::TYPE_INFO;
return $level;
}
function isLogMessage(?string $msg, int $level, string $msgKey): bool {
return $msg !== null && $level >= $this->getLogLevel($msgKey);
}
function isPrintMessage(?string $msg, int $level, string $msgKey): bool {
return $msg !== null && $level >= $this->getPrintLevel($msgKey);
}
protected function date() {
return date_create()->format($this->ppDateFormat);
}
protected function getString($text): string {
return c::string(c::nq($text));
}
protected function filterTags(string $text): string {
return preg_replace('/<[^>]*>/', "", $text);
}
protected function filterColors(string $text): string {
return $text;
}
protected function wnl(IWriter $writer, ?bool $color, string $sep, ...$values): void {
$values = c::flatten($values);
foreach ($values as &$value) {
$value = $this->getString($value);
if (!$value) $value = false;
}; unset($value);
$text = $this->filterTags($writer->toString($sep, $values));
if ($color === null) $color = $writer->isatty();
if (!$color) $text = $this->filterColors($text);
$writer->wnl($text);
}
protected function fixDest(?int &$type): void {
if ($type === null) $type = 0;
if (($type & self::DEST_MASK) === 0) $type += self::DEST_ALL;
}
protected function shouldLog(?int $type): bool {
if ($this->logOutput === null) return false;
$this->fixDest($type);
return ($type & self::DEST_LOG) != 0;
}
protected function shouldPrint(?int $type): bool {
$this->fixDest($type);
return ($type & self::DEST_DISPLAY) != 0 || $this->ppDisplayLog;
}
#############################################################################
# Sections
protected function logStartSection($title): void {
$datetime = $this->ppAddDate? $this->date()." ": "\n";
$title = "$datetime>>>> $title <<<<";
$this->wnl($this->logOutput, false, "", $title);
}
protected function logEndSection(): void {
}
protected abstract function printStartSection($title, ?int $msgType, ?int $msgLevel): void;
protected abstract function printEndSection(): void;
/** @var bool */
private $inSection;
function isInSection(): bool {
return $this->inSection;
}
function startSection($title, ?int $msgType=null, ?int $msgLevel=null): IMessenger {
if ($this->inSection) $this->endSection();
$allowLog = $msgLevel === null || $msgLevel >= $this->getLogLevel(self::KEY_USER);
$allowPrint = $msgLevel === null || $msgLevel >= $this->getPrintLevel(self::KEY_USER);
if ($allowLog || $allowPrint) {
if ($allowLog && $this->shouldLog($msgType)) $this->logStartSection($title);
if ($allowPrint && $this->shouldPrint($msgType)) $this->printStartSection($title, $msgType, $msgLevel);
$this->inSection = true;
}
return $this;
}
function endSection(): IMessenger {
if ($this->inSection) {
$this->printEndSection();
$this->inSection = false;
}
return $this;
}
#############################################################################
# Groupes
protected function getGroupCount(?array $group): int {
if ($group === null ) return 0;
$count = $group["count"];
return $count !== null? $count: 2;
}
protected function getGroupIndent(?array $group, ?int $indent=null, int $maxCount=1): string {
if ($group === null) return "";
if ($indent === null) {
$indent = $group["indent"];
$count = $this->getGroupCount($group);
if ($count <= $maxCount) $indent--;
}
return str_repeat(" ", $indent);
}
protected function getGroupPrefix(?array $group, int $maxCount=1): string {
if ($group === null) return "";
$count = $this->getGroupCount($group);
if ($count > $maxCount) return "";
return $group["prefix"]." :";
}
protected function logStartGroup(array $group): void {
if ($this->getGroupCount($group) > 1) {
$groupIndent = $this->getGroupIndent($group, $group["indent"] - 1);
if ($this->ppAddDate) $groupIndent = self::date()." $groupIndent";
$this->wnl($this->logOutput, false, " ", $groupIndent.">", $group["prefix"]);
}
}
protected function logEndGroup(array $group): void {
}
protected abstract function printStartGroup(array $group, ?array $groups): void;
protected abstract function printEndGroup(array $group): void;
private $groups;
public function isInGroup(): bool {
return $this->groups !== null;
}
function startGroup($prefix, int $count=null, ?int $msgType=null, ?int $msgLevel=null): IMessenger {
$allowLog = $msgLevel === null || $msgLevel >= $this->getLogLevel(self::KEY_USER);
$allowPrint = $msgLevel === null || $msgLevel >= $this->getPrintLevel(self::KEY_USER);
if ($allowLog || $allowPrint) {
$indent = 1;
if ($this->groups !== null) {
foreach ($this->groups as $group) {
if ($group !== null) $indent++;
}
}
$group = [
"indent" => $indent,
"prefix" => $prefix,
"count" => $count,
"type" => $msgType,
"level" => $msgLevel,
];
if ($allowLog && $this->shouldLog($msgType)) $this->logStartGroup($group);
if ($allowPrint && $this->shouldPrint($msgType)) $this->printStartGroup($group, $this->groups);
} else {
$group = false;
}
$this->groups[] = $group;
return $this;
}
function endGroup(): IMessenger {
$group = array_pop($this->groups);
if ($group) {
$type = $group["type"];
if ($this->shouldLog($type)) $this->logEndGroup($group);
if ($this->shouldPrint($type)) $this->printEndGroup($group);
}
if (!$this->groups) $this->groups = null;
return $this;
}
function end(bool $all=false): IMessenger {
if ($all) {
while ($this->groups) $this->endGroup();
$this->endSection();
} elseif ($this->groups) $this->endGroup();
else $this->endSection();
return $this;
}
#############################################################################
# Messages
const TYPE_PREFIXES = [
# les clés doivent être ordonnées de la plus grande à la plus petite
# la 2ème valeur indique s'il faut garder le préfixe s'il y a un result
self::LEVEL_CRITICAL => [
self::TYPE_ERROR => ["CRITICAL!", true],
self::TYPE_WARNING => ["ATTENTION!", true],
self::TYPE_DEBUG => ["IMPORTANT!", true],
],
self::LEVEL_MAJOR => [
self::TYPE_ERROR => ["ERROR:", true],
self::TYPE_WARNING => ["WARN:", true],
self::TYPE_INFO => ["INFO:", false],
self::TYPE_DEBUG => ["DEBUG:", true],
],
self::LEVEL_NORMAL => [
self::TYPE_ERROR => ["E", true],
self::TYPE_WARNING => ["W", true],
self::TYPE_INFO => ["", false],
self::TYPE_DEBUG => ["D", true],
],
self::LEVEL_MINOR => [
self::TYPE_ERROR => ["e", true],
self::TYPE_WARNING => ["w", true],
self::TYPE_INFO => ["i", false],
self::TYPE_DEBUG => ["d", true],
],
];
const RESULT_PREFIXES = [
self::RESULT_FAILURE => "(FAILURE)",
self::RESULT_SUCCESS => "(SUCCESS)",
self::RESULT_NEUTRAL => "*",
self::RESULT_NONE => null,
];
protected function getResultPrefix(int $result, bool $color): ?string {
return self::RESULT_PREFIXES[$result];
}
protected function getTypePrefixSuffix(int $type, int $level, bool $color, bool $haveResultPrefix): array {
$typePrefixSuffix = false;
foreach (self::TYPE_PREFIXES as $prefixLevel => $prefixes) {
if ($level >= $prefixLevel) {
foreach ($prefixes as $prefixType => $prefixValue) {
if ($type >= $prefixType) {
$typePrefixSuffix = $prefixValue;
break;
}
}
}
if ($typePrefixSuffix !== false) break;
}
if ($typePrefixSuffix === false) return [null, null];
elseif ($haveResultPrefix && !$typePrefixSuffix[1]) return [null, null];
$prefix = $typePrefixSuffix[0];
$suffix = "";
return [$prefix, $suffix];
}
/** @var Metadata */
private static $message_md;
protected static function message_md(): Metadata {
return md_utils::ensure_md(self::$message_md
, array_merge(self::MESSAGE_SCHEMA, self::MESSAGE_OPTIONS_SCHEMA));
}
protected function logMsg(
?array $groups,
bool $logUser, $userMsg,
bool $logTech, $techMsg,
bool $logException, $exceptionMsg,
int $type, int $level
): void {
$group = A::last($groups);
if ($group === false) {
# groupe neutralisé
return;
}
$groupIndent = $this->getGroupIndent($group);
$groupPrefix = $this->getGroupPrefix($group);
if ($this->ppAddDate) $groupIndent = self::date()." $groupIndent";
$color = false;
$result = $type & self::RESULT_MASK;
$type = $type & self::TYPE_MASK;
$resultPrefix = $this->getResultPrefix($result, $color);
[$typePrefix, $typeSuffix] = $this->getTypePrefixSuffix($type, $level, $color, $resultPrefix !== null);
$prefix = $groupIndent.$typePrefix.$resultPrefix;
$logOutput = $this->logOutput;
if ($logUser) $this->wnl($logOutput, $color, " ", $prefix, $groupPrefix, $userMsg, $typeSuffix);
if ($logTech) $this->wnl($logOutput, $color, " ", $prefix, "TECH:", $techMsg, $typeSuffix);
if ($logException) {
[$summary, $traceback] = $exceptionMsg;
$this->wnl($logOutput, $color, " ", $prefix, "TRACEBACK:", $summary, $typeSuffix);
$this->wnl($logOutput, false, "", $traceback);
}
}
protected abstract function getUserMsg($msg): array;
protected abstract function getTechMsg($msg): array;
protected abstract function getExceptionMsg($exception, $user, $tech, bool $haveTechMsgOrSummary): array;
protected abstract function printMsg(
?array $groups
, bool $printUser, $userMsg
, bool $printTech, $techMsg
, bool $printException, $exceptionMsg
, int $type, int $level, array $options
): void;
protected function processMsgOptions(array $options): void {
}
function ensureMessage(&$message): void {
static::message_md()->ensureSchema($message);
$user =& $message[self::KEY_USER];
$tech =& $message[self::KEY_TECH];
$exception =& $message[self::KEY_EXCEPTION];
if ($exception === null) {
if ($tech instanceof Throwable || $tech instanceof ExceptionShadow) $exception = $tech;
elseif ($user instanceof Throwable || $user instanceof ExceptionShadow) $exception = $user;
}
if ($tech === null) {
if ($user instanceof Throwable || $user instanceof ExceptionShadow) $tech = $user;
}
}
function addMessage($message, int $type, int $level): IMessenger {
$allowType = ($type & self::TYPE_MASK) >= $this->getTypeLevel();
if (!$allowType) return $this;
$this->fixDest($type);
$this->ensureMessage($message);
$user = $message[self::KEY_USER];
$logUser = $level >= $this->getLogLevel(self::KEY_USER);
$printUser = $level >= $this->getPrintLevel(self::KEY_USER);
$userMsg = false;
if ($user !== null) {
if ($user instanceof UserException) $msg = $user->getUserMessage();
elseif ($user instanceof Throwable || $user instanceof ExceptionShadow) $msg = $user->getMessage();
else $msg = $user;
if (!$msg) $printUser = false;
else $userMsg = $this->getUserMsg($msg);
}
$logUser &= $userMsg !== false;
$printUser &= $userMsg !== false;
$tech = $message[self::KEY_TECH];
$logTech = $level >= $this->getLogLevel(self::KEY_TECH);
$printTech = $level >= $this->getPrintLevel(self::KEY_TECH);
$techMsg = false;
$techSummary = false;
$exception = $message[self::KEY_EXCEPTION];
$logException = $exception !== null && $level >= $this->getLogLevel(self::KEY_EXCEPTION);;
$printException = $exception !== null && $level >= $this->getPrintLevel(self::KEY_EXCEPTION);;
$exceptionMsg = false;
if ($tech !== null) {
if ($tech instanceof UserException) {
$msg = $tech->getTechMessage();
} elseif (($tech instanceof Throwable || $tech instanceof ExceptionShadow) && !$printException) {
$techSummary = true;
$msg = UserException::get_summary($tech);
} else {
$msg = $tech;
}
if (!$msg) $printTech = false;
else $techMsg = $this->getTechMsg($msg);
}
$logTech &= $techMsg !== false;
$printTech &= $techMsg !== false;
if ($exception !== null) {
$exceptionMsg = $this->getExceptionMsg($exception, $user, $tech, $techMsg || $techSummary);
}
$logException &= $exceptionMsg !== false;
$printException &= $exceptionMsg !== false;
$options = $message;
if ($this->shouldLog($type)) {
$this->logMsg(
$this->groups,
$logUser, $userMsg,
$logTech, $techMsg,
$logException, $exceptionMsg,
$type, $level);
}
if ($this->shouldPrint($type)) {
$this->printMsg(
$this->groups,
$printUser, $userMsg,
$printTech, $techMsg,
$printException, $exceptionMsg,
$type, $level, $options);
}
$this->processMsgOptions($options);
return $this;
}
function aresult($result, ?array $args=null, $message=null, ?int $type=null): IMessenger {
if ($message !== null) static::message_md()->ensureSchema($message);
if (is_callable($result)) {
if ($args === null) $args = [];
try {
$result = func::call($result, ...$args);
if ($result === null) {
# cas des fonctions void
$result = true;
}
} catch (Exception $e) {
$result = $e;
}
}
$type = ($type?: 0) & self::DEST_MASK;
$level = self::LEVEL_MAJOR;
if ($result instanceof Exception) {
$type += self::TYPE_INFO + self::RESULT_FAILURE;
$message[self::KEY_USER] = $result;
} elseif (is_string($result)) {
$type += self::TYPE_INFO + self::RESULT_SUCCESS;
$message[self::KEY_USER] = $result;
} elseif ($result === null) {
$type += self::TYPE_INFO + self::RESULT_NEUTRAL;
A::replace_z($message, self::KEY_USER, "en cours");
} elseif ($result) {
$type += self::TYPE_INFO + self::RESULT_SUCCESS;
A::replace_z($message, self::KEY_USER, "succès");
} else {
$type += self::TYPE_INFO + self::RESULT_FAILURE;
A::replace_z($message, self::KEY_USER, "échec");
}
$this->addMessage($message, $type, $level);
if ($this->isInGroup()) $this->endGroup();
return $this;
}
function astep($message=null, ?int $type=null): IMessenger {
$this->aresult(null, null, $message, $type);
return $this;
}
function asuccess($message=null, ?int $type=null): IMessenger {
$this->aresult(true, null, $message, $type);
return $this;
}
function afailure($message=null, ?Throwable $e=null, ?int $type=null): IMessenger {
static::message_md()->ensureSchema($message);
if ($message === null) {
$message = $e;
} elseif ($message[self::KEY_USER] === null) {
$message[self::KEY_USER] = $e;
} else {
A::replace_z($message, self::KEY_EXCEPTION, $e);
}
$this->aresult(false, null, $message, $type);
return $this;
}
function action($message, $result=null, ?array $args=null, ?int $type=null, ?int $level=null): IMessenger {
$this->startGroup($message, 1, $type, $level);
if ($result !== null) $this->aresult($result, $args, null, $type);
return $this;
}
}