I would like to implement logging mechanism to file in PHP:
For example:
Class A {
public function f_A {
log_to_file($message);
}
}
Class B {
public function f_B {
log_to_file($message);
}
}
I will be very grateful for any tips. I would like to implement some easy and elegant solution.
I was thinking about it (thank you for your answers) and I think I will do it this way (maybe, there are some errors, I was writing it from scratch):
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);
PHP's logging functions You can log any event you choose by explicitly calling PHP's error_log() or syslog() function within your code. These functions create logs containing the message string you provide. The syslog() function will use the configuration in your rsyslog. conf file to write log messages.
The PSR-3 (PHP Standards Recommendation) logging standard defines a logging interface with eight methods (debug, info, notice, warning, error, critical, alert, emergency). The message passed to those functions should be a string or an object with a __toString method to cast it into a string.
To log errors in PHP, open the php. ini file and uncomment/add the following lines of code. If you want to enable PHP error logging in individual files, add this code at the top of the PHP file. ini_set('display_errors', 1); ini_set('display_startup_errors', 1); error_reporting(E_ALL);
In general there are two major use-cases for use of loggers within your code:
invasive logging:
For the most part people use this approach because it is the easiest to understand.
In reality you should only use invasive logging if logging is part of the domain logic itself. For example - in classes that deal with payments or management of sensitive information.
Non-invasive logging:
With this method instead of altering the class that you wish to log, you wrap an existing instance in a container that lets you track every exchange between instance and rest of application.
You also gain the ability to enable such logging temporarily, while debugging some specific problem outside of the development environment or when you are conducting some research of user behaviour. Since the class of the logged instance is never altered, the risk of disrupting the project's behaviour is a lot lower when compared to invasive logging.
To do this you have two main approaches available. You can either inject an instance that implements the Logger
interface, or provide the class with a factory that in turn will initialize the logging system only when necessary.
Note:
Since it seems that direct injection is not some hidden mystery for you, I will leave that part out... only I would urge you to avoid using constants outside of a file where they have been defined.
Now .. the implementation with factory and lazy loading.
You start by defining the API that you will use (in perfect world you start with unit-tests).
class Foobar
{
private $loggerFactory;
public function __construct(Creator $loggerFactory, ....)
{
$this->loggerFactory = $loggerFactory;
....
}
....
public function someLoggedMethod()
{
$logger = $this->loggerFactory->provide('simple');
$logger->log( ... logged data .. );
....
}
....
}
This factory will have two additional benefits:
Note:
Actually, when written this way the class Foobar only depends on an instance that implements the Creator interface. Usually you will inject either a builder (if you need to type of instance, probably with some setting) or a factory (if you want to create different instance with same interface).
Next step would be implementation of the 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];
}
}
When you call $factory->provide('thing');
, the factory looks up if the instance has already been created. If the search fails it creates a new instance.
Note: I am actually not entirely sure that this can be called "factory" since the instantiation is really encapsulated in the anonymous functions.
And the last step is actually wiring it all up with providers:
$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 );
Of course to fully understand this approach you will have to know how closures work in PHP, but you will have to learn them anyway.
The core idea of this approach is that instead of injecting the logger, you put an existing instance in a container which acts as membrane between said instance and application. This membrane can then perform different tasks, one of those is logging.
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;
}
}
}
This class can also be used together with the above described lazy factory.
To use this structure, you simply do the following:
$instance = new Foobar;
$instance = new LogBrane( $instance, $logger );
$instance->someMethod();
At this point the container which wraps the instance becomes a fully functional replacement of the original. The rest of your application can handle it as if it is a simple object (pass around, call methods upon). And the wrapped instance itself is not aware that it is being logged.
And if at some point you decide to remove the logging then it can be done without rewriting the rest of your application.
Logger's goal is to save debugging info. Logger has to be class with interface to store message and level of distress. Implementation is secondary. Today you want file logging. Tomorrow you may want put logs to database. So that logic must be written on logger class side. There is already written nice logger called Monolog https://github.com/Seldaek/monolog
If you want a full logging framework, with support for logging to different outputs, log4PHP is an open source solution.
If you want a small implementation that suits your needs now, something like this should do it
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);
Not tested, but should work.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With