diff --git a/docs/00-Introduction.md b/docs/00-Introduction.md index a89cfa9..deef77a 100644 --- a/docs/00-Introduction.md +++ b/docs/00-Introduction.md @@ -1,88 +1,37 @@ -# Introduction - -**BaseException** - библиотека унификации исключений для *PHP*. - -**BaseException** решает задачи: - -1. Метаданные исключений. -2. Шаблонизация сообщений. -2. Аспектирование исключений. -3. Нормализация исключений при помощи "контейнеров". -4. Регистрация исключений в глобальном реестре. - -## Метаданные исключений и шаблонизация - -При анализе исключений важно иметь ясное представление о том, что произошло. -Для этого необходимо хранить метаданные исключения, которые помогут в дальнейшем анализе. -Данная библиотека реализует возможность создавать исключения с контекстом, который содержит метаданные исключения. - -Метаданные исключения представляют собой ассоциативный массив, -элементами которого могут быть только простые типы данных. - -## Поддержка шаблонов сообщений - -**BaseException** поддерживает концепцию *шаблона сообщения* (начиная с версии 2.0). -Шаблон сообщения - это строка, содержащая *placeholders*, -которые заменяются на значения данных из контекста исключения. -Формат шаблона соответствует [PSR-3](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-3-logger-interface.md). - -Когда исключение создаётся, в конструктор передаются данные, -которые будут использоваться для замены *placeholders* в шаблоне сообщения. -Конструктор использует шаблон и генерирует описание исключения по шаблону и метаданным. - -Шаблон исключения позволяет добиться нескольких важных бенефитов: -* Возможность перевода сообщений об ошибках на разные языки, основываясь на шаблоне. -* Возможность группировать исключения по шаблону. - -Шаблонизация сообщений позволяет записать в журнал не только результирующее сообщение, -но и данные, которые были использованы для его формирования: - -```php - $exception = new BaseException(['template' => 'User {user} from {ip} is not allowed', 'user' => $user, 'ip' => $ip]); - $logger->error($exception); -``` - -Это даёт возможность в дальнейшем анализировать журнал и понимать, -какие данные были использованы для формирования сообщения об ошибке, -группировать исключения по шаблону, либо делать поиск по метаданным. - -## Аспектирование исключений - -Аспект - это класс характеристик исключения, который позволяет обобщать код и задачи по обработке исключений. -Например, аспекты позволяют ответить на вопросы: - -- кто виноват: программист или система; -- что делать: игнорировать проблему или немедленно решать. - -## Нормализация исключений - -Нормализация исключений - это поддержка системой ограниченного числа выброшенных исключений. -Она может достигаться такими способами: - -1. Наследование. -2. Привидение к контейнеру. -3. Интерфейсы. - -Например, привидение к контейнеру: - -```php - try - { - ... - } - catch(\Exception $e) - { - // BaseException наследует характеристики $e - throw new BaseException($e); - } -``` - -## Регистрация исключений - -**BaseException** позволяет регистрировать исключения в глобальном реестре до того, как исключение, будет выброшено. -На текущий момент эта возможность существует, но не используется на практике. -Тем не менее в некоторых ситуациях, она может стать дополнительным инструментом для анализа исключений. - -## Небольшая библиотека готовых исключений - -Кроме реализации сервисного кода, **BaseException** так же содержит небольшой список готовых классов для создания типичных исключений. + +# Introduction + +**BaseException** is a library for exception unification in *PHP*. + +**BaseException** addresses the following tasks: + +1. Exception metadata. +2. Message templating. +3. Exception aspectization. +4. Exception normalization using "containers." +5. Exception registration in a global registry. + +## Exception Metadata and Templating + +When analyzing exceptions, it is important to have a clear understanding of what happened. For this, it is necessary to store exception metadata to assist in further analysis. This library enables the creation of exceptions with context that includes exception metadata. + +Exception metadata is an associative array containing only simple data types. + +## Support for Message Templates + +**BaseException** supports the concept of *message templates* (starting from version 2.0). A message template is a string containing *placeholders*, which are replaced with context data values. The template format complies with [PSR-3](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-3-logger-interface.md). + +When an exception is created, data is passed to the constructor to replace *placeholders* in the message template. The constructor uses the template and generates a description of the exception based on the template and metadata. + +The exception template provides several important benefits: +- Ability to translate error messages into different languages based on the template. +- Ability to group exceptions by template. + +Message templating allows logging not only the resulting message but also the data used to form it: + +```php + $exception = new BaseException(['template' => 'User {user} from {ip} is not allowed', 'user' => $user, 'ip' => $ip]); + $logger->error($exception); +``` + +This makes it possible to analyze the log later and understand what data was used to form the error message. diff --git a/docs/02-Paradigm.md b/docs/02-Paradigm.md index dc59d7c..63a26f5 100644 --- a/docs/02-Paradigm.md +++ b/docs/02-Paradigm.md @@ -1,88 +1,59 @@ -# Paradigm - -## Исключения и логирование - -Создание классов исключений тесно связаны с методикой логирования. - -Исключения создаются с двумя целями: - -1. Передать информацию о произошедшей ошибке для кода. -2. Предоставить достаточную информацию для логирования. - -Для решения первой задачи, используются классы и интерфейсы, позволяя коду по типу исключения определить, что произошло. -Для решения второй задачи, используются метаданные исключения, которые позволяют логировать исключения с дополнительной информацией. - -Поэтому, создавая новый класс исключения, необходимо учитывать, -что он должен содержать метаданные, которые будут использоваться для логирования. - -```php - -final class UserNotAllowed extends \IfCastle\Exceptions\BaseException -{ - protected string $template = 'User {user} from {ip} is not allowed'; - protected array $tags = ['user', 'auth']; - - public function __construct(string $user, string $ip) - { - parent::__construct([ - 'user' => $user, - 'ip' => $ip - ]); - } - -``` - -В данном примере, класс `UserNotAllowed` содержит шаблон сообщения и теги, которые будут использоваться для логирования. -Когда это исключение попадёт в логгер, он сможет использовать шаблон сообщения и теги для формирования сообщения об ошибке. - -```php - $exception = new UserNotAllowed('admin', '127.0.0.1'); - $logger->error($exception); -``` - -Если в качестве системы логирования вы используете OpenTelemetry, -в таком случае метаданные исключения будут использоваться для формирования атрибутов, -которые попадут в трейсер вместе с тегами и шаблоном сообщения. - -## Исключения и теги - -Теги позволяют сгруппировать исключения произвольным образом. - -Создавая класс исключения можно определить теги, которые позже попадут в логгер. - -```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 + +# Paradigm + +## Exceptions and Logging + +Creating exception classes is closely related to the logging methodology. + +Exceptions are created with two goals: + +1. To convey information about the error to the code. +2. To provide sufficient information for logging. + +To accomplish the first task, classes and interfaces are used, allowing the code to determine the error type. To achieve the second goal, exception metadata is used, allowing exceptions to be logged with additional information. + +Therefore, when creating a new exception class, it is essential to consider that it should contain metadata used for logging. + +```php +final class UserNotAllowed extends \IfCastle\Exceptions\BaseException +{ + protected string $template = 'User {user} from {ip} is not allowed'; + protected array $tags = ['user', 'auth']; + + public function __construct(string $user, string $ip) + { + parent::__construct([ + 'user' => $user, + 'ip' => $ip + ]); + } +} +``` + +In this example, the `UserNotAllowed` class contains a message template and tags used for logging. When this exception is logged, the logger can use the message template and tags to form an error message. + +```php + $exception = new UserNotAllowed('admin', '127.0.0.1'); + $logger->error($exception); +``` + +If you use OpenTelemetry as your logging system, the exception metadata will be used to form attributes that will be included in the tracer along with tags and the message template. + +## Exceptions and Tags + +Tags allow grouping exceptions in an arbitrary manner. + +When creating an exception class, you can define tags that will later be included in the logger. + +```php +final class MyException extends \IfCastle\Exceptions\BaseException +{ + protected array $tags = ['system', 'service']; +} +``` + +You can also pass tags in the exception constructor. + +```php + $exception = new MyException(); +``` diff --git a/docs/03-BaseException.md b/docs/03-BaseException.md index 698aa02..e5c8c52 100644 --- a/docs/03-BaseException.md +++ b/docs/03-BaseException.md @@ -1,369 +1,67 @@ -# BaseException - -## Конструктор - -Конструктор `BaseException` имеет три режима работы: - -1. Стандартный (как это определено в `\Exception`). -2. Режим структурированных данных. -3. Режим контейнера. - -Наследуемые исключения могут так же прозрачно поддерживать эти режимы, а могут перекрывать их своим конструктором. - -Например, класс `UnexpectedValue`, может вести себя как `BaseException`, -если параметр `$name` будет массивом. - -```php -/** - * 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) - { - if (!\is_scalar($name)) { - parent::__construct($name); - return; - } - - parent::__construct([ - 'name' => $name, - 'value' => $this->toString($value), - 'message' => $rules, - 'type' => $this->typeInfo($value), - ]); - } -} -``` - -## Шаблоны сообщений - -Каждое исключение может иметь свой уникальный шаблон сообщения, которые можно определить в свойстве `template`: - -```php -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` работает следующим образом: - -1. *Placeholders* шаблона заменяются данными из контекста, и передаются в конструктор `\Exception`. -Таким образом, метод `getMessage()` вернёт полный текст ошибки. -2. Если в конструктор `BaseException` передано свойство `message`, -оно воспринимается как дополнительное сообщение, и присоединятся к результирующему сообщению через точку. Например: - -```php - - $exception = new BaseException - ([ - 'template' => 'This is the template', - 'message' => 'This is a message' - ]); - - echo $exception->getMessage(); - -// Result: -// This is the template. This is a message - -``` - -## Управление журналированием - -Класс `BaseException` имеет два флага, которые управляют журналированием: -* `isLoggable` - флаг, указывающий, что исключение должно быть записано в журнал. -* `isFatal` - флаг, указывающий, что исключение является фатальным. - -Каким образом эти флажки будут использоваться, зависит от реализации журналирования, однако общие правила таковы: -1. Если не установлен флаг `isLoggable`, то исключение не будет записано в журнал. -2. Если исключение является фатальным, то оно будет записано в журнал. -3. Если исключение не является фатальным, но установлен флаг `isLoggable`, то оно будет записано в журнал. - -Фатальные исключения могут быть обработаны особым образом, но это зависит от реализации. - -## Наследование - -В большинстве случаев дочерний класс переопределяет только конструктор, так как ему нужно сформировать данные исключения. - -Если дочернему классу требуется изменить поведение исключения, он может изменять унаследованные свойства. -В данном случае - это поведение является нормальным. - -Упрощённый алгоритм `BaseException::__construct`: - -1. Анализировать входные данные. -2. Вызвать родительский конструктор `\Exception`. -3. Если `BaseException::isLoggable` финализировать исключение. -4. Если `BaseException::isFatal` вызвать обработчик фатальных исключений. - -Конструктор может изменять любые свойства после своего вызова. -Поэтому, если нужно модифицировать свойства класса окончательно, делайте это после вызова базового конструктора. - -Изменение свойств флажков (с префиксом `is`) вызывает изменение в поведении базового конструктора. - -Например, вот так можно включить журналирование: - -```php - class LoggableException extends BaseException - { - /** - * Флаг логирования. - * Если флаг равен true - то исключение - * собирается быть записанным в журнал. - * - * @var boolean - */ - protected $isLoggable = true; - } -``` - -А вот это исключение является фатальным: - -```php - class MyException extends BaseException - { - protected $isFatal = true; - } -``` - -Класс `BaseException` не журналируется и не имеет аспектов. -Поэтому для наследования удобно иметь дополнительные "базовые исключения": - -1. `LoggableException` - для исключений, которые журналируются. -2. `SystemException` - для исключений с аспектом "системное". -3. `RuntimeException` - для исключений с аспектом "runtime". -4. `FatalException`, `FatalRuntimeException`, `FatalSystemException` - для фатальных исключений. - -(подробнее о морализировании в разделе: [Журналирование][1]) - -## Рекомендации по формированию данных - -Обратите внимание, что конструктор `BaseException` не изменяет данные, которые вы передаёте ему. -Эту работу обязаны делать дочерние исключения. - -Хорошо придерживаться рекомендаций: - -1. Не хранить в исключении объекты. -2. При формировании `data` усекать данные. -3. Сохранять только самое важное и необходимое. -4. Использовать в массиве `data` только базовые типы: скаляры и массивы. - -### Не хранить в исключении объекты - -Исключения могут попадать в журнал, и хранится в памяти намного дольше, чем объекты, переданные в исключение. -Таким образом, вы будете раздувать приложение, так как сборщик мусора не сможет освободить память. -Если для Web-скриптов это не актуально, то для фоновых заданий - скорее всего. - -### Усечение данных - -Свойство `BaseException::data` попадает в журнал. -Хотя данные, которые попадают в журнал, сохраняют структуру, и могут быть десериализированы заново, -не стоит использовать большие массивы данных. -Это не только замедляет процесс обработки журнала, но и создаёт ненужную избыточность. -Если в самом деле необходимо сохранять большие массивы данных, лучше использовать отладочный режим. - -Для усечения данных можно использовать методы: - -- `BaseException::getType()`; -- `BaseException::truncate()`. - -### Использование в data базовых типов - -Это требование имеет с одной стороны тот же смысл, что и "Не хранить объекты в исключении". -Но главная причина иная. Свойство `data` может участвовать в процессе сериализации исключения. -А объекты не всегда можно правильно сериализировать/десериалировать. -Хотя можно воспользовать интерфейсом `Serializable` - это не самая хорошая идея, -так как целью сериализации могут быть другие форматы, отличные по сути от `Serializable`. - -Если вы хотите получить возможность легко сериализировать исключения - пытайтесь строго следовать этому правилу. - -## Отладочные данные - -Для сохранения отладочных данных, -`BaseException` предоставляет метод `set_debug_data()`. -Этот метод имеет атрибут `protected`, а значит предназначен только для внутреннего использования. - -Алгоритм `BaseException::setDebugData()`: - -1. Проверить, активен ли режим отладки. -2. Если да, перевести данные в строку, и сохранить их. - -Пример использования: - -```php - class UnexpectedValue extends LoggableException - { - public function __construct($name, $value = null) - { - // Для журнала отладки сохраним полные данные: - $this->setDebugData(['value' => $value]); - - parent::__construct - ([ - 'message' => 'Unexpected value', - 'name' => $name, - 'type' => self::getValueType($value) - ]); - } - } -``` - -Используя свойство `isDebug`, вы можете намеренно записать данные, в независимости от того, включён ли режим отладки или нет: - -```php - class UnexpectedValue extends LoggableException - { - public function __construct($name, $value = null) - { - // Принудительное включение режима отладки - $this->isDebug = true; - // Для отладчика сохраним полные данные: - $this->setDebugData(['value' => $value]); - - parent::__construct - ([ - 'message' => 'Unexpected value', - 'name' => $name, - 'type' => self::get_value_type($value) - ]); - } - } -``` - -При этом отладочные данные запишутся только для вашего исключения. -Это позволяет быстро ввести отладочный режим даже на рабочей системе, только в необходимом месте. - -Таким же образом, можно и отключить отладочный режим: - -```php - // Все исключения, которые наследуются от этого - // получат выключенный отладочный режим - // (если конечно не переопределят его явно) - class NoDebugException extends LoggableException - { - protected $isDebug = false; - } -``` - -## Сериализация - -Кроме основных методов `BaseException` поддерживает сериализацию/десериализацию. -Это может быть полезно для маршалинга исключений через удалённые сервисы, -сохранения исключений на диск и прочего, где обычно используется сериализация. - -Целевым форматом сериализации является асоциативный массив. -Массив выбран потому, что его легко преобразовать в любой другой формат: `json`, `xml`, `phpserialize`, etc. - -Формат массива: - -```php - [ - 'type' => get_class($exception), - 'source' => ['source' => '', 'type' => '', 'function' => ''], - 'message' => $exception->getMessage(), - 'template' => $exception->template(), - 'code' => $exception->getCode(), - 'data' => $exception->get_data(), - 'container' => 'если это контейнер-исключение' - ]; -``` - -Исключения контейнеры возвращают сериализацию не самих себя, а исключения `previous`. - -За общую сериализацию/десериализацию отвечают методы: - -- `BaseException::toArray()`; -- `BaseException::arrayToErrors()`. - -Метод arrayToErrors является защищённым, и предназначен для использования в дочерних классах, только в том случае, -если исключение может быть восстановлено из сериализованных данных. -Обычно такие исключения являются DTO объектами, что не является общим случаем. - -## Особенности - -Следующие методы `BaseException` существуют потому, что стандартные методы `PHP` `\Exception` объявлены как `final`, -но поведение их не соответствует общей парадигме `BaseException`. - -### Определение источника исключения - -Для определения источника исключения используется специальный метод `BaseException::get_source()`. Он возвращает ассоциативный массив вида: - -```php - [ - 'source' => 'класс или файл:строка, где возникло исключение', - 'type' => 'тип вызова', - 'function' => 'имя функции или метода, где возникло исключение' - ]; -``` - -Метод `BaseException::getSource()` используется при журналировании вместо метода -`BaseException::getFile()`, -потому что определение источника по имени класса является более естественным методом относительно устройства языка. -После введения `namespace` и `autoload` реальная структура файлов больше не имеет большого значения, -а информация об `namespace` сохраняется и тогда, когда весь код объединяется в один файл. - -Обратите внимание, что порядок ключей в массиве `source` имеет значение, и он создан таким образом, что если выполнить слияние массива в строку, то вы получите строковое указание источника: - -```php - - namespace Test; - - class ZClass - { - function zfun() - { - throw new BaseException('...'); - } - } - - try - { - $Test = new ZClass(); - $Test->zfun(); - } - catch(BaseException $e) - { - // out: Test\ZClass->zfun - echo implode('', $exception->getSource()); - } -``` - -Для вычисления источника классов `PHP` `\Exception` используйте метод `HelperTrait::getSourceFor()`. - -### Определение вложенного исключения - -`BaseException` создаёт контейнеры для любых объектов, которые поддерживают интерфейс `BaseExceptionInterface`. -Этот приём позволяет оборачивать в исключения другие объекты, которые имеют схожее поведение, но не являются `PHP` исключениями. - -Однако `\Exception::$previous` может быть только дочерним объектом от `\Exception`. Чтобы обойти это органичение, `BaseException` помещает объект `BaseExceptionI` в свойство `data` (в контейнере это свойство не используется для данных). - -Для доступа к вложенному объекту `BaseExceptionI` следует использовать метод `BaseExceptionI::get_previous()`, который всегда вернёт правильный объект: - -- `\Exception`; -- либо `BaseExceptionI`; -- либо `null`, если объекта нет. - -[1]: 04-Logging.md "Журналирование" + +# BaseException + +## Constructor + +The `BaseException` constructor has three modes of operation: + +1. Standard (as defined in `\Exception`). +2. Structured data mode. +3. Container mode. + +Inherited exceptions can transparently support these modes or override them with their own constructor. + +For example, the `UnexpectedValue` class can behave like `BaseException` if the `$name` parameter is an array. + +```php +/** + * 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) + { + if (!\is_scalar($name)) { + parent::__construct($name); + return; + } + + parent::__construct([ + 'name' => $name, + 'value' => $this->toString($value), + 'message' => $rules, + 'type' => $this->typeInfo($value), + ]); + } +} +``` + +## Message Templates + +Each exception can have its own unique message template, which can be defined in the `template` property: + +```php +class UnexpectedValueType extends LoggableException +{ + protected $template = 'Unexpected type occurred for the value {name} and type {type}. Expected {expected}'; +``` + +A message template can also be passed in the exception context using the special key `template`: + +```php + $exception = new UnexpectedValueType + ([ + 'template' => 'Custom template {name} {type} {expected}', + ]); +``` + diff --git a/docs/04-Logging.md b/docs/04-Logging.md index 42fd1d3..b249f7e 100644 --- a/docs/04-Logging.md +++ b/docs/04-Logging.md @@ -1,205 +1,54 @@ -# Logging - -## Процесс журналирования - -выделить текст блоком - -> Функциональность `Registry` и `StorageInterface` является устаревшей и не рекомендуется к использованию. - -Журналирование проходит в два этапа: - -1. Исключение попадает в хранилище реестра (`Registry`). -2. Реестр передаёт исключения в журнал. - -Для этого, код `BaseException` вызывает `Registry::register_exception(…)`, -которая в свою очередь передаёт исключение в глобальное хранилище исключений: - -```php - static public function register_exception($exception) - { - ... - self::$exceptions->add_exception($exception); - ... - } -``` - -Это можно отобразить на схеме: - - __construct -> Registry::register_exception -> StorageI - -где: - -- `__construct` - конструктор исключения (неважно какого). -- `Registry` - статический класс. -- `StorageInterface` - объект, который на самом деле хранит исключения. - -Где же тут журнализатор? Правильно - его здесь нет. И его не должно быть. -Так как вопросы журналирования сами по себе никак не относятся к библиотеке `BaseException`. - -Единственной точкой интеграции является метод: `Registry::save_exception_log()`. - -Он вызывается в тот момент, когда можно сохранить реестр исключений. -Когда именно? Всё зависит от ситуации. Например, это может быть момент завершения программы. -Всё, что делает этот сложный метод, можно увидеть в коде: - -```php - static public function save_exception_log() - { - if(is_callable(self::$save_handler)) - { - call_user_func(self::$save_handler, …); - } - } -``` - -Метод вызывает зарегестрированный обработчик `$save_handler`, -который определяется методом `Registry::set_save_handler($callback)`. -Именно на этот обработчик и ложится задача реального журналирования исключений. - -## Предпологаемый алгоритм журнализатора - -Хотя `BaseException` никак не может повлиять на реализацию журнализатора, -тем не менее существует некоторое ожидаемое поведение, которое журнализатор должен поддерживать. - -Прототип обработчика `$save_handler`: - -```php - /** - * Журнализатор исключений. - * - * @param array $exceptions Хранилище исключений - * @param callable $reset_log Функция сброса журнала - * @param ArrayAccess $logger_options опции журналирования - * @param ArrayAccess $debug_options опции для отладки (профайлинга) - */ - function save_handler - ( - $exceptions, - callable $reset_log, - $logger_options = [], - $debug_options = [] - ) -``` - -Общий алгоритм: - -1. Журнализатор получает список исключений (его копию). -2. Журнализатор обходит список, чтобы записать его в журнал. -3. Журнализатор вызывает делегат `$reset_log`, чтобы очистить от журнала исключений. - -Порядок действий алгоритма означает, что если работа журнала завершится как-то неожиданно, -список исключений останется. Но на самом деле, ситуация сложнее. - -Пока журнал записывает данные, в списке исключений могут появиться новые записи. -Речь не идёт о "паралельных" потоках, а о коде самого журнализатора. -Это означает, что журнализатор обязан гарантировать, что ни одно исключение не попадёт в журнал, -а если такие исключения и возникнут - они "остаются на совести" у журнализатора. - -Каждое исключение поддаётся обработке: - -1. Если `isLoggable()` === false, исключение не попадает в журнал! -2. Если `getDebugData()` не пуст (empty()), тогда реализуется алгоритм записи в журнал для отладки. -3. Определить аспект исключения и журнал, в который оно будет записано. -4. Сериализовать данные и записать в выбранный журнал. - -Обратите внимание: - -1. Даже если исключение находится в реестре, оно может не попасть в журнал, если флаг `isLoggable()` -на момент реальной журнализации будет равен `false`. -2. В независимости от того включён или нет режим отладки или профилирования, данные `getDebugData()` -должны быть обработаны. -3. Отладочный журнал может находиться отдельно от остальных. - -## Управление флагом isLoggable - -Только что мы установили важный момент, -что любое журналируемое исключение может попасть в реестр исключений, но не попасть в реальный журнал. - -Достаточно часто в конструкторе исключения невозможно знать заранее: нужно ли его журнализировать или нет. Например, если файл не доступен, с одной стороны это серьёзная ошибка, а с другой стороны - может быть ожидаемая ошибка. Решение о журналировании должен принять код свыше. Но этот случай так же косвенно указывает на неправильное использование исключений. - -С другой стороны, возможна ситуация, в которой код верхнего уровня может поместить в журнал более точное описание ошибки. В этом случае, предыдущее исключение, ему следует подавить: - -```php - try - { - ... - throw new LoggableException - ( - 'А того великана я знал - он у меня пастухом работал. - Да вот беда - на прошлой неделе пил он воду из этой чашки, - поскользнулся, упал в нее и утонул. - Три дня искали - не нашли, видно, в море унесло…' - ); - ... - } - catch(BaseException $kingdoms) - { - // Полцарства?! - // Выбрасываем другое исключение, которое попадёт в журнал - throw new LoggableException - ( - 'неправда', - 0, - $kingdoms->setLoggable(false) - ); - } -``` - -## Аспекты журналирования - -Для определения аспектов исключения, используются интерфейсы, так как они являются "естественными элементами языка", могут быть легко проверены и получены, и являются глобальными для всей программы. - -`BaseException` предлагает реализацию основных аспектов: - -1. Для системных сбоев, используется `SystemExceptionI`. -2. Для сбоев из-за внешних факторов `RuntimeExceptionI`. -3. Отсутствие аспекта - причиной ошибки является сам код программы. - -В основном аспекты используются для сортировки потока сообщений: - -1. В какой журнал поместить исключение. -2. На какой email отправить уведомление. -3. Как пометить исключение в журнале. - -Так как, аспекты определяются в виде интерфейсов, это побуждает: - -1. В каждом проекте продумать свои аспекты, если нужно. -2. Не злоупотреблять количеством аспектов. - -Что хорошо согласуется с парадигмой: чем меньше аспектов - тем лучше. - -`BaseException` убедительно рекомендует **никогда не создавать своих аспектов**. -Потому, что число аспектов не зависит от размера системы: будь то Операционная система, или приложение "Hello World". - -Если всё-таки нужно отделить группу исключений для отдельной подсистемы, -нужно перегрузить механизм регистрации исключения в реестре, и использовать другой реестр. Так как, если аспекты являются глобальными для всех компонентов системы, то отдельное журналирование требуется только конкретному модулю. - -## Переопределение реестра исключений - -Любое дочернее исключение от `BaseException` всегда может переопределить регистрацию исключения: - -```php -class MyException extends LoggableException -{ - public function __construct(MyObjectI $object, LoggerI $logger) - { - // Сохранить реальное состояние флага - $is_loggable = $this->isLoggable; - - // Это исключение не попадает в стандартный реестр - $this->isLoggable = false; - parent::__construct($this->format_data($object)); - - $this->isLoggable = $is_loggable; - - if($is_loggable) - { - // Но записывается в свой - $logger->add_to_log($this); - } - } -} -``` - -Обратите внимание, что код из примера сохраняет логику флага `$isLoggable`, -и позволяет наследуемым исключениям переопределять его значение. + +# Logging + +## Logging Process + +> The `Registry` and `StorageInterface` functionality is outdated and not recommended for use. + +Logging occurs in two stages: + +1. The exception is stored in the registry (`Registry`). +2. The registry forwards the exceptions to the log. + +To achieve this, the `BaseException` code calls `Registry::register_exception(…)`, which, in turn, sends the exception to the global exception storage: + +```php + static public function register_exception($exception) + { + ... + self::$exceptions->add_exception($exception); + ... + } +``` + +This can be represented in the scheme: + + __construct -> Registry::register_exception -> StorageI + +where: + +- `__construct` - the exception constructor (regardless of type). +- `Registry` - static class. +- `StorageInterface` - the object that actually stores exceptions. + +Where is the logger here? That's right - it isn't here, and it shouldn't be. Logging functionality itself is not related to the `BaseException` library. + +The only integration point is the method: `Registry::save_exception_log()`. + +This method is called when the exception registry can be saved. When exactly? It depends on the situation, such as when the program completes. This complex method does the following: + +```php + static public function save_exception_log() + { + if(is_callable(self::$save_handler)) + { + call_user_func(self::$save_handler, …); + } + } +``` + +The method calls the registered handler `$save_handler`, which is set by the `Registry::set_save_handler($callback)` method. This handler is responsible for the actual logging of exceptions. + +## Proposed Logger Algorithm + +Although `BaseException` cannot influence the logger's implementation, there is some expected behavior the logger should follow. diff --git a/docs/05-Registry.md b/docs/05-Registry.md index 8023dbf..faf8669 100644 --- a/docs/05-Registry.md +++ b/docs/05-Registry.md @@ -1,96 +1,43 @@ -# Registry - -> Функциональность `Registry` и `StorageInterface` является устаревшей и не рекомендуется к использованию. - -Класс `Registry` является статическим. Хотя в `PHP` пока нет статических классов по типу C#, в `Registry` вызов конструктора запрещён. - -`Registry` обеспечивает регистрацию исключений до того, как они будут помещены в журнал. Для этого `Registry` использует Хранилище, которое может быть переопределено программистом: - -```php - static public function set_registry_storage(StorageI $storage) -``` - -Интерфейс `StorageInterface` содержит всего несколько методов, и позволяет влиять на ход регистрации исключений. - -Дополнительное расширение функциональности `Registry` можно получить, используя методы: - -- `set_unhandled_handler` - необработанные исключения. -- `set_fatal_handler` - ошибка с флагом `is_fatal`. - -Метод `set_unhandled_handler` используется тогда, когда `Registry` позволено обрабатывать потоки ошибок и исключений `PHP`. Для этого нужно вызвать метод `Registry::install_global_handlers()`, после чего в реестр начнут попадать все необработанные исключения и ошибки. - -Обработчик `set_unhandled_handler` будет вызван после основного обработчика `Registry::exception_handler(\Exception $exception)`. - -Метод `set_fatal_handler` будет рассмотрен подробнее в "Fatal Exception". - -## Журналирование ошибок PHP - -`Registry` поддерживает журналирование не только исключений, но и потока ошибок `PHP`. Для этого он использует класс `Exceptions\Errors\Error`, который хотя и не является исключением, но реализует интерфейс `BaseExceptionI`. - -Все ошибки кроме `E_USER_*`, считаются ошибками программиста, и попадают в общий журнал. Это же касается `E_NOTICE` и `E_DEPRECATED`. - -Таким образом `Registry` требует, чтобы код `PHP` был полностью чист от предупреждений любого рода. - -## Необработанные исключения - -Что произойдёт с исключением, если оно не было обработано и попало в `Registry::exception_handler()`? - -```php - if($exception instanceof BaseExceptionI) - { - // Если исключение достигает обработчика - // оно не логируется в случае, если: - // - уже было журналированным - // - или если является контейнером для другого исключения - if($exception->is_loggable() || $exception->is_container()) - { - new UnhandledException($exception); - return; - } - - $exception->set_loggable(true); - } - - self::register_exception($exception); - - new UnhandledException($exception); - ... -``` - -`Registry` придерживается алгоритма: - -1. Если исключение `BaseException` имеет сброшенный флаг `is_loggable` оно всё равно будет журналировано как необработанное. -2. Если исключение уже находится в журнале - нет смысла его помещать туда снова. -3. Если исключение является контейнером, то ответственность за журналирование лежит на нём. - -Обратите внимание на `new UnhandledException($exception)`. Это исключение используется только как маркер факта, что последнее исключение не было поймано: - -```php -class UnhandledException extends LoggableException -{ - public function __construct(\Exception $exception) - { - parent::__construct - ([ - 'message' => 'Unhandled Exception', - 'type' => get_class($exception), - 'source' => self::get_source_for($exception), - 'previous' => $exception - ]); - } -} -``` - -Таким образом в журнале сохранится отдельная запись об необработанном исключении, и можно будет отличить два случая: когда целевое исключение было обработано, а когда - нет. - -## Двойное журналирование - -Нужно отметить, что возможен особый случай двойного журналирования: - -1. Исключение имеет установленный флаг `is_loggable` и попадает в журнал. -2. Другой код ловит исключение, и сбрасывает флаг -3. Другой код снова выбрасывает это же исключение -4. Исключение становится наобработанным. -5. Так как его флаг `is_loggable` равен `false`, оно логируется заново. - -Я решил оставить данный Use Case как есть, и допустить двойную запись исключения, так как, если в коде происходит что-то подобное, значит его нужно срочно пересмотреть. + +# Registry + +> The `Registry` and `StorageInterface` functionality is outdated and not recommended for use. + +The `Registry` class is static. Although PHP does not yet support static classes like C#, the constructor call in `Registry` is prohibited. + +`Registry` registers exceptions before they are logged. For this, `Registry` uses Storage, which can be overridden by the developer: + +```php + static public function set_registry_storage(StorageI $storage) +``` + +The `StorageInterface` interface contains only a few methods and allows influence over the exception registration process. + +Additional functionality extension in `Registry` can be obtained using methods: + +- `set_unhandled_handler` - unhandled exceptions. +- `set_fatal_handler` - error with `is_fatal` flag. + +The `set_unhandled_handler` method is used when `Registry` is allowed to handle PHP error and exception streams. To enable this, call the `Registry::install_global_handlers()` method, after which all unhandled exceptions and errors will begin to enter the registry. + +The `set_unhandled_handler` will be called after the main handler `Registry::exception_handler(\Exception $exception)`. + +The `set_fatal_handler` method will be discussed in detail in "Fatal Exception." + +## PHP Error Logging + +`Registry` supports logging not only exceptions but also PHP error streams. For this, it uses the `Exceptions\Errors\Error` class, which, although not an exception, implements the `BaseExceptionI` interface. + +All errors except `E_USER_*` are considered programmer errors and enter the general log. This also applies to `E_NOTICE` and `E_DEPRECATED`. + +Thus, `Registry` requires PHP code to be completely free of any warnings. + +## Unhandled Exceptions + +What happens if an exception goes unhandled and enters `Registry::exception_handler()`? + +```php + if($exception instanceof BaseExceptionI) + { +``` + diff --git a/docs/06-FatalException.md b/docs/06-FatalException.md index 0821b85..8337671 100644 --- a/docs/06-FatalException.md +++ b/docs/06-FatalException.md @@ -1,76 +1,73 @@ -# Fatal Exception - -Аспект "Фатальная ошибка" реализован в виде свойства `isFatal`, а так же публичного метода: `isFatal()`. -Интересно заметить, что по сравнению с флагом `isLoggable()` -у флага `isFatal` нет возможности сброса (разве что изнутри класса). - -Так как `isFatal` - это свойство, то фатальным может стать любое исключение в любой момент времени: - -```php - try - { - ... - } - catch(BaseException $e) - { - throw $e->markAsFatal(); - } -``` - -Можно так же использовать исключение-контейнер, чтобы наделить этой характеристикой другое исключение: - -```php - try - { - ... - } - catch(BaseException $e) - { - // Теперь исключение $e - имеет аспект фатального. - throw new FatalException($e); - } -``` - -## Обработчик фатальных исключений - -`Registry` предоставляет метод `Registry::setFatalHandler($callback)` для обработки появления фатальных исключений. - -Прототип обработчика: - -```php - function(BaseExceptionI $exception) - { - // Если это контейнер - используем его содержимое - if($exception->isContainer()) - { - $real_exception = $exception->getPrevious(); - } - else - { - $real_exception = $exception; - } - ... - } -``` - -Обработчик фатальных исключений вызывается в момент, когда исключение становится фатальным, то есть: - -1. В конце конструктора исключения `BaseException`. -2. В момент вызова метода `setFatal()`. - -Обработчик фатального исключения вызывается не для журналирования. -Он нужен для введения особого специального алгоритма, в условиях возможной нехватки ресурсов. -Возможные задачи обработчика таковы: - -- остановить программу или сервис; -- предотвратить повторные запуски work-еров, пока проблема не решиться; -- запустить процесс анализа сбоя. - -## Особенности обработки для журнализатора - -Журнализатор так же может обрабатывать фатальное исключение не так, как обычно: - -1. Проверить возможность записи на диск или в файл журнала. -2. Если файл журнала не доступен, попытаться использовать системный журнал. -3. Использовать EMAIL для отправки уведомления. -4. Если EMAIL не работает, использовать альтернативные каналы. + +# Fatal Exception + +The "Fatal Error" aspect is implemented as a property `isFatal` and a public method `isFatal()`. Notably, unlike the `isLoggable()` flag, the `isFatal` flag cannot be reset (except from within the class). + +Since `isFatal` is a property, any exception can become fatal at any time: + +```php + try + { + ... + } + catch(BaseException $e) + { + throw $e->markAsFatal(); + } +``` + +A container exception can also be used to assign this characteristic to another exception: + +```php + try + { + ... + } + catch(BaseException $e) + { + // Now the exception $e has the fatal aspect. + throw new FatalException($e); + } +``` + +## Fatal Exception Handler + +`Registry` provides the method `Registry::setFatalHandler($callback)` to handle fatal exceptions. + +Handler prototype: + +```php + function(BaseExceptionI $exception) + { + // If this is a container, use its content + if($exception->isContainer()) + { + $real_exception = $exception->getPrevious(); + } + else + { + $real_exception = $exception; + } + ... + } +``` + +The fatal exception handler is called when an exception becomes fatal, specifically: + +1. At the end of the `BaseException` constructor. +2. When calling the `setFatal()` method. + +The fatal exception handler is not intended for logging. It is needed to implement a specific algorithm under resource-limited conditions. Possible tasks of the handler include: + +- Stopping the program or service; +- Preventing worker restarts until the issue is resolved; +- Initiating a failure analysis process. + +## Logging Specifics for Fatal Exceptions + +The logger may also handle a fatal exception differently: + +1. Check the ability to write to disk or log file. +2. If the log file is unavailable, attempt to use the system log. +3. Use EMAIL to send a notification. +4. If EMAIL fails, use alternative channels. diff --git a/docs/ru/00-Introduction.md b/docs/ru/00-Introduction.md new file mode 100644 index 0000000..b9b3d33 --- /dev/null +++ b/docs/ru/00-Introduction.md @@ -0,0 +1,88 @@ +# Introduction + +**BaseException** - библиотека унификации исключений для *PHP*. + +**BaseException** решает задачи: + +1. Метаданные исключений. +2. Шаблонизация сообщений. +2. Аспектирование исключений. +3. Нормализация исключений при помощи "контейнеров". +4. Регистрация исключений в глобальном реестре. + +## Метаданные исключений и шаблонизация + +При анализе исключений важно иметь ясное представление о том, что произошло. +Для этого необходимо хранить метаданные исключения, которые помогут в дальнейшем анализе. +Данная библиотека реализует возможность создавать исключения с контекстом, который содержит метаданные исключения. + +Метаданные исключения представляют собой ассоциативный массив, +элементами которого могут быть только простые типы данных. + +## Поддержка шаблонов сообщений + +**BaseException** поддерживает концепцию *шаблона сообщения* (начиная с версии 2.0). +Шаблон сообщения - это строка, содержащая *placeholders*, +которые заменяются на значения данных из контекста исключения. +Формат шаблона соответствует [PSR-3](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-3-logger-interface.md). + +Когда исключение создаётся, в конструктор передаются данные, +которые будут использоваться для замены *placeholders* в шаблоне сообщения. +Конструктор использует шаблон и генерирует описание исключения по шаблону и метаданным. + +Шаблон исключения позволяет добиться нескольких важных бенефитов: +* Возможность перевода сообщений об ошибках на разные языки, основываясь на шаблоне. +* Возможность группировать исключения по шаблону. + +Шаблонизация сообщений позволяет записать в журнал не только результирующее сообщение, +но и данные, которые были использованы для его формирования: + +```php + $exception = new BaseException(['template' => 'User {user} from {ip} is not allowed', 'user' => $user, 'ip' => $ip]); + $logger->error($exception); +``` + +Это даёт возможность в дальнейшем анализировать журнал и понимать, +какие данные были использованы для формирования сообщения об ошибке, +группировать исключения по шаблону, либо делать поиск по метаданным. + +## Аспектирование исключений + +Аспект - это класс характеристик исключения, который позволяет обобщать код и задачи по обработке исключений. +Например, аспекты позволяют ответить на вопросы: + +- кто виноват: программист или система; +- что делать: игнорировать проблему или немедленно решать. + +## Нормализация исключений + +Нормализация исключений - это поддержка системой ограниченного числа выброшенных исключений. +Она может достигаться такими способами: + +1. Наследование. +2. Привидение к контейнеру. +3. Интерфейсы. + +Например, привидение к контейнеру: + +```php + try + { + ... + } + catch(\Exception $e) + { + // BaseException наследует характеристики $e + throw new BaseException($e); + } +``` + +## Регистрация исключений + +**BaseException** позволяет регистрировать исключения в глобальном реестре до того, как исключение, будет выброшено. +На текущий момент эта возможность существует, но не используется на практике. +Тем не менее в некоторых ситуациях, она может стать дополнительным инструментом для анализа исключений. + +## Небольшая библиотека готовых исключений + +Кроме реализации сервисного кода, **BaseException** так же содержит небольшой список готовых классов для создания типичных исключений. diff --git a/docs/ru/01-Overview.md b/docs/ru/01-Overview.md new file mode 100644 index 0000000..25d61ca --- /dev/null +++ b/docs/ru/01-Overview.md @@ -0,0 +1,111 @@ + +# Overview + +# BaseException::__construct() + +Основное использование: + +```php + throw new BaseException('message', 0, $previous); +``` + +Список параметров: + +```php + // использование array() + $exception = new BaseException + ([ + 'message' => 'message', + 'code' => 0, + 'previous' => $previous, + 'mydata' => [1,2,3] + ]); + + ... + + // print_r([1,2,3]); + print_r($exception->get_data()); +``` + +Исключение-контейнер: + +```php + try + { + try + { + throw new \Exception('test'); + } + catch(\Exception $e) + { + // наследует данные исключения + throw new BaseException($e); + } + } + catch(BaseException $exception) + { + // вывод "test" + echo $exception->getMessage(); + } +``` + +Контейнер используется для изменения флага `is_loggable`: + +```php + try + { + try + { + // исключение, не подлежащее журналированию! + throw new BaseException('test'); + } + catch(\Exception $e) + { + // журналируем BaseException, но не LoggableException + throw new LoggableException($e); + } + } + catch(LoggableException $exception) + { + // вывод: "true" + if($exception->getPrevious() === $e) + { + echo 'true'; + } + } +``` + +## Наследование от BaseException + +```php +class ClassNotExist extends BaseException +{ + // Это исключение будет журналироваться + protected $isLoggable = true; + + /** + * ClassNotExist + * + * @param string $class Имя класса + */ + public function __construct($class) + { + parent::__construct + ( + array + ( + 'message' => "Class '$class' does not exist", + 'class' => $class + ) + ); + } +} +``` + +## FatalException + +```php +class MyFatalException extends BaseException +{ + // Это исключение имеет аспект "fatal" + protected $isFatal = true; diff --git a/docs/ru/02-Paradigm.md b/docs/ru/02-Paradigm.md new file mode 100644 index 0000000..8fd33e6 --- /dev/null +++ b/docs/ru/02-Paradigm.md @@ -0,0 +1,88 @@ +# Paradigm + +## Исключения и логирование + +Создание классов исключений тесно связаны с методикой логирования. + +Исключения создаются с двумя целями: + +1. Передать информацию о произошедшей ошибке для кода. +2. Предоставить достаточную информацию для логирования. + +Для решения первой задачи, используются классы и интерфейсы, позволяя коду по типу исключения определить, что произошло. +Для решения второй задачи, используются метаданные исключения, которые позволяют логировать исключения с дополнительной информацией. + +Поэтому, создавая новый класс исключения, необходимо учитывать, +что он должен содержать метаданные, которые будут использоваться для логирования. + +```php + +final class UserNotAllowed extends \IfCastle\Exceptions\BaseException +{ + protected string $template = 'User {user} from {ip} is not allowed'; + protected array $tags = ['user', 'auth']; + + public function __construct(string $user, string $ip) + { + parent::__construct([ + 'user' => $user, + 'ip' => $ip + ]); + } + +``` + +В данном примере, класс `UserNotAllowed` содержит шаблон сообщения и теги, которые будут использоваться для логирования. +Когда это исключение попадёт в логгер, он сможет использовать шаблон сообщения и теги для формирования сообщения об ошибке. + +```php + $exception = new UserNotAllowed('admin', '127.0.0.1'); + $logger->error($exception); +``` + +Если в качестве системы логирования вы используете OpenTelemetry, +в таком случае метаданные исключения будут использоваться для формирования атрибутов, +которые попадут в трейсер вместе с тегами и шаблоном сообщения. + +## Исключения и теги + +Теги позволяют сгруппировать исключения произвольным образом. + +Создавая класс исключения можно определить теги, которые позже попадут в логгер. + +```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/ru/03-BaseException.md b/docs/ru/03-BaseException.md new file mode 100644 index 0000000..ecb433e --- /dev/null +++ b/docs/ru/03-BaseException.md @@ -0,0 +1,369 @@ +# BaseException + +## Конструктор + +Конструктор `BaseException` имеет три режима работы: + +1. Стандартный (как это определено в `\Exception`). +2. Режим структурированных данных. +3. Режим контейнера. + +Наследуемые исключения могут так же прозрачно поддерживать эти режимы, а могут перекрывать их своим конструктором. + +Например, класс `UnexpectedValue`, может вести себя как `BaseException`, +если параметр `$name` будет массивом. + +```php +/** + * 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) + { + if (!\is_scalar($name)) { + parent::__construct($name); + return; + } + + parent::__construct([ + 'name' => $name, + 'value' => $this->toString($value), + 'message' => $rules, + 'type' => $this->typeInfo($value), + ]); + } +} +``` + +## Шаблоны сообщений + +Каждое исключение может иметь свой уникальный шаблон сообщения, которые можно определить в свойстве `template`: + +```php +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` работает следующим образом: + +1. *Placeholders* шаблона заменяются данными из контекста, и передаются в конструктор `\Exception`. +Таким образом, метод `getMessage()` вернёт полный текст ошибки. +2. Если в конструктор `BaseException` передано свойство `message`, +оно воспринимается как дополнительное сообщение, и присоединятся к результирующему сообщению через точку. Например: + +```php + + $exception = new BaseException + ([ + 'template' => 'This is the template', + 'message' => 'This is a message' + ]); + + echo $exception->getMessage(); + +// Result: +// This is the template. This is a message + +``` + +## Управление журналированием + +Класс `BaseException` имеет два флага, которые управляют журналированием: +* `isLoggable` - флаг, указывающий, что исключение должно быть записано в журнал. +* `isFatal` - флаг, указывающий, что исключение является фатальным. + +Каким образом эти флажки будут использоваться, зависит от реализации журналирования, однако общие правила таковы: +1. Если не установлен флаг `isLoggable`, то исключение не будет записано в журнал. +2. Если исключение является фатальным, то оно будет записано в журнал. +3. Если исключение не является фатальным, но установлен флаг `isLoggable`, то оно будет записано в журнал. + +Фатальные исключения могут быть обработаны особым образом, но это зависит от реализации. + +## Наследование + +В большинстве случаев дочерний класс переопределяет только конструктор, так как ему нужно сформировать данные исключения. + +Если дочернему классу требуется изменить поведение исключения, он может изменять унаследованные свойства. +В данном случае - это поведение является нормальным. + +Упрощённый алгоритм `BaseException::__construct`: + +1. Анализировать входные данные. +2. Вызвать родительский конструктор `\Exception`. +3. Если `BaseException::isLoggable` финализировать исключение. +4. Если `BaseException::isFatal` вызвать обработчик фатальных исключений. + +Конструктор может изменять любые свойства после своего вызова. +Поэтому, если нужно модифицировать свойства класса окончательно, делайте это после вызова базового конструктора. + +Изменение свойств флажков (с префиксом `is`) вызывает изменение в поведении базового конструктора. + +Например, вот так можно включить журналирование: + +```php + class LoggableException extends BaseException + { + /** + * Флаг логирования. + * Если флаг равен true - то исключение + * собирается быть записанным в журнал. + * + * @var boolean + */ + protected $isLoggable = true; + } +``` + +А вот это исключение является фатальным: + +```php + class MyException extends BaseException + { + protected $isFatal = true; + } +``` + +Класс `BaseException` не журналируется и не имеет аспектов. +Поэтому для наследования удобно иметь дополнительные "базовые исключения": + +1. `LoggableException` - для исключений, которые журналируются. +2. `SystemException` - для исключений с аспектом "системное". +3. `RuntimeException` - для исключений с аспектом "runtime". +4. `FatalException`, `FatalRuntimeException`, `FatalSystemException` - для фатальных исключений. + +(подробнее о морализировании в разделе: [Журналирование][1]) + +## Рекомендации по формированию данных + +Обратите внимание, что конструктор `BaseException` не изменяет данные, которые вы передаёте ему. +Эту работу обязаны делать дочерние исключения. + +Хорошо придерживаться рекомендаций: + +1. Не хранить в исключении объекты. +2. При формировании `data` усекать данные. +3. Сохранять только самое важное и необходимое. +4. Использовать в массиве `data` только базовые типы: скаляры и массивы. + +### Не хранить в исключении объекты + +Исключения могут попадать в журнал, и хранится в памяти намного дольше, чем объекты, переданные в исключение. +Таким образом, вы будете раздувать приложение, так как сборщик мусора не сможет освободить память. +Если для Web-скриптов это не актуально, то для фоновых заданий - скорее всего. + +### Усечение данных + +Свойство `BaseException::data` попадает в журнал. +Хотя данные, которые попадают в журнал, сохраняют структуру, и могут быть десериализированы заново, +не стоит использовать большие массивы данных. +Это не только замедляет процесс обработки журнала, но и создаёт ненужную избыточность. +Если в самом деле необходимо сохранять большие массивы данных, лучше использовать отладочный режим. + +Для усечения данных можно использовать методы: + +- `BaseException::getType()`; +- `BaseException::truncate()`. + +### Использование в data базовых типов + +Это требование имеет с одной стороны тот же смысл, что и "Не хранить объекты в исключении". +Но главная причина иная. Свойство `data` может участвовать в процессе сериализации исключения. +А объекты не всегда можно правильно сериализировать/десериалировать. +Хотя можно воспользовать интерфейсом `Serializable` - это не самая хорошая идея, +так как целью сериализации могут быть другие форматы, отличные по сути от `Serializable`. + +Если вы хотите получить возможность легко сериализировать исключения - пытайтесь строго следовать этому правилу. + +## Отладочные данные + +Для сохранения отладочных данных, +`BaseException` предоставляет метод `set_debug_data()`. +Этот метод имеет атрибут `protected`, а значит предназначен только для внутреннего использования. + +Алгоритм `BaseException::setDebugData()`: + +1. Проверить, активен ли режим отладки. +2. Если да, перевести данные в строку, и сохранить их. + +Пример использования: + +```php + class UnexpectedValue extends LoggableException + { + public function __construct($name, $value = null) + { + // Для журнала отладки сохраним полные данные: + $this->setDebugData(['value' => $value]); + + parent::__construct + ([ + 'message' => 'Unexpected value', + 'name' => $name, + 'type' => self::getValueType($value) + ]); + } + } +``` + +Используя свойство `isDebug`, вы можете намеренно записать данные, в независимости от того, включён ли режим отладки или нет: + +```php + class UnexpectedValue extends LoggableException + { + public function __construct($name, $value = null) + { + // Принудительное включение режима отладки + $this->isDebug = true; + // Для отладчика сохраним полные данные: + $this->setDebugData(['value' => $value]); + + parent::__construct + ([ + 'message' => 'Unexpected value', + 'name' => $name, + 'type' => self::get_value_type($value) + ]); + } + } +``` + +При этом отладочные данные запишутся только для вашего исключения. +Это позволяет быстро ввести отладочный режим даже на рабочей системе, только в необходимом месте. + +Таким же образом, можно и отключить отладочный режим: + +```php + // Все исключения, которые наследуются от этого + // получат выключенный отладочный режим + // (если конечно не переопределят его явно) + class NoDebugException extends LoggableException + { + protected $isDebug = false; + } +``` + +## Сериализация + +Кроме основных методов `BaseException` поддерживает сериализацию/десериализацию. +Это может быть полезно для маршалинга исключений через удалённые сервисы, +сохранения исключений на диск и прочего, где обычно используется сериализация. + +Целевым форматом сериализации является асоциативный массив. +Массив выбран потому, что его легко преобразовать в любой другой формат: `json`, `xml`, `phpserialize`, etc. + +Формат массива: + +```php + [ + 'type' => get_class($exception), + 'source' => ['source' => '', 'type' => '', 'function' => ''], + 'message' => $exception->getMessage(), + 'template' => $exception->template(), + 'code' => $exception->getCode(), + 'data' => $exception->get_data(), + 'container' => 'если это контейнер-исключение' + ]; +``` + +Исключения контейнеры возвращают сериализацию не самих себя, а исключения `previous`. + +За общую сериализацию/десериализацию отвечают методы: + +- `BaseException::toArray()`; +- `BaseException::arrayToErrors()`. + +Метод arrayToErrors является защищённым, и предназначен для использования в дочерних классах, только в том случае, +если исключение может быть восстановлено из сериализованных данных. +Обычно такие исключения являются DTO объектами, что не является общим случаем. + +## Особенности + +Следующие методы `BaseException` существуют потому, что стандартные методы `PHP` `\Exception` объявлены как `final`, +но поведение их не соответствует общей парадигме `BaseException`. + +### Определение источника исключения + +Для определения источника исключения используется специальный метод `BaseException::get_source()`. Он возвращает ассоциативный массив вида: + +```php + [ + 'source' => 'класс или файл:строка, где возникло исключение', + 'type' => 'тип вызова', + 'function' => 'имя функции или метода, где возникло исключение' + ]; +``` + +Метод `BaseException::getSource()` используется при журналировании вместо метода +`BaseException::getFile()`, +потому что определение источника по имени класса является более естественным методом относительно устройства языка. +После введения `namespace` и `autoload` реальная структура файлов больше не имеет большого значения, +а информация об `namespace` сохраняется и тогда, когда весь код объединяется в один файл. + +Обратите внимание, что порядок ключей в массиве `source` имеет значение, и он создан таким образом, что если выполнить слияние массива в строку, то вы получите строковое указание источника: + +```php + + namespace Test; + + class ZClass + { + function zfun() + { + throw new BaseException('...'); + } + } + + try + { + $Test = new ZClass(); + $Test->zfun(); + } + catch(BaseException $e) + { + // out: Test\ZClass->zfun + echo implode('', $exception->getSource()); + } +``` + +Для вычисления источника классов `PHP` `\Exception` используйте метод `HelperTrait::getSourceFor()`. + +### Определение вложенного исключения + +`BaseException` создаёт контейнеры для любых объектов, которые поддерживают интерфейс `BaseExceptionInterface`. +Этот приём позволяет оборачивать в исключения другие объекты, которые имеют схожее поведение, но не являются `PHP` исключениями. + +Однако `\Exception::$previous` может быть только дочерним объектом от `\Exception`. Чтобы обойти это органичение, `BaseException` помещает объект `BaseExceptionI` в свойство `data` (в контейнере это свойство не используется для данных). + +Для доступа к вложенному объекту `BaseExceptionI` следует использовать метод `BaseExceptionI::get_previous()`, который всегда вернёт правильный объект: + +- `\Exception`; +- либо `BaseExceptionI`; +- либо `null`, если объекта нет. + +[1]: 04-Logging.md "Журналирование" diff --git a/docs/ru/04-Logging.md b/docs/ru/04-Logging.md new file mode 100644 index 0000000..d5ecc84 --- /dev/null +++ b/docs/ru/04-Logging.md @@ -0,0 +1,205 @@ +# Logging + +## Процесс журналирования + +выделить текст блоком + +> Функциональность `Registry` и `StorageInterface` является устаревшей и не рекомендуется к использованию. + +Журналирование проходит в два этапа: + +1. Исключение попадает в хранилище реестра (`Registry`). +2. Реестр передаёт исключения в журнал. + +Для этого, код `BaseException` вызывает `Registry::register_exception(…)`, +которая в свою очередь передаёт исключение в глобальное хранилище исключений: + +```php + static public function register_exception($exception) + { + ... + self::$exceptions->add_exception($exception); + ... + } +``` + +Это можно отобразить на схеме: + + __construct -> Registry::register_exception -> StorageI + +где: + +- `__construct` - конструктор исключения (неважно какого). +- `Registry` - статический класс. +- `StorageInterface` - объект, который на самом деле хранит исключения. + +Где же тут журнализатор? Правильно - его здесь нет. И его не должно быть. +Так как вопросы журналирования сами по себе никак не относятся к библиотеке `BaseException`. + +Единственной точкой интеграции является метод: `Registry::save_exception_log()`. + +Он вызывается в тот момент, когда можно сохранить реестр исключений. +Когда именно? Всё зависит от ситуации. Например, это может быть момент завершения программы. +Всё, что делает этот сложный метод, можно увидеть в коде: + +```php + static public function save_exception_log() + { + if(is_callable(self::$save_handler)) + { + call_user_func(self::$save_handler, …); + } + } +``` + +Метод вызывает зарегестрированный обработчик `$save_handler`, +который определяется методом `Registry::set_save_handler($callback)`. +Именно на этот обработчик и ложится задача реального журналирования исключений. + +## Предпологаемый алгоритм журнализатора + +Хотя `BaseException` никак не может повлиять на реализацию журнализатора, +тем не менее существует некоторое ожидаемое поведение, которое журнализатор должен поддерживать. + +Прототип обработчика `$save_handler`: + +```php + /** + * Журнализатор исключений. + * + * @param array $exceptions Хранилище исключений + * @param callable $reset_log Функция сброса журнала + * @param ArrayAccess $logger_options опции журналирования + * @param ArrayAccess $debug_options опции для отладки (профайлинга) + */ + function save_handler + ( + $exceptions, + callable $reset_log, + $logger_options = [], + $debug_options = [] + ) +``` + +Общий алгоритм: + +1. Журнализатор получает список исключений (его копию). +2. Журнализатор обходит список, чтобы записать его в журнал. +3. Журнализатор вызывает делегат `$reset_log`, чтобы очистить от журнала исключений. + +Порядок действий алгоритма означает, что если работа журнала завершится как-то неожиданно, +список исключений останется. Но на самом деле, ситуация сложнее. + +Пока журнал записывает данные, в списке исключений могут появиться новые записи. +Речь не идёт о "паралельных" потоках, а о коде самого журнализатора. +Это означает, что журнализатор обязан гарантировать, что ни одно исключение не попадёт в журнал, +а если такие исключения и возникнут - они "остаются на совести" у журнализатора. + +Каждое исключение поддаётся обработке: + +1. Если `isLoggable()` === false, исключение не попадает в журнал! +2. Если `getDebugData()` не пуст (empty()), тогда реализуется алгоритм записи в журнал для отладки. +3. Определить аспект исключения и журнал, в который оно будет записано. +4. Сериализовать данные и записать в выбранный журнал. + +Обратите внимание: + +1. Даже если исключение находится в реестре, оно может не попасть в журнал, если флаг `isLoggable()` +на момент реальной журнализации будет равен `false`. +2. В независимости от того включён или нет режим отладки или профилирования, данные `getDebugData()` +должны быть обработаны. +3. Отладочный журнал может находиться отдельно от остальных. + +## Управление флагом isLoggable + +Только что мы установили важный момент, +что любое журналируемое исключение может попасть в реестр исключений, но не попасть в реальный журнал. + +Достаточно часто в конструкторе исключения невозможно знать заранее: нужно ли его журнализировать или нет. Например, если файл не доступен, с одной стороны это серьёзная ошибка, а с другой стороны - может быть ожидаемая ошибка. Решение о журналировании должен принять код свыше. Но этот случай так же косвенно указывает на неправильное использование исключений. + +С другой стороны, возможна ситуация, в которой код верхнего уровня может поместить в журнал более точное описание ошибки. В этом случае, предыдущее исключение, ему следует подавить: + +```php + try + { + ... + throw new LoggableException + ( + 'А того великана я знал - он у меня пастухом работал. + Да вот беда - на прошлой неделе пил он воду из этой чашки, + поскользнулся, упал в нее и утонул. + Три дня искали - не нашли, видно, в море унесло…' + ); + ... + } + catch(BaseException $kingdoms) + { + // Полцарства?! + // Выбрасываем другое исключение, которое попадёт в журнал + throw new LoggableException + ( + 'неправда', + 0, + $kingdoms->setLoggable(false) + ); + } +``` + +## Аспекты журналирования + +Для определения аспектов исключения, используются интерфейсы, так как они являются "естественными элементами языка", могут быть легко проверены и получены, и являются глобальными для всей программы. + +`BaseException` предлагает реализацию основных аспектов: + +1. Для системных сбоев, используется `SystemExceptionI`. +2. Для сбоев из-за внешних факторов `RuntimeExceptionI`. +3. Отсутствие аспекта - причиной ошибки является сам код программы. + +В основном аспекты используются для сортировки потока сообщений: + +1. В какой журнал поместить исключение. +2. На какой email отправить уведомление. +3. Как пометить исключение в журнале. + +Так как, аспекты определяются в виде интерфейсов, это побуждает: + +1. В каждом проекте продумать свои аспекты, если нужно. +2. Не злоупотреблять количеством аспектов. + +Что хорошо согласуется с парадигмой: чем меньше аспектов - тем лучше. + +`BaseException` убедительно рекомендует **никогда не создавать своих аспектов**. +Потому, что число аспектов не зависит от размера системы: будь то Операционная система, или приложение "Hello World". + +Если всё-таки нужно отделить группу исключений для отдельной подсистемы, +нужно перегрузить механизм регистрации исключения в реестре, и использовать другой реестр. Так как, если аспекты являются глобальными для всех компонентов системы, то отдельное журналирование требуется только конкретному модулю. + +## Переопределение реестра исключений + +Любое дочернее исключение от `BaseException` всегда может переопределить регистрацию исключения: + +```php +class MyException extends LoggableException +{ + public function __construct(MyObjectI $object, LoggerI $logger) + { + // Сохранить реальное состояние флага + $is_loggable = $this->isLoggable; + + // Это исключение не попадает в стандартный реестр + $this->isLoggable = false; + parent::__construct($this->format_data($object)); + + $this->isLoggable = $is_loggable; + + if($is_loggable) + { + // Но записывается в свой + $logger->add_to_log($this); + } + } +} +``` + +Обратите внимание, что код из примера сохраняет логику флага `$isLoggable`, +и позволяет наследуемым исключениям переопределять его значение. diff --git a/docs/ru/05-Registry.md b/docs/ru/05-Registry.md new file mode 100644 index 0000000..6f66235 --- /dev/null +++ b/docs/ru/05-Registry.md @@ -0,0 +1,96 @@ +# Registry + +> Функциональность `Registry` и `StorageInterface` является устаревшей и не рекомендуется к использованию. + +Класс `Registry` является статическим. Хотя в `PHP` пока нет статических классов по типу C#, в `Registry` вызов конструктора запрещён. + +`Registry` обеспечивает регистрацию исключений до того, как они будут помещены в журнал. Для этого `Registry` использует Хранилище, которое может быть переопределено программистом: + +```php + static public function set_registry_storage(StorageI $storage) +``` + +Интерфейс `StorageInterface` содержит всего несколько методов, и позволяет влиять на ход регистрации исключений. + +Дополнительное расширение функциональности `Registry` можно получить, используя методы: + +- `set_unhandled_handler` - необработанные исключения. +- `set_fatal_handler` - ошибка с флагом `is_fatal`. + +Метод `set_unhandled_handler` используется тогда, когда `Registry` позволено обрабатывать потоки ошибок и исключений `PHP`. Для этого нужно вызвать метод `Registry::install_global_handlers()`, после чего в реестр начнут попадать все необработанные исключения и ошибки. + +Обработчик `set_unhandled_handler` будет вызван после основного обработчика `Registry::exception_handler(\Exception $exception)`. + +Метод `set_fatal_handler` будет рассмотрен подробнее в "Fatal Exception". + +## Журналирование ошибок PHP + +`Registry` поддерживает журналирование не только исключений, но и потока ошибок `PHP`. Для этого он использует класс `Exceptions\Errors\Error`, который хотя и не является исключением, но реализует интерфейс `BaseExceptionI`. + +Все ошибки кроме `E_USER_*`, считаются ошибками программиста, и попадают в общий журнал. Это же касается `E_NOTICE` и `E_DEPRECATED`. + +Таким образом `Registry` требует, чтобы код `PHP` был полностью чист от предупреждений любого рода. + +## Необработанные исключения + +Что произойдёт с исключением, если оно не было обработано и попало в `Registry::exception_handler()`? + +```php + if($exception instanceof BaseExceptionI) + { + // Если исключение достигает обработчика + // оно не логируется в случае, если: + // - уже было журналированным + // - или если является контейнером для другого исключения + if($exception->is_loggable() || $exception->is_container()) + { + new UnhandledException($exception); + return; + } + + $exception->set_loggable(true); + } + + self::register_exception($exception); + + new UnhandledException($exception); + ... +``` + +`Registry` придерживается алгоритма: + +1. Если исключение `BaseException` имеет сброшенный флаг `is_loggable` оно всё равно будет журналировано как необработанное. +2. Если исключение уже находится в журнале - нет смысла его помещать туда снова. +3. Если исключение является контейнером, то ответственность за журналирование лежит на нём. + +Обратите внимание на `new UnhandledException($exception)`. Это исключение используется только как маркер факта, что последнее исключение не было поймано: + +```php +class UnhandledException extends LoggableException +{ + public function __construct(\Exception $exception) + { + parent::__construct + ([ + 'message' => 'Unhandled Exception', + 'type' => get_class($exception), + 'source' => self::get_source_for($exception), + 'previous' => $exception + ]); + } +} +``` + +Таким образом в журнале сохранится отдельная запись об необработанном исключении, и можно будет отличить два случая: когда целевое исключение было обработано, а когда - нет. + +## Двойное журналирование + +Нужно отметить, что возможен особый случай двойного журналирования: + +1. Исключение имеет установленный флаг `is_loggable` и попадает в журнал. +2. Другой код ловит исключение, и сбрасывает флаг +3. Другой код снова выбрасывает это же исключение +4. Исключение становится наобработанным. +5. Так как его флаг `is_loggable` равен `false`, оно логируется заново. + +Я решил оставить данный Use Case как есть, и допустить двойную запись исключения, так как, если в коде происходит что-то подобное, значит его нужно срочно пересмотреть. diff --git a/docs/ru/06-FatalException.md b/docs/ru/06-FatalException.md new file mode 100644 index 0000000..9c32cbd --- /dev/null +++ b/docs/ru/06-FatalException.md @@ -0,0 +1,76 @@ +# Fatal Exception + +Аспект "Фатальная ошибка" реализован в виде свойства `isFatal`, а так же публичного метода: `isFatal()`. +Интересно заметить, что по сравнению с флагом `isLoggable()` +у флага `isFatal` нет возможности сброса (разве что изнутри класса). + +Так как `isFatal` - это свойство, то фатальным может стать любое исключение в любой момент времени: + +```php + try + { + ... + } + catch(BaseException $e) + { + throw $e->markAsFatal(); + } +``` + +Можно так же использовать исключение-контейнер, чтобы наделить этой характеристикой другое исключение: + +```php + try + { + ... + } + catch(BaseException $e) + { + // Теперь исключение $e - имеет аспект фатального. + throw new FatalException($e); + } +``` + +## Обработчик фатальных исключений + +`Registry` предоставляет метод `Registry::setFatalHandler($callback)` для обработки появления фатальных исключений. + +Прототип обработчика: + +```php + function(BaseExceptionI $exception) + { + // Если это контейнер - используем его содержимое + if($exception->isContainer()) + { + $real_exception = $exception->getPrevious(); + } + else + { + $real_exception = $exception; + } + ... + } +``` + +Обработчик фатальных исключений вызывается в момент, когда исключение становится фатальным, то есть: + +1. В конце конструктора исключения `BaseException`. +2. В момент вызова метода `setFatal()`. + +Обработчик фатального исключения вызывается не для журналирования. +Он нужен для введения особого специального алгоритма, в условиях возможной нехватки ресурсов. +Возможные задачи обработчика таковы: + +- остановить программу или сервис; +- предотвратить повторные запуски work-еров, пока проблема не решиться; +- запустить процесс анализа сбоя. + +## Особенности обработки для журнализатора + +Журнализатор так же может обрабатывать фатальное исключение не так, как обычно: + +1. Проверить возможность записи на диск или в файл журнала. +2. Если файл журнала не доступен, попытаться использовать системный журнал. +3. Использовать EMAIL для отправки уведомления. +4. Если EMAIL не работает, использовать альтернативные каналы.