From 98dabcb623784da585ff5bbc353da985f923c34f Mon Sep 17 00:00:00 2001 From: Edmond <1571649+EdmondDantes@users.noreply.github.com> Date: Sat, 26 Oct 2024 23:01:22 +0300 Subject: [PATCH] @ update doc --- docs/00-Introduction.md | 2 +- docs/01-Overview.md | 16 ++---- docs/02-Paradigm.md | 41 +++++++++++++++ docs/03-BaseException.md | 106 +++++++++++++++++++++++++------------- docs/04-Logging.md | 69 ++++++++++++++++--------- docs/05-Registry.md | 2 + docs/06-FatalException.md | 16 +++--- src/BaseException.php | 4 +- src/PsrLogAdapter.php | 1 + src/Registry.php | 6 +++ src/UnexpectedValue.php | 2 +- tests/RegistryTest.php | 1 + 12 files changed, 184 insertions(+), 82 deletions(-) diff --git a/docs/00-Introduction.md b/docs/00-Introduction.md index e82f6a0..a89cfa9 100644 --- a/docs/00-Introduction.md +++ b/docs/00-Introduction.md @@ -39,7 +39,7 @@ ```php $exception = new BaseException(['template' => 'User {user} from {ip} is not allowed', 'user' => $user, 'ip' => $ip]); - $logger->error($exception->getMessage(), $exception->getExceptionData()); + $logger->error($exception); ``` Это даёт возможность в дальнейшем анализировать журнал и понимать, diff --git a/docs/01-Overview.md b/docs/01-Overview.md index b30bed3..903235c 100644 --- a/docs/01-Overview.md +++ b/docs/01-Overview.md @@ -85,7 +85,7 @@ The container is used to change the flag `is_loggable`: class ClassNotExist extends BaseException { // This exception will be logged - protected $is_loggable = true; + protected $isLoggable = true; /** * ClassNotExist @@ -111,8 +111,8 @@ class ClassNotExist extends BaseException ```php class MyFatalException extends BaseException { - // This exception has aspect: "fatal" - protected $is_fatal = true; + // This exception has an aspect: "fatal" + protected $isFatal = true; } ``` @@ -123,14 +123,8 @@ class MyException extends BaseException { public function __construct($object) { - $this->set_debug_data($object); + $this->setDebugData($object->toArray()); parent::__construct('its too bad!'); } } -``` - -## BaseException static methods - -## Registry - -## StorageInterface +``` \ No newline at end of file diff --git a/docs/02-Paradigm.md b/docs/02-Paradigm.md index 1b5e009..dc59d7c 100644 --- a/docs/02-Paradigm.md +++ b/docs/02-Paradigm.md @@ -44,4 +44,45 @@ final class UserNotAllowed extends \IfCastle\Exceptions\BaseException в таком случае метаданные исключения будут использоваться для формирования атрибутов, которые попадут в трейсер вместе с тегами и шаблоном сообщения. +## Исключения и теги +Теги позволяют сгруппировать исключения произвольным образом. + +Создавая класс исключения можно определить теги, которые позже попадут в логгер. + +```php +final class MyException extends \IfCastle\Exceptions\BaseException +{ + protected array $tags = ['system', 'service']; +} +``` + +Вы так же можете передать теги в конструкторе исключения. + +```php + $exception = new MyException(['tags' => ['custom', 'tag']]); + $logger->error($exception); +``` + +Теги, добавленные в конструкторе, будут объединены с тегами, определёнными в классе исключения. + +## Исключения и аспекты + +Иногда нужно разделить разные исключения по особым типам обработки. +Например, некоторые исключения могут быть показаны пользователю, в то время, когда другие -- нет. + +`BaseException` предлагает для этого специальный интерфейс: `ClientAvailableInterface`, +который указывает, что исключение может быть показано пользователю. + +Интерфейс так же содержит дополнительные методы: +* `getClientMessage` +* `clientSerialize` + +Которые позволяют получить сообщение для пользователя и сериализовать исключение для клиента особым образом, +в то время как в журнал будут записаны метаданные исключения. + +Кроме аспекта `ClientAvailableInterface`, `BaseException` так же предлагает такие аспекты: +* `SystemExceptionInterface` - исключение, которое произошло из-за ошибки в системе, например диск переполнен. +* `RuntimeExceptionInterface` - исключение, которое случилось во время работы программы, но не является ошибкой программиста. + +Все другие исключения считаются ошибками программиста и не должны быть показаны пользователю. \ No newline at end of file diff --git a/docs/03-BaseException.md b/docs/03-BaseException.md index 1ccb965..698aa02 100644 --- a/docs/03-BaseException.md +++ b/docs/03-BaseException.md @@ -11,42 +11,44 @@ Наследуемые исключения могут так же прозрачно поддерживать эти режимы, а могут перекрывать их своим конструктором. Например, класс `UnexpectedValue`, может вести себя как `BaseException`, -если параметр `$name` будет иметь тип отличный от скаляра. +если параметр `$name` будет массивом. ```php - class UnexpectedValue extends LoggableException +/** + * The variable has an unexpected value! + */ +class UnexpectedValue extends LoggableException +{ + protected string $template = 'Unexpected value {value} occurred in the variable {name}'; + + /** + * The variable has an unexpected value! + * + * @param string|array $name Variable name + * or list of parameters for exception + * @param mixed $value Value + * @param string|class-string|null $rules Rules description + */ + public function __construct(array|string $name, mixed $value = null, string|null $rules = null) { - public function __construct(string|array $name, mixed $value = null, string $rules = null) - { - if(!is_scalar($name)) - { - parent::__construct($name); - return; - } - - parent::__construct - ([ - 'message' => 'Unexpected value', - 'name' => $name, - 'value' => self::truncate($value), - 'rules' => $rules, - 'type' => self::get_value_type($value) - ]); + if (!\is_scalar($name)) { + parent::__construct($name); + return; } + + parent::__construct([ + 'name' => $name, + 'value' => $this->toString($value), + 'message' => $rules, + 'type' => $this->typeInfo($value), + ]); } +} ``` ## Шаблоны сообщений -Исключения содержат свойство `message`, которое в общем случае является уникальным для каждого экземпляра класса, -что исключает возможность интернационализации. -Также с точки зрения разделения знаний лучше не смешивать описание ошибки с данными из контекста. - -Шаблоны сообщений решают данную задачу, определяя текст шаблона ошибки, который формирует сообщение `message`. - -Так как базовый класс `\Exception` запрещает переопределять метод `getMessage()`, -шаблон формируется в отдельном свойстве `template` и может быть инициализирован через конструктор `BaseException`. -Однако правилом хорошего тона является определение шаблона в дочернем классе, при помощи переопределения свойства: +Каждое исключение может иметь свой уникальный шаблон сообщения, которые можно определить в свойстве `template`: ```php class UnexpectedValueType extends LoggableException @@ -54,9 +56,21 @@ class UnexpectedValueType extends LoggableException protected $template = 'Unexpected type occurred for the value {name} and type {type}. Expected {expected}'; ``` +Шаблон сообщения так же может быть передан в контексте исключения с помощью специального ключа `template`: + +```php + $exception = new UnexpectedValueType + ([ + 'template' => 'Custom template {name} {type} {expected}', + 'name' => 'test', + 'type' => 'string', + 'expected' => 'integer' + ]); +``` + Формат строки шаблона соответствует правилам [PSR-3](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-3-logger-interface.md). -Если исключение определяет шаблон, тогда конструктор `BaseException` работает иначе: +Если исключение определяет шаблон, тогда конструктор `BaseException` работает следующим образом: 1. *Placeholders* шаблона заменяются данными из контекста, и передаются в конструктор `\Exception`. Таким образом, метод `getMessage()` вернёт полный текст ошибки. @@ -78,6 +92,19 @@ class UnexpectedValueType extends LoggableException ``` +## Управление журналированием + +Класс `BaseException` имеет два флага, которые управляют журналированием: +* `isLoggable` - флаг, указывающий, что исключение должно быть записано в журнал. +* `isFatal` - флаг, указывающий, что исключение является фатальным. + +Каким образом эти флажки будут использоваться, зависит от реализации журналирования, однако общие правила таковы: +1. Если не установлен флаг `isLoggable`, то исключение не будет записано в журнал. +2. Если исключение является фатальным, то оно будет записано в журнал. +3. Если исключение не является фатальным, но установлен флаг `isLoggable`, то оно будет записано в журнал. + +Фатальные исключения могут быть обработаны особым образом, но это зависит от реализации. + ## Наследование В большинстве случаев дочерний класс переопределяет только конструктор, так как ему нужно сформировать данные исключения. @@ -92,7 +119,8 @@ class UnexpectedValueType extends LoggableException 3. Если `BaseException::isLoggable` финализировать исключение. 4. Если `BaseException::isFatal` вызвать обработчик фатальных исключений. -Конструктор может изменять любые свойства после своего вызова. Поэтому, если вам нужно модифицировать свойства класса окончательно, делайте это после вызова базового конструктора. +Конструктор может изменять любые свойства после своего вызова. +Поэтому, если нужно модифицировать свойства класса окончательно, делайте это после вызова базового конструктора. Изменение свойств флажков (с префиксом `is`) вызывает изменение в поведении базового конструктора. @@ -191,7 +219,7 @@ class UnexpectedValueType extends LoggableException public function __construct($name, $value = null) { // Для журнала отладки сохраним полные данные: - $this->set_debug_data(['value' => $value]); + $this->setDebugData(['value' => $value]); parent::__construct ([ @@ -211,9 +239,9 @@ class UnexpectedValueType extends LoggableException public function __construct($name, $value = null) { // Принудительное включение режима отладки - $this->is_debug = true; + $this->isDebug = true; // Для отладчика сохраним полные данные: - $this->set_debug_data(['value' => $value]); + $this->setDebugData(['value' => $value]); parent::__construct ([ @@ -265,10 +293,14 @@ class UnexpectedValueType extends LoggableException Исключения контейнеры возвращают сериализацию не самих себя, а исключения `previous`. -За общую сериализацию/десериализацию отвечают статические методы: +За общую сериализацию/десериализацию отвечают методы: + +- `BaseException::toArray()`; +- `BaseException::arrayToErrors()`. -- `BaseException::errors_to_array()`; -- `BaseException::array_to_errors()`. +Метод arrayToErrors является защищённым, и предназначен для использования в дочерних классах, только в том случае, +если исключение может быть восстановлено из сериализованных данных. +Обычно такие исключения являются DTO объектами, что не является общим случаем. ## Особенности @@ -315,11 +347,11 @@ class UnexpectedValueType extends LoggableException catch(BaseException $e) { // out: Test\ZClass->zfun - echo implode('', $exception->get_source()); + echo implode('', $exception->getSource()); } ``` -Для вычисления источника классов `PHP` `\Exception` используйте метод `BaseException::get_source_for()`. +Для вычисления источника классов `PHP` `\Exception` используйте метод `HelperTrait::getSourceFor()`. ### Определение вложенного исключения diff --git a/docs/04-Logging.md b/docs/04-Logging.md index 680ddc5..42fd1d3 100644 --- a/docs/04-Logging.md +++ b/docs/04-Logging.md @@ -2,12 +2,17 @@ ## Процесс журналирования +выделить текст блоком + +> Функциональность `Registry` и `StorageInterface` является устаревшей и не рекомендуется к использованию. + Журналирование проходит в два этапа: 1. Исключение попадает в хранилище реестра (`Registry`). 2. Реестр передаёт исключения в журнал. -Для этого, код `BaseException` вызывает `Registry::register_exception(…)`, которая в свою очередь передаёт исключение в глобальное хранилище исключений: +Для этого, код `BaseException` вызывает `Registry::register_exception(…)`, +которая в свою очередь передаёт исключение в глобальное хранилище исключений: ```php static public function register_exception($exception) @@ -24,15 +29,18 @@ где: -- `__construct` - конструктор исключения (неважно какого). -- `Registry` - статический класс. -- `StorageI` - объект, который на самом деле хранит исключения. +- `__construct` - конструктор исключения (неважно какого). +- `Registry` - статический класс. +- `StorageInterface` - объект, который на самом деле хранит исключения. -Где же тут журнализатор? Правильно - его здесь нет. И его не должно быть. Так как вопросы журналирования сами по себе никак не относятся к библиотеке `BaseException`. +Где же тут журнализатор? Правильно - его здесь нет. И его не должно быть. +Так как вопросы журналирования сами по себе никак не относятся к библиотеке `BaseException`. Единственной точкой интеграции является метод: `Registry::save_exception_log()`. -Он вызывается в тот момент, когда можно сохранить реестр исключений. Когда именно? Всё зависит от ситуации. Например, это может быть момент завершения программы. Всё, что делает этот сложный метод, можно увидеть в коде: +Он вызывается в тот момент, когда можно сохранить реестр исключений. +Когда именно? Всё зависит от ситуации. Например, это может быть момент завершения программы. +Всё, что делает этот сложный метод, можно увидеть в коде: ```php static public function save_exception_log() @@ -44,11 +52,14 @@ } ``` -Метод вызывает зарегестрированный обработчик `$save_handler`, который определяется методом `Registry::set_save_handler($callback)`. Именно на этот обработчик и ложится задача реального журналирования исключений. +Метод вызывает зарегестрированный обработчик `$save_handler`, +который определяется методом `Registry::set_save_handler($callback)`. +Именно на этот обработчик и ложится задача реального журналирования исключений. ## Предпологаемый алгоритм журнализатора -Хотя `BaseException` никак не может повлиять на реализацию журнализатора, тем не менее существует некоторое ожидаемое поведение, которое журнализатор должен поддерживать. +Хотя `BaseException` никак не может повлиять на реализацию журнализатора, +тем не менее существует некоторое ожидаемое поведение, которое журнализатор должен поддерживать. Прототип обработчика `$save_handler`: @@ -73,29 +84,36 @@ Общий алгоритм: 1. Журнализатор получает список исключений (его копию). -2. Журнализатор обходит список чтобы записать его в журнал. -3. Журнализатор вызывает делегат `$reset_log`, чтобы очистить от журнал исключений. +2. Журнализатор обходит список, чтобы записать его в журнал. +3. Журнализатор вызывает делегат `$reset_log`, чтобы очистить от журнала исключений. -Порядок действий алгоритма означает, что если работа журнала завершится как-то неожиданно, список исключений останется. Но на самом деле, ситуация сложнее. +Порядок действий алгоритма означает, что если работа журнала завершится как-то неожиданно, +список исключений останется. Но на самом деле, ситуация сложнее. -Пока журнал записывает данные, в списке исключений могут появится новые записи. Речь не идёт о "паралельных" потоках, а о коде самого журнализатора. Это означает, что журнализатор обязан гарантировать, что ни одно исключение не попадёт в журнал, а если такие исключения и возникнут - они "остаются на совести" у журнализатора. +Пока журнал записывает данные, в списке исключений могут появиться новые записи. +Речь не идёт о "паралельных" потоках, а о коде самого журнализатора. +Это означает, что журнализатор обязан гарантировать, что ни одно исключение не попадёт в журнал, +а если такие исключения и возникнут - они "остаются на совести" у журнализатора. Каждое исключение поддаётся обработке: -1. Если `is_loggable()` === false, исключение не попадает в журнал! -2. Если `get_debug_data()` не пуст (empty()), тогда реализуется алгоритм записи в журнал для отладки. +1. Если `isLoggable()` === false, исключение не попадает в журнал! +2. Если `getDebugData()` не пуст (empty()), тогда реализуется алгоритм записи в журнал для отладки. 3. Определить аспект исключения и журнал, в который оно будет записано. 4. Сериализовать данные и записать в выбранный журнал. Обратите внимание: -1. Даже, если исключение находится в реестре, оно может не попасть в журнал, если флаг `is_loggable()` на момент реальной журнализации будет равен `false`. -2. В независимости от того включён или нет режим отладки или профилирования, данные `get_debug_data()` должны быть обработаны. +1. Даже если исключение находится в реестре, оно может не попасть в журнал, если флаг `isLoggable()` +на момент реальной журнализации будет равен `false`. +2. В независимости от того включён или нет режим отладки или профилирования, данные `getDebugData()` +должны быть обработаны. 3. Отладочный журнал может находиться отдельно от остальных. -## Управление флагом is_loggable +## Управление флагом isLoggable -Только что мы установили важный момент, что любое журналируемое исключение может попасть в реестр исключений, но не попасть в реальный журнал. +Только что мы установили важный момент, +что любое журналируемое исключение может попасть в реестр исключений, но не попасть в реальный журнал. Достаточно часто в конструкторе исключения невозможно знать заранее: нужно ли его журнализировать или нет. Например, если файл не доступен, с одной стороны это серьёзная ошибка, а с другой стороны - может быть ожидаемая ошибка. Решение о журналировании должен принять код свыше. Но этот случай так же косвенно указывает на неправильное использование исключений. @@ -150,9 +168,11 @@ Что хорошо согласуется с парадигмой: чем меньше аспектов - тем лучше. -`BaseException` убедительно рекомендует **никогда не создавать своих аспектов**. Потому, что число аспектов не зависит от размера системы: будь то Операционная система, или приложение "Hello World". +`BaseException` убедительно рекомендует **никогда не создавать своих аспектов**. +Потому, что число аспектов не зависит от размера системы: будь то Операционная система, или приложение "Hello World". -Если всё таки нужно отделить группу исключений для отдельной подсистемы, нужно перегрузить механизм регистрации исключения в реестре, и использовать другой реестр. Так как, если аспекты являются глобальными для всех компонентов системы, то отдельное журналирование требуется только конкретному модулю. +Если всё-таки нужно отделить группу исключений для отдельной подсистемы, +нужно перегрузить механизм регистрации исключения в реестре, и использовать другой реестр. Так как, если аспекты являются глобальными для всех компонентов системы, то отдельное журналирование требуется только конкретному модулю. ## Переопределение реестра исключений @@ -164,13 +184,13 @@ class MyException extends LoggableException public function __construct(MyObjectI $object, LoggerI $logger) { // Сохранить реальное состояние флага - $is_loggable = $this->is_loggable; + $is_loggable = $this->isLoggable; // Это исключение не попадает в стандартный реестр - $this->is_loggable = false; + $this->isLoggable = false; parent::__construct($this->format_data($object)); - $this->is_loggable = $is_loggable; + $this->isLoggable = $is_loggable; if($is_loggable) { @@ -181,4 +201,5 @@ class MyException extends LoggableException } ``` -Обратите внимание, что код из примера сохраняет логику флага `$is_loggable`, и позволяет наследуемым исключениям переопределять его значение. +Обратите внимание, что код из примера сохраняет логику флага `$isLoggable`, +и позволяет наследуемым исключениям переопределять его значение. diff --git a/docs/05-Registry.md b/docs/05-Registry.md index f76af1f..8023dbf 100644 --- a/docs/05-Registry.md +++ b/docs/05-Registry.md @@ -1,5 +1,7 @@ # Registry +> Функциональность `Registry` и `StorageInterface` является устаревшей и не рекомендуется к использованию. + Класс `Registry` является статическим. Хотя в `PHP` пока нет статических классов по типу C#, в `Registry` вызов конструктора запрещён. `Registry` обеспечивает регистрацию исключений до того, как они будут помещены в журнал. Для этого `Registry` использует Хранилище, которое может быть переопределено программистом: diff --git a/docs/06-FatalException.md b/docs/06-FatalException.md index 32a6cd9..0821b85 100644 --- a/docs/06-FatalException.md +++ b/docs/06-FatalException.md @@ -1,6 +1,8 @@ # Fatal Exception -Аспект "Фатальная ошибка" реализован в виде свойства `is_fatal`, а так же публичного метода: `is_fatal()`. Интересно заметить, что по сравнению с флагом `is_loggable()` у флага `is_fatal` нет возможности сброса (разве что изнутри класса). +Аспект "Фатальная ошибка" реализован в виде свойства `isFatal`, а так же публичного метода: `isFatal()`. +Интересно заметить, что по сравнению с флагом `isLoggable()` +у флага `isFatal` нет возможности сброса (разве что изнутри класса). Так как `isFatal` - это свойство, то фатальным может стать любое исключение в любой момент времени: @@ -31,7 +33,7 @@ ## Обработчик фатальных исключений -`Registry` предоставляет метод `Registry::set_fatal_handler($callback)` для обработки появления фатальных исключений. +`Registry` предоставляет метод `Registry::setFatalHandler($callback)` для обработки появления фатальных исключений. Прототип обработчика: @@ -39,9 +41,9 @@ function(BaseExceptionI $exception) { // Если это контейнер - используем его содержимое - if($exception->is_container()) + if($exception->isContainer()) { - $real_exception = $exception->get_previous(); + $real_exception = $exception->getPrevious(); } else { @@ -54,9 +56,11 @@ Обработчик фатальных исключений вызывается в момент, когда исключение становится фатальным, то есть: 1. В конце конструктора исключения `BaseException`. -2. В момент вызова метода `set_fatal()`. +2. В момент вызова метода `setFatal()`. -Обработчик фатального исключения вызывается не для журналирования. Он нужен для введения особого специального алгоритма, в условиях возможной нехватки ресурсов. Возможные задачи обработчика таковы: +Обработчик фатального исключения вызывается не для журналирования. +Он нужен для введения особого специального алгоритма, в условиях возможной нехватки ресурсов. +Возможные задачи обработчика таковы: - остановить программу или сервис; - предотвратить повторные запуски work-еров, пока проблема не решиться; diff --git a/src/BaseException.php b/src/BaseException.php index d4203d9..a1d6005 100644 --- a/src/BaseException.php +++ b/src/BaseException.php @@ -200,12 +200,12 @@ public function __construct(BaseExceptionInterface|\Throwable|array|string $exce } // The container is never in the journal - if ($this->isLoggable && $this->isContainer === false) { + if (Registry::$isActive && $this->isLoggable && $this->isContainer === false) { Registry::registerException($this); } // The handler for fatal exceptions - if ($this->isFatal) { + if (Registry::$isActive && $this->isFatal) { Registry::callFatalHandler($this); } } diff --git a/src/PsrLogAdapter.php b/src/PsrLogAdapter.php index 96a7969..a00a3c0 100644 --- a/src/PsrLogAdapter.php +++ b/src/PsrLogAdapter.php @@ -38,6 +38,7 @@ public function log(mixed $level, \Stringable|string $message, array $context = $context['tags'] = array_merge($context['tags'] ?? [], $message->getTags()); $context['exception_data'] = $message->getExceptionData(); $context['exception_debug'] = $message->getDebugData(); + $context['exception_template'] = $message->template(); $this->logger->log($level, $message->getMessage(), $context); } diff --git a/src/Registry.php b/src/Registry.php index 44242c0..00ab207 100644 --- a/src/Registry.php +++ b/src/Registry.php @@ -15,6 +15,12 @@ */ class Registry { + /** + * Flag for global handler. + * @var bool + */ + public static bool $isActive = false; + /** * Options for logger. * @var array|\ArrayAccess diff --git a/src/UnexpectedValue.php b/src/UnexpectedValue.php index 1b9858e..d7d823a 100644 --- a/src/UnexpectedValue.php +++ b/src/UnexpectedValue.php @@ -5,7 +5,7 @@ namespace IfCastle\Exceptions; /** - * The variable has unexpected value! + * The variable has an unexpected value! */ class UnexpectedValue extends LoggableException { diff --git a/tests/RegistryTest.php b/tests/RegistryTest.php index b0dd3f5..af55603 100644 --- a/tests/RegistryTest.php +++ b/tests/RegistryTest.php @@ -97,6 +97,7 @@ class RegistryTest extends \PHPUnit\Framework\TestCase #[\Override] protected function setUp(): void { + Registry::$isActive = true; Registry::resetExceptionLog(); $this->testData =