PHP - Implement Logging Mechanism to File in Several Classes

PHP - Implement logging mechanism to file in several classes

Where are loggers used?

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.

Implementing an invasive logger

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:

  • it can ensure that only one instance is created without a need for global state
  • provide a seam for use when writing unit-tests

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.

Implementing non-invasive logging

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.

Implementation of SplObserver pattern with selective subscription

The splSubject does send itself when sending notifications. It's possible to implement a callback method in the subject so observers can figure out what exactly has changed.

function update(SplSubject $subject) {
$changed = $subject->getChanges();
....
}

you would have to probably create a new interface to force the existence of getChanges() in the subject.

On different kind of notifications, you can take a look at message-queue systems. They allow you to subscribe to different message-boxes ('logging.error', 'logging.warning', or even 'logging'), where they will receive notifications if another system (the subject) sends a message to the corresponding queue. They're not much more difficult to implement as the splObserver/splSubject.

PHP MVC: How to inject helpers, utilities and Request objects?

I personally use Request as dependency of controller's method, that actually needs it.

As for the rest of your code, it seem that you are attempting to shove too much within controller. I would recommend for you to add a "service layer", that handles the application logic, with each controller only depending on those services as requirements in constructor.

To add anything more, you will have to show some code.

How to use a global constant instead of a class constant in PHP version 5.6

You are now doing $logger::LOG_LEVEL, which is taking the 'LOG_LEVEL' out of the class whichever $logger is (in this case a \Monolog\Logger). That doesn't have a static variable named LOG_LEVEL, thus you get the undefined.

You have just have 'LOG_LEVEL' defined, out of any class, so:

 $fileHandler = new \Monolog\Handler\StreamHandler(LOG_FILE, LOG_LEVEL); 

Fancy solution:

You could do a static class and include that in your main page:

Class CONFIG {
public static $LOG_LEVEL = 'default Value';
}

// Then you can use this anywhere:
CONFIG::$LOG_LEVEL
$fileHandler = new \Monolog\Handler\StreamHandler(LOG_FILE, CONFIG::$LOG_LEVEL);

The advantage of this is having only one file for configs, not scattered across all kinds of files, which'll become very annoying very fast.

Import php class from file

include("db/models/Foo.php");

Since your db folder is inside route you can simple access it like that.

You don't need the ./ to move one step before cause your main is already in the root. You need to move inside your nested folders to reach the Foo.php

Since you did not provide the full tree i assumed that your path to Foo.php is:

C:\Users\Desktop\Code\test-project\db\models\Foo.php

Customize logging - CakePHP (1.3)

Global convenience methods

Well, you can't really do monkey patching in PHP (5.2 at least), and that is probably a good thing for the the developers that have to maintain your code after you are gone. :)

CakePHP - being an MVC framework with strict conventions - makes it hard for you to break the MVC paradigm by only allowing you the extend the parts you need in isolation (ie. AppModel, AppController, etc.) and keeping the object-orientated foundation untouched in the core (making it hard to add code that "can be used everywhere" for potential misuse).

As for adding functionality that transcends all the MVC separation, the place for this is app/config/bootstrap.php. When you place code here it seems clear that it is not part of the framework (quite rightly so), but allows you to add these sort of essentials before CakePHP even loads. A few options of what to do here might be:

  1. Create a function (eg. some custom functions such as error() that call CakeLog::write() in the way you like.)
  2. Load a class (eg. load your very own logging class called something like.. Log, so you can call Log::error() in places)
  3. See below:

The logger API

Cake does allow for many customisations to be made to things like the logger, but unfortunately the API exposed to us is already defined in the core in this case. The API for logging in CakePHP is as follows, and you can use either approach anywhere you like (well, the former only in classes):

$this->log($msg, $level) // any class extending `Object` inherits this
// or
CakeLog::write($level, $message); // this is actually what is called by the above

The arbitrary $level parameter that you are trying to eliminate is actually quite a powerful feature:

$this->log('Cannot connect to SMTP server', 'email'); // logs to app/logs/email.log
// vs
$this->email('Cannot connect to SMTP server'); // ambiguous - does this send emails?

We just created a brand new log type without writing an extra line of code and it's quite clear what the intention of our code is.

Customising the logger

The core developers had the foresight to add a condition allowing us to completely replace the logger class should we wish to:

function log($msg, $type = LOG_ERROR) {
if (!class_exists('CakeLog')) { // winning
require LIBS . 'cake_log.php';
}
// ...

As you can see, the core CakeLog class only gets instantiated if no such class exists, giving you the opportunity to insert something of your own creation (or an exact copy with a few tweaks - though you would want to sync changes with core - manually - when upgrading):

// app/config/bootstrap.php
App::import('Lib', 'CakeLog'); // copy cake/libs/cake_log.php to app/lib/cake_log.php

The above would give you full control over the implementation of the CakeLog class in your application, so you could do something like dynamically adding the calling class name to your log messages. However, a more direct way of doing that (and other types of logging - such as to a database) would be to create a custom log stream:

CakeLog::config('file', array(
'engine' => 'FileLog', // copy cake/libs/log/file_log.php to app/libs/log/file_log.php
));

TL;DR - Although you can load your own code before CakePHP bootstraps or for use in isolation in each of the MVC layers provided, you shouldn't tamper with the object hierarchy provided by the core. This makes it hard to add class methods that are inherited globally.

My advice: use the API given to you and concentrate on adding more features instead of syntactical subtleties. :)

Can I extend a class using more than 1 class in PHP?

If you really want to fake multiple inheritance in PHP 5.3, you can use the magic function __call().

This is ugly though it works from class A user's point of view :

class B {
public function method_from_b($s) {
echo $s;
}
}

class C {
public function method_from_c($s) {
echo $s;
}
}

class A extends B
{
private $c;

public function __construct()
{
$this->c = new C;
}

// fake "extends C" using magic function
public function __call($method, $args)
{
$this->c->$method($args[0]);
}
}

$a = new A;
$a->method_from_b("abc");
$a->method_from_c("def");

Prints "abcdef"



Related Topics



Leave a reply



Submit