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:
- Create a function (eg. some custom functions such as
error()
that callCakeLog::write()
in the way you like.) - Load a class (eg. load your very own logging class called something like..
Log
, so you can callLog::error()
in places) - 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
How to Read Xml File from Url Using PHP
Which Function in PHP Validate If the String Is Valid HTML
How to Detect the Screen Dpi Using JavaScript
Unpacking an Array of Arguments in PHP
PHP Call Class Method/Function
In PHP, How to Call a Function Interpolated in String
How to Determine Whether It's a Mobile Device with PHP
Laravel Polymorphic Relations Has Many Through
Wordpress Woocommerce - Use Update_Post_Meta to Add Product Attributes
Compile Error: "G++: Error Trying to Exec 'Cc1Plus': Execvp: No Such File or Directory"
What's the Best Way to Get the Fractional Part of a Float in PHP
PHP Pdo. Error Number '00000' When Query Is Correct
How to Validate Phone Number Using PHP
But These Conflict with Your Requirements or Minimum-Stability