PHP - Внедрить механизм журналирования для работы в нескольких классах

Я хотел бы реализовать механизм журналирования для файла в PHP:

  • путь к файлу журнала будет находиться в файле конфигурации config.php
  • в нескольких классах я хотел бы записать некоторые события в файл журнала

Например:

    Class A {

        public function f_A {
            log_to_file($message);
        }

    }

    Class B {

        public function f_B {
            log_to_file($message);
        }

    }

Буду очень благодарен за любые советы. Я хотел бы реализовать несколько простых и элегантных решений.

Я думал об этом (спасибо за ваши ответы), и я думаю, что я это сделаю (может быть, есть некоторые ошибки, я писал это с нуля):

interface Logger {
    public function log_message($message);
}

class LoggerFile implements Logger {
    private $log_file;

public function __construct($log_file) {
    $this->log_file = $log_file;
}
public function log_message($message) {
        if (is_string($message)) {
            file_put_contents($this->log_file, date("Y-m-d H:i:s")." ".$message."\n", FILE_APPEND);
        }
    }
}

//maybe in the future logging into database

class LoggerDb implements Logger {
    private $db;

    public function __construct($db) {
        //some code
    }
public function log_message($message) {
        //some code
    }
}

Class A {
    private $logger;

public function __construct(Logger $l) {
        $this->logger = $l;
    }


public function f_A {
    $this->logger->log_message($message);
}
}

Class B {
    private $logger;

public function __construct(Logger $l) {
        $this->logger = $l;
    }


public function f_B {
    $this->logger->log_message($message);
}
}

//usage:

//in config.php:

define("CONFIG_LOG_FILE", "log/app_log.log");

//in the index.php or some other files

$logger = new LoggerFile(CONFIG_LOG_FILE);

$instance_a = new A($logger);
$instance_b = new B($logger);

Ответ 1

Где используются регистраторы?

В общем, есть два основных варианта использования логгеров в вашем коде:

  • инвазивный журнал:

    По большей части люди используют этот подход, потому что это легче всего понять.

    В действительности вы должны использовать только инвазивное ведение журнала, если ведение журнала является частью самой логики домена. Например, в классах, связанных с платежами или управлением конфиденциальной информацией.

  • Неинвазивное ведение журнала:

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

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

Внедрение инвазивного регистратора

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

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

Теперь.. реализация с factory и ленивая загрузка.

Вы начинаете с определения API, который вы будете использовать (в идеальном мире вы начинаете с модульных тестов).

class Foobar 
{
    private $loggerFactory;

    public function __construct(Creator $loggerFactory, ....)
    {
        $this->loggerFactory = $loggerFactory;
        ....
    }
    .... 

    public function someLoggedMethod()
    {
        $logger = $this->loggerFactory->provide('simple');
        $logger->log( ... logged data .. );
        ....
    }
    ....
}

Этот factory будет иметь два дополнительных преимущества:

  • он может гарантировать, что создается только один экземпляр без необходимости глобального состояния
  • обеспечить шов для использования при написании модульных тестов

Примечание.. На самом деле, при написании этого способа класс Foobar зависит только от экземпляра, реализующего интерфейс Creator. Обычно вы вводите либо построитель (если вам нужно ввести экземпляр, возможно, с некоторой настройкой), либо factory (если вы хотите создать другой экземпляр с тем же интерфейсом).

Следующим шагом будет реализация factory:

class LazyLoggerFactory implements Creator
{

    private $loggers = [];
    private $providers = [];

    public function addProvider($name, callable $provider)
    {
        $this->providers[$name] = $provider;
        return $this;
    }

    public function provide($name)
    {
        if (array_key_exists($name, $this->loggers) === false)
        {
            $this->loggers[$name] = call_user_func($this->providers[$name]);
        }
        return $this->loggers[$name];
    }

}

Когда вы вызываете $factory->provide('thing');, factory просматривает, если экземпляр уже создан. Если поиск не выполняется, он создает новый экземпляр.

Примечание. Я действительно не совсем уверен, что это можно назвать "factory", поскольку инстанцирование действительно инкапсулировано в анонимные функции.

И последний шаг на самом деле подключить все к провайдерам:

$config = include '/path/to/config/loggers.php';

$loggerFactory = new LazyLoggerFactory;
$loggerFactory->addProvider('simple', function() use ($config){
    $instance = new SimpleFileLogger($config['log_file']);
    return $instance;
});

/* 
$loggerFactory->addProvider('fake', function(){
    $instance = new NullLogger;
    return $instance;
});
*/

$test = new Foobar( $loggerFactory );

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

Внедрение неинвазивного ведения журнала

Основная идея этого подхода заключается в том, что вместо ввода регистратора вы помещаете существующий экземпляр в контейнер, который действует как мембрана между указанным экземпляром и приложением. Эта мембрана может выполнять различные задачи, одна из которых регистрируется.

class LogBrane
{
    protected $target = null;
    protected $logger = null;

    public function __construct( $target, Logger $logger )
    {
        $this->target = $target;
        $this->logger = $logger;
    }

    public function __call( $method, $arguments )
    {
        if ( method_exists( $this->target, $method ) === false )
        {
            // sometime you will want to log call of nonexistent method
        }

        try
        {
            $response = call_user_func_array( [$this->target, $method], 
                                              $arguments );

            // write log, if you want
            $this->logger->log(....);
        }
        catch (Exception $e)
        {
            // write log about exception 
            $this->logger->log(....);

            // and re-throw to not disrupt the behavior
            throw $e;
        }
    }
}

Этот класс также может использоваться вместе с описанным выше ленивым factory.

Чтобы использовать эту структуру, вы просто выполняете следующее:

$instance = new Foobar;

$instance = new LogBrane( $instance, $logger );
$instance->someMethod();

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

И если в какой-то момент вы решите удалить журнал, то это можно сделать, не переписывая остальную часть вашего приложения.

Ответ 2

Задача Logger - сохранить информацию об отладке. Logger должен быть классом с интерфейсом для хранения сообщений и уровня бедствия. Реализация является вторичной. Сегодня вы хотите регистрировать файлы. Завтра вы можете поместить журналы в базу данных. Так что логика должна быть написана на стороне класса журнала. Существует уже написанный хороший журнал под названием Monolog https://github.com/Seldaek/monolog

Ответ 3

Если вам нужна полная структура ведения журнала, с поддержкой ведения журнала на разные выходы, log4PHP является открытым исходным кодом.

Если вам нужна небольшая реализация, которая соответствует вашим потребностям, то что-то вроде этого должно сделать это

class Logger
{
    const INFO = 'info';
    const ERROR = 'error';

    private static $instance;
    private $config = array();

    private function __construct()
    {
        $this->config = require "/path/to/config.php";
    }

    private static function getInstance()
    {
        if(!self::$instance)
        {
            self::$instance = new Logger();
        }
        return self::$instance;
    }

    private function writeToFile($message)
    {
        file_put_contents($this->config['log_file'], "$message\n", FILE_APPEND);
    }

    public static function log($message, $level = Logger::INFO)
    {
        $date = date('Y-m-d H:i:s');
        $severity = "[$level]";
        $message = "$date $severity ::$message";
        self::getInstance()->writeToFile($message);
    }
}

//config.php
return array(
    'log_file' => '/tmp/my_log.txt'
);

Logger::log($message);

Не тестировалось, но должно работать.