Я хотел бы реализовать механизм журналирования для файла в 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("Ymd 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);
В общем, есть два основных варианта использования логгеров в вашем коде:
инвазивный каротаж:
По большей части люди используют этот подход, потому что это проще всего понять.
В действительности вы должны использовать только инвазивное ведение журнала, если ведение журнала является частью самой логики домена. Например, в классах, связанных с платежами или управлением конфиденциальной информацией.
Неинвазивное каротаж:
С помощью этого метода вместо изменения класса, который вы хотите зарегистрировать, вы переносите существующий экземпляр в контейнер, который позволяет отслеживать каждый обмен между экземпляром и остальной частью приложения.
Вы также можете временно включить такое ведение журнала, отлаживая некоторую конкретную проблему за пределами среды разработки или когда вы проводите некоторое исследование поведения пользователя. Поскольку класс регистрируемого экземпляра никогда не изменяется, риск нарушения поведения проекта намного ниже по сравнению с инвазивным протоколированием.
Для этого у вас есть два основных подхода. Вы можете либо ввести экземпляр, который реализует интерфейс Logger
, либо предоставить классу фабрику, которая, в свою очередь, будет инициализировать систему ведения журнала только в случае необходимости.
Заметка:
Поскольку кажется, что прямая инъекция – это не какая-то скрытая тайна для вас, я оставлю эту часть … только я бы настоятельно рекомендовал вам избегать использования констант вне файла, где они были определены.
Теперь .. реализация с заводской и ленивой загрузкой.
Вы начинаете с определения 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 .. ); .... } .... }
Эта фабрика будет иметь два дополнительных преимущества:
Заметка:
Собственно, при написании этого класса класс Foobar зависит только от экземпляра, реализующего интерфейс Creator. Обычно вы вводите либо построитель (если вам нужно ввести экземпляр, возможно, с некоторыми настройками), либо фабрику (если вы хотите создать другой экземпляр с тем же интерфейсом).
Следующим шагом будет внедрение фабрики :
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');
, фабрика ищет, если экземпляр уже создан. Если поиск не выполняется, он создает новый экземпляр.
Примечание. На самом деле я не совсем уверен, что это можно назвать «фабрикой», поскольку инстанцирование действительно инкапсулировано в анонимные функции.
И последний шаг на самом деле заключается в подключении всех провайдеров:
$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; } } }
Этот класс также может использоваться вместе с описанной выше ленивой фабрикой.
Чтобы использовать эту структуру, вы просто выполняете следующие действия:
$instance = new Foobar; $instance = new LogBrane( $instance, $logger ); $instance->someMethod();
В этот момент контейнер, который обертывает экземпляр, становится полностью функциональной заменой оригинала. Остальная часть вашего приложения может обрабатывать его, как если бы это был простой объект (пройдите, вызовите методы). И сам обернутый экземпляр не знает, что он регистрируется.
И если в какой-то момент вы решите удалить журнал, то это можно сделать, не переписывая остальную часть вашего приложения.
Цель Logger – сохранить информацию об отладке. Logger должен быть классом с интерфейсом для хранения сообщений и уровня бедствия. Реализация является вторичной. Сегодня вы хотите регистрировать файлы. Завтра вы можете поместить журналы в базу данных. Так что логика должна быть написана на стороне класса журнала. Есть уже написанный хороший журнал под названием Монолог https://github.com/Seldaek/monolog
Если вы хотите создать полную систему ведения журнала, с поддержкой ведения журнала на разные выходы, 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('Ymd 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);
Не проверен, но должен работать.