It’s been almost a year since I first mentioned Monolog, one of the leading PHP loggers. During that year my use of Monolog has developed, along with my coding style. Recently I’ve hit upon a new system allowing me to clean up my code, but still hopefully sticking to best practices.
As Monolog is such a core component to how I go about coding now, I figured I’d devote some more time and effort to sharing what I’ve picked up. Starting with:
Getting the logger to where we want it
Generally speaking, you have two options for getting an object into your code
You create the objects where you need them
Pros
- Easy to create what you need, where you need it.
- No wasted resources having objects where you don’t need them.
Cons
- Makes unit testing virtually impossible.
- If an object changes, that’s a site-wide update.
You use Dependency Injection and pass in the objects you need
Pros
- Unit testing is a breeze
Cons
- Working out the chain to see who actually creates the object.
As I’m now a disciple of the Church of Grumpy (testing is the one true way), I decided DI was the way to go. And in the beginning, it worked perfectly.
We chose to use Laravel for our front-end framework, with the majority of our processing code framework-agnostic. This meant I had access to the ease of site-wide configuration and security, whilst leaving the door open if we wanted to switch frameworks in the future. I used the built-in Laravel “Inversion of Control container” to inject the logger into my controllers, instantly making it available at the higher levels of the code.
class AdminController extends BaseController { protected $logger; public function __construct(\Core\Logger $logger) { $this->logger = $logger; $this->logger->applicationLog('+ SearchController', $logger::DEBUG); parent::__construct(); }
Nothing is passed in to the constructor; the IoC container kicks in and magically creates the logger for me. Then, when I want to shift into the lower levels of the code, I can simply call
new \Search\Criteria($this->logger);
The logger is passed in, it can be mocked in a unit test, all is good. Or, rather, it was in the early days.
Before long, classes were having to set the logger, not because they needed it, but because something below it needed it. Without realising it, I’d tied my codebase into a long line, I couldn’t jump from one part to another. In effect, I’d made tightly-coupled code…
Then one quiet evening, whilst reading up on some design patterns, I found the following snippet:
function __construct(Persistence $persistence = null) { $this->persistence = $persistence ? : new InMemoryPersistence(); }
At first I didn’t understand the syntax, because it uses a shorthand feature. If the first result of a ternary is blank, it sets it to the result of the evaluation. In others words, “If $persistence exists, set it to object. If not, create a new object and set that instead.”.
So… if I want to inject an object, I can. If not, it will self-configure to a default state. And just like that, it all fell into place.
Code should always have a default running state, but modification at run-time is allowed for any reason (the most common being testing, but at any point you want your code to be flexible).
A quick change to the logger class to make it a singleton (straight forward, but I can go into more depth in the future if you want), and I now have:
new \Search\Criteria(); class Criteria { public function __construct(\Core\Logger $logger = null) { $this->logger = $logger : ? \Core\Logger::getInstance(); }
If the Criteria object needs access to the logger, it can spin it up. If not, I remove the line. If I want to inject a mock logger, I can.
A simple idea, but it’s allowed me to stop passing the logger through multiple objects, whilst retaining the power of dependency injection. The only downside is if we want to change Logger to another default class. That that, will be addressed in the next post.