Статьи

Ведение журнала с PSR-3 для улучшения возможности повторного использования

Ведение журнала является одной из самых распространенных задач в PHP. Мы используем журналы для отслеживания сообщений об ошибках, записи важных событий и устранения проблем с нашим кодом. В любом PHP-проекте код, вероятно, будет полон обращений к библиотеке журналов, которая обрабатывает эти действия для нас.

К сожалению, наличие обращений к библиотеке журналов, разбросанной по всему нашему коду, делает код зависимым от доступности этой библиотеки, что является явным нарушением принципа инверсии зависимости. Даже если мы используем внедрение зависимостей для предоставления нашим объектам доступа к библиотеке журналов, различия между библиотеками журналов означают, что переключение между ними может быть трудным и длительным, что требует серьезного рефакторинга всей нашей кодовой базы.

Для обеспечения совместимости между библиотеками журналирования группа PHP-FIG недавно выпустила PSR-3 , общий интерфейс для объектов журналирования. В этой статье я расскажу, как интерфейс регистратора, определенный в PSR-3, позволяет нам писать повторно используемый код, который не зависит от какой-либо конкретной реализации ведения журнала.

Во-первых, быстрый учебник

Прежде чем мы рассмотрим, как PSR-3 может сделать наш код более пригодным для повторного использования, необходимо понять, что такое PSR-3. Если вы уже знакомы с PSR-3, вы можете пропустить этот раздел.

Сердцем спецификации является интерфейс для объектов логгера. Этот интерфейс предоставляет восемь методов для обработки сообщений различной степени серьезности и универсальный метод log()

Восемь уровней серьезности, поддерживаемых PSR-3, основаны на RFC 5424 и описаны ниже:

  • Emergency
  • Alert
  • Critical
  • Error
  • Warning
  • Notice
  • Info
  • Debug

Каждый из методов ведения журнала принимает сообщение, которое должно быть строкой или объектом с методом __toString() Дополнительный аргумент принимает массив, который может быть предоставлен для предоставления контекстной информации для сообщения журнала.

Полное объяснение этих методов и параметров можно найти в спецификации PSR-3 .

Получение файлов PSR-3

Получить файлы, необходимые для работы с PSR-3, легко – вы можете найти их в репозитории Psr / Log GitHub . Вы также можете использовать Composer для получения файлов от Packagist . Ниже приведен пример файла composer.json для извлечения файлов Psr / Log:

 {
    "require": {
        "psr/log": "dev-master"
    }
}

Как регистрация может ограничить повторное использование кода

Существует множество различных библиотек журналов для PHP, каждая со своим подходом к сбору и записи данных. Хотя среди них есть некоторые общие идеи, каждая библиотека имеет свой уникальный набор методов ведения журналов. Это означает, что переключение между регистраторами может быть сложной задачей, часто требующей изменения кода везде, где используется регистрация. Это противоречит идее повторного использования кода и принципам SOLID объектно-ориентированного проектирования. Мы остаемся в ситуации, которая требует, чтобы мы либо объявили зависимость от конкретной библиотеки журналов, либо полностью избегали журналирования.

Чтобы проиллюстрировать эту проблему более четко, нужен конкретный пример. Допустим, мы создаем простой объект Mailer для обработки отправки электронной почты. Мы хотим, чтобы Mailer регистрировал сообщение при отправке электронного письма, и мы решили использовать превосходную библиотеку Monolog для удовлетворения наших потребностей в журнале.

 <?php
namespace Email;

class Mailer
{
    private $logger;
    
    public function __construct($logger)
    {
        $this->logger = $logger;
    }
    
    public function sendEmail($emailAddress)
    {
        // code to send an email...
        
        // log a message
        $this->logger->addInfo("Email sent to $emailAddress");
    }
}

Мы можем использовать этот класс со следующим кодом:

 <?php
// create a Monolog object
$logger = new MonologLogger("Mail");
$logger->pushHandler(new MonologHandlerStreamHandler("mail.log"));

// create the mailer and send an email
$mailer = new EmailMailer($logger);
$mailer->sendEmail("email@example.com");

Запуск этого кода приведет к появлению новой записи в файле mail.log

На этом этапе у нас может возникнуть соблазн сказать, что мы написали многократно используемый объект Mailer. Мы делаем регистратор доступным для Mailer с внедрением зависимостей, чтобы мы могли менять конфигурацию регистратора без необходимости касаться кода Mailer. Похоже, что мы успешно следовали принципам SOLID и избежали создания каких-либо жестких зависимостей.

Но предположим, что мы хотим повторно использовать наш класс Mailer в другом проекте, который использует Analog для обработки взаимодействий журналирования. Теперь мы столкнулись с проблемой, потому что у Analog нет метода addInfo() Чтобы записать сообщение информационного уровня с Analog, мы вызываем Analog::log($message, Analog::INFO)

Мы могли бы изменить наш класс Mailer для использования методов Analog, как показано ниже.

 <?php
namespace Email;

class Mailer
{
    public function sendEmail($emailAddress)
    {
        // code to send an email...
        
        // log a message
        Analog::log("Email sent to $emailAddress", Analog::INFO);
    }
}

Мы можем использовать обновленный класс Mailer, используя следующий код:

 <?php
// set up Analog
Analog::handler(AnalogHandlerFile::init("mail.log"));

// create the mailer and send an email
$mailer = new EmailMailer();
$mailer->sendEmail("email@example.com");

Хотя это будет работать, это далеко от идеала. Мы столкнулись с зависимостью Mailer от конкретной реализации журналирования, которая требует, чтобы класс изменялся всякий раз, когда вводится новый регистратор. Это делает класс менее пригодным для повторного использования и вынуждает нас выбирать между зависимостью от доступности конкретного регистратора или полным отказом от регистрации в нашем классе.

Использование PSR-3 для избежания зависимости от регистратора

Как объяснил Алехандро Жервасио в своей превосходной статье на эту тему, принцип обращения зависимостей говорит нам, что мы должны зависеть от абстракций, а не от конкрементов. В случае с журналированием нашей проблемой до сих пор было отсутствие достойной абстракции, от которой можно было бы зависеть.

Вот тут-то и появляется PSR-3. PSR-3 предназначен для решения проблемы несовместимых реализаций журналирования, предоставляя универсальный интерфейс для регистраторов, метко названный LoggerInterface Предоставляя интерфейс, который не привязан к какой-либо конкретной реализации, PSR-3 освобождает нас от необходимости полагаться на определенный регистратор – вместо этого мы можем напечатать подсказку для LoggerInterface Я обновил класс Mailer ниже, чтобы продемонстрировать это:

 <?php
namespace Email;

class Mailer
{
    private $logger;
    
    public function __construct(PsrLogLoggerInterface $logger)
    {
        $this->logger = $logger;
    }
    
    public function sendEmail($emailAddress)
    {
        // code to send an email...
        
        // log a message
        $this->logger->info("Email sent to $emailAddress");
    }
}

Конструктор был изменен, чтобы принимать разработчика LoggerInterfacesendEmail()info()

Monolog уже соответствует PSR-3, и Analog предоставляет объект-оболочку, который реализует LoggerInterface

Вот как бы вы назвали класс с Monolog:

 <?php
// create a Monolog object
$logger = new MonologLogger("Mail");
$logger->pushHandler(new MonologHandlerStreamHandler("mail.log"));

// create the mailer and send an email
$mailer = new EmailMailer($logger);
$mailer->sendEmail("email@example.com");

И с аналогом:

 <?php
// create a PSR-3 compatible Analog wrapper
$logger = new AnalogLogger();
$logger->handler(AnalogHandlerFile::init("mail.log"));

// create the mailer and send an email
$mailer = new EmailMailer($logger);
$mailer->sendEmail("email@example.com");

Теперь мы можем использовать наш объект Mailer с любой библиотекой без необходимости редактировать класс Mailer или изменять способ его использования.

Использование шаблона адаптера для регистраторов, которые не поддерживают PSR-3

До сих пор мы успешно отделяли объект Mailer от любой конкретной реализации ведения журналов, запрашивая разработчик LoggerInterface Но как насчет регистраторов, которым еще предстоит добавить поддержку PSR-3? Например, популярная библиотека KLogger некоторое время не обновлялась и в настоящее время не совместима с PSR-3.

К счастью, нам просто сопоставить методы, предоставляемые KLogger, с методами, определенными LoggerInterfaceшаблона адаптера . Вспомогательные файлы в репозитории Psr / Log позволяют легко создавать классы адаптеров, предоставляя нам класс AbstractLogger Абстрактный класс просто перенаправляет восемь специфичных для уровня методов ведения журнала, определенных в LoggerInterfacelog() Расширяя класс AbstractLoggerlog() Я продемонстрирую это ниже, создав простой адаптер для KLogger:

 <?php
namespace Logger;

class KloggerAdapter extends PsrLogAbstractLogger implements PsrLogLoggerInterface
{
    private $logger;    
    
    public function __construct($logger)
    {
        $this->logger = $logger;
    }

    public function log($level, $message, array $context = array())
    {
        // PSR-3 states that $message should be a string
        $message = (string)$message;

        // map $level to the relevant KLogger method
        switch ($level) {
            case PsrLogLogLevel::EMERGENCY:
                $this->logger->logEmerg($message, $context);
                break;
            case PsrLogLogLevel::ALERT:
                $this->logger->logAlert($message, $context);
                break;
            case PsrLogLogLevel::CRITICAL:
                $this->logger->logCrit($message, $context);
                break;
            case PsrLogLogLevel::ERROR:
                $this->logger->logError($message, $context);
                break;
            case PsrLogLogLevel::WARNING:
                $this->logger->logWarn($message, $context);
                break;
            case PsrLogLogLevel::NOTICE:
                $this->logger->logNotice($message, $context);
                break;
            case PsrLogLogLevel::INFO:
                $this->logger->logInfo($message, $context);
                break;
            case PsrLogLogLevel::DEBUG:
                // KLogger logDebug() method does not accept a second
                // argument
                $this->logger->logDebug($message);
                break;
            default:
                // PSR-3 states that we must throw a
                // PsrLogInvalidArgumentException if we don't
                // recognize the level
                throw new PsrLogInvalidArgumentException(
                    "Unknown severity level"
                );
        }
    }
}

Метод log()LoggerInterface Оборачивая класс KLogger таким образом, мы можем использовать его, не нарушая контракт LoggerInterface

Теперь мы можем использовать адаптер KLogger для работы с классом Mailer:

 <?php
// create a new KLogger object which will log to the "logs" directory
$klogger = KLogger::instance("logs");

// inject KLoggger to a PSR-3 compatible KloggerAdapter
$logger = new LoggerKloggerAdapter($klogger);

// send an email
$mailer = new EmailMailer($logger);
$mailer->sendEmail("email@example.com");

Используя класс адаптера, мы можем использовать KLogger без изменения класса Mailer и все еще придерживаться LoggerInterface

KLogger не принимает второй аргумент для сообщений уровня отладки, поэтому он технически не соответствует PSR-3 даже с адаптером. Расширение KLogger до полной совместимости с PSR-3 было бы тривиальной задачей, но она выходит за рамки этой статьи. Тем не менее, можно с уверенностью сказать, что использование нашего класса адаптера очень близко приближает нас к полному соответствию PSR-3 и позволяет использовать LoggerInterface

Вывод

В этой статье мы увидели, как использование PSR-3 может помочь нам написать независимый от регистратора код, который не зависит от конкретной реализации ведения журнала. Многие крупные проекты PHP уже добавили поддержку PSR-3, в том числе Monolog, Symfony и Mustache.php , и другие известные имена, такие как Drupal , обсуждают, как лучше интегрировать его. Поскольку PSR-3 делает журналирование менее барьером для повторного использования кода, мы должны увидеть больше библиотек и структур, правильно использующих журналирование для предоставления полезной информации разработчикам.

Повлияет ли PSR-3 на то, как вы используете логирование в своих приложениях? Дайте нам знать в комментариях ниже.

Изображение через Fotolia