Matthew Daly's Blog

I'm a web developer in Norfolk. This is my blog...

14th May 2019 12:15 pm

Writing Golden Master Tests for Laravel Applications

Last year I wrote a post illustrating how to write golden master tests for PHP applications in general. This approach works, but has a number of issues:

  • Because it uses a headless browser such as Goutte, it’s inevitably slow (a typical test run for the legacy application I wrote those tests for is 3-4 minutes)
  • It can’t allow for differing content, so any changes to the content will break the tests

These factors limit its utility for many PHP applications. However, for a Laravel application you’re in a much better position:

  • You can use Browserkit rather than a headless browser, resulting in much faster response times
  • You can set up a testing database, and populate it with the same data each time, ensuring that the only thing that can change is how that data is processed to create the required HTML

Here I’ll show you how to adapt that approach to work with a Laravel application.

We rely on Browserkit testing for this approach, so you need to install that:

$ composer require --dev laravel/browser-kit-testing

Next, we need to create our base golden master test case:

<?php
namespace Tests;
use Tests\BrowserTestCase;
class GoldenMasterTestCase extends BrowserTestCase
{
use CreatesApplication;
public $baseUrl = 'http://localhost';
protected $snapshotDir = "tests/snapshots/";
protected $response;
protected $path;
public function goto($path)
{
$this->path = $path;
$this->response = $this->call('GET', $path);
$this->assertNotEquals(404, $this->response->status());
return $this;
}
public function saveHtml()
{
if (!$this->snapshotExists()) {
$this->saveSnapshot();
}
return $this;
}
public function assertSnapshotsMatch()
{
$path = $this->getPath();
$newHtml = $this->processHtml($this->getHtml());
$oldHtml = $this->getOldHtml();
$diff = "";
if (function_exists('xdiff_string_diff')) {
$diff = xdiff_string_diff($oldHtml, $newHtml);
}
$message = "The path $path does not match the snapshot\n$diff";
self::assertThat($newHtml == $oldHtml, self::isTrue(), $message);
}
protected function getHtml()
{
return $this->response->getContent();
}
protected function getPath()
{
return $this->path;
}
protected function getEscapedPath()
{
return $this->snapshotDir.str_replace('/', '_', $this->getPath()).'.snap';
}
protected function snapshotExists()
{
return file_exists($this->getEscapedPath());
}
protected function processHtml($html)
{
return preg_replace('/(<input type="hidden"[^>]+\>|<meta name="csrf-token" content="([a-zA-Z0-9]+)">)/i', '', $html);
}
protected function saveSnapshot()
{
$html = $this->processHtml($this->getHtml());
file_put_contents($this->getEscapedPath(), $html);
}
protected function getOldHtml()
{
return file_get_contents($this->getEscapedPath());
}
}

The goto() method sets the current path on the object, then fetches the page. It verifies the page was found, and then returns an instance of the object, to allow for method chaining.

Another method of note is the saveHtml() method. This checks to see if the snapshot exists - if not, it saves it. The snapshot is essentially just the HTML returned from that route, but certain content may need to be stripped out, which is done in the processHtml() method. In this case we’ve stripped out hidden fields and the CSRF token meta tag, as CSRF tokens are generated anew each time and will break the snapshots.

The last method we’ll look at is the assertSnapshotsMatch() method. This will get the current HTML, and that for any snapshot for that route, and then compare them. If they differ, it will fail the assertion. In addition, if xdiff_string_diff is available, it will show a diff of the two files - be warned, these can sometimes be large, but they can be helpful in debugging.

Also, note our snapshots directory - tests/snapshots. If you do make a breaking change and want to delete a snapshot, then you can find it in there - the format replaces forward slashes with underscores, and appends a file extension of .snap, but feel free to customise this to your needs.

Next, we’ll create a test for routes that don’t require authentication, at tests/GoldenMaster/ExampleTest.php:

<?php
namespace Tests\GoldenMaster;
use Tests\GoldenMasterTestCase;
use Illuminate\Foundation\Testing\RefreshDatabase;
use App\User;
class ExampleTest extends GoldenMasterTestCase
{
use RefreshDatabase;
/**
* @dataProvider nonAuthDataProvider
*/
public function testNonAuthPages($data)
{
$this->goto($data)
->saveHtml()
->assertSnapshotsMatch();
}
public function nonAuthDataProvider()
{
return [
['/register'],
['/login'],
];
}
}

Note the use of the data provider. We want to be able to step through a list of routes, and verify each in turn, so it makes sense to set up a data provider method as nonAuthDataProvider(), which will return an array of routes. If you haven’t used data providers before, they are an easy way to reduce boilerplate in your tests when you need to test the same thing over and over with different data, and you can learn more here.

Now, having seen the methods used, it should be easy to understand testNonAuthPages(). It goes through the following steps:

  • Visit the route passed through, eg /register
  • Save the HTML to a snapshot, if not already saved
  • Assert that the current content matches the snapshot

Using this method, you can test a lot of routes for unexpected changes quite easily. If you’ve used snapshot tests with something like Jest, this is a similar approach.

Authenticated routes

This won’t quite work with authenticated routes, so a few more changes are required. You’ll get a response, but if you look at the HTML it will clearly show the user is being redirected for all of them, so there’s not much point in testing them.

If your content does not differ between users, you can add the trait Illuminate\Foundation\Testing\WithoutMiddleware to your test to disable the authentication and allow the test to get the content without being redirected.

If, however, your content does differ between users, you need to instead create a user object, and use the actingAs() method already available in Laravel tests to set the user, as follows:

<?php
namespace Tests\GoldenMaster;
use Tests\GoldenMasterTestCase;
use Illuminate\Foundation\Testing\RefreshDatabase;
use App\User;
class ExampleTest extends GoldenMasterTestCase
{
use RefreshDatabase;
/**
* @dataProvider authDataProvider
*/
public function testAuthPages($data)
{
$user = factory(User::class)->create([
'email' => 'eric@example.com',
'name' => 'Eric Smith',
'password' => 'password'
]);
$this->actingAs($user)
->goto($data)
->saveHtml()
->assertSnapshotsMatch();
}
public function authDataProvider()
{
return [
['/'],
];
}
}

This will allow us to visit a specific page as a user, without being redirected.

Summary

This can be a useful technique to catch unexpected breakages in applications, particularly ones which have little or no conventional test coverage. While I originated this technique on a Zend 1 legacy code base, leveraging the tools available in Laravel makes this technique much faster and more useful. If your existing Laravel application is not as well tested as you’d like, and you have some substantial changes to make that risk breaking some of the functionality, having these sorts of golden master tests set up can be a quick and easy way of catching any problems as soon as possible.

4th March 2019 9:26 pm

How Much Difference Does Adding An Index to a Database Table Make?

For the last few weeks, I’ve been kept busy at work building out a new homepage for the legacy intranet system I maintain. The new homepage is built virtually from scratch with React, and has a completely new set of queries. In addition, I’ve also rebuilt the UI for the navigation to use React too. This has allowed me to bypass a lot of the worst code in the whole code base with the intent to get rid of it once the new home page is live - something I’m very pleased about!

As part of this, I built some new functionality to show items added in the last seven days. This section of the home page can be sorted by several parameters, including popularity. I also added the facility to expand that to 31 days via an AJAX request. However, the AJAX request was painfully slow, often taking 20-30 seconds. Also, the home page was quite slow to load in the first place, and examining the query time in Clockwork indicated that the culprit was the query for the new items.

Further examination of the query behind the new items (both on initial page load and the 31 day AJAX request) indicated that the problem was a join. Last year, one of my first tasks had been to add the facility to record a track for any media item when it was visited. This was accomplished using a polymorphic relationship. While Zend 1 doesn’t have the kind of out-of-the-box support for polymorphic relationships that Laravel has, it’s possible to fake it so I created a tracks table whose columns included trackable_id for the primary key of the tracked object, trackable_type for its class, and user_id for the ID of the user who visited it. Now, I was using that same table to determine the number of times each item had been viewed by joining it on each of the media items, which was the first time it was being read for anything other than a report generated in the admin, and performance was dog slow.

Once I’d established that removing that join from the query removed the performance issue, then it became apparent I was going to need to add an index to the tracks table. The table had got fairly large (low hundreds of thousands), so it had a lot to sort through. As the join used the trackable_id field to join onto the items added, that seemed like a good candidate, so I added the index there.

The results were dramatic, to put it mildly. The initial page load time dropped from 4.44s to 1.29s - around a third of the previous amount. For the AJAX request to fetch the last 31 day’s new items, the results were even more impressive - the loading time dropped from 22.44s to 1.61s. Overall, figuring out which part of the query was causing the poor performance and resolving it took about ten minutes, and resulted in a staggering improvement.

If you don’t have a particularly strong theoretical background with relational databases, knowledge of indices can fall by the wayside somewhat. However, as you can see from this example, if you have a particularly slow query, then adding an index can make a staggering difference, so it’s really worth taking the time to understand a bit more about indices and when they can be useful.

20th February 2019 5:25 pm

Searching Content With Fuse.js

Search is a problem I’m currently taking a big interest in. The legacy project I maintain has an utterly abominable search facility, one that I’m eager to replace with something like Elasticsearch. But smaller sites that are too small for Elasticsearch to be worth the bother can still benefit from having a decent search implementation. Despite some recent improvements, relational databases aren’t generally that good a fit for search because they don’t really understand the concept of relevance - you can’t easily order something by how good a match it is, and your database may not deal with fuzzy matching well.

I’m currently working on a small flat-file CMS as a personal project. It’s built with PHP, but it’s intended to be as simple as possible, with no database, no caching service, and certainly no search service, so it needs something small and simple, but still effective for search.

In the past I’ve used Lunr.js on my own site, and it works very well for this use case. However, it’s problematic for this case as the index needs to be generated in Javascript on the server side, and adding Node.js to the stack for a flat-file PHP CMS is not really an option. What I needed was something where I could generate the index in any language I chose, load it via AJAX, and search it on the client side. I recently happened to stumble across Fuse.js, which was pretty much exactly what I was after.

Suppose we have the following index:

[
{
"title":"About me",
"path":"about/"
},
{
"title":"Meet the team",
"path":"about/meet-the-team/"
},
{
"title":"Alice",
"path":"about/meet-the-team/alice/"
},
{
"title":"Bob",
"path":"about/meet-the-team/bob/"
},
{
"title":"Chris",
"path":"about/meet-the-team/chris/"
},
{
"title":"Home",
"path":"index/"
}
]

This index can be generated in any way you see fit. In this case, the page content is stored in Markdown files with YAML front matter, so I wrote a Symfony console command which gets all the Markdown files in the content folder, parses them to get the titles, and retrieves the path. You could also retrieve other items in front matter such as categories or tags, and the page content, and include that in the index. The data then gets converted to JSON and saved to the index file. As you can see, there’s nothing special about this JSON - these two fields happen to be the ones I’ve chosen.

Now we can load the JSON file via AJAX, and pass it to a new Fuse instance. You can search the index using the .search() method, as shown below:

import Fuse from 'fuse.js';
window.$ = window.jQuery = require('jquery');
$(document).ready(function () {
window.$.getJSON('/storage/index.json', function (response) {
const fuse = new Fuse(response, {
keys: ['title'],
shouldSort: true
});
$('#search').on('keyup', function () {
let result = fuse.search($(this).val());
// Output it
let resultdiv = $('ul.searchresults');
if (result.length === 0) {
// Hide results
resultdiv.hide();
} else {
// Show results
resultdiv.empty();
for (let item in result.slice(0,4)) {
let searchitem = '<li><a href="/' + result[item].path + '">' + result[item].title + '</a></li>';
resultdiv.append(searchitem);
}
resultdiv.show();
}
});
});
});

The really great thing about Fuse.js is that it can search just about any JSON content, making it extremely flexible. For a site with a MySQL database, you could generate the JSON from one or more tables in the database, cache it in Redis or Memcached indefinitely until such time as the content changes again, and only regenerate it then, making for an extremely efficient client-side search that doesn’t need to hit the database during normal operation. Or you could generate it from static files, as in this example. It also means the backend language is not an issue, since you can easily generate the JSON file in PHP, Javascript, Python or any other language.

As you can see, it’s pretty straightforward to use Fuse.js to create a working search field out of the box, but the website lists a number of options allowing you to customise the search for your particular use case, and I’d recommend looking through these if you’re planning on using it on a project.

16th February 2019 7:00 pm

Higher-order Components in React

In the last few weeks I’ve been working on a big rebuild of the homepage of the legacy application I maintain. As I’ve been slowly transitioning it to use React on the front end, I used that, and it’s by far the largest React project I’ve worked on to date. This has pushed me to use some more advanced React techniques I hadn’t touched on before. I’ve also had to create some different components that have common functionality.

React used to use mixins to share common functionality, but the consensus is now that mixins are considered harmful, so they have been removed. Instead, developers are encouraged to create higher-order components to contain the shared functionality.

A higher-order component is a function that accepts a React component as an argument, and then returns another component that wraps the provided one. The shared functionality is defined inside the wrapping component, and so any state or methods defined in the wrapping component can then be passed as props into the wrapped one, as in this simple example:

import React, { Component } from 'react';
export default function hocExample(WrappedComponent) {
class hocExample extends Component {
constructor(props) {
this.state = {
foo: false
};
this.doStuff = this.doStuff.bind(this);
}
doStuff() {
this.setState({
foo: true
});
}
render() {
return (
<WrappedComponent foo={this.state.foo} doStuff={this.doStuff} />
);
}
}
return hocExample;
}

If you’ve been working with React for a while, even if you haven’t written a higher-order component, you’ve probably used one. For instance, withRouter() from react-router is a good example of a higher-order component that forms part of an existing library.

A real-world example

A very common use case I’ve come across is handling a click outside of a component. For instance, if you have a sidebar or popup component, it’s common to want to close it when the user clicks outside the component. As such, it’s worth taking the time to refactor it to make it reusable.

In principle you can achieve this on any component as follows:

  • The component should accept two props - an active prop that denotes whether the component is active or not, and an onClickOutside() prop method that is called on a click outside
  • On mount, an event listener should be added to the document to listen for mousedown events, and it should be removed on unmount
  • When the event listener is fired, it should use a ref on the component to determine if the ref contains the event target. If so, and the status is active, the onClickOutside() method should be called

Moving this to a higher order component makes a couple of issues slightly more complex, but not very. We can’t easily get a ref of the wrapped component, so I had to resort to using ReactDOM.findDOMNode() instead, which is potentially a bit dodgy as they’re talking about deprecating that.

import React, { Component } from 'react';
import { findDOMNode } from 'react-dom';
export default function clicksOutside(WrappedComponent) {
class clicksOutside extends Component {
constructor(props) {
super(props);
this.setWrapperRef = this.setWrapperRef.bind(this);
this.handleClickOutside = this.handleClickOutside.bind(this);
}
componentDidMount() {
document.addEventListener('mousedown', this.handleClickOutside);
}
componentWillUnmount() {
document.removeEventListener('mousedown', this.handleClickOutside);
}
setWrapperRef(node) {
this.wrapperRef = node;
}
handleClickOutside(event) {
const {target} = event;
if (this.wrapperRef && target instanceof Node) {
const ref = findDOMNode(this.wrapperRef);
if (ref && !ref.contains(target) && this.props.active === true) {
this.props.onClickOutside();
}
}
}
render() {
return (
<WrappedComponent {...this.props} ref={this.setWrapperRef} />
);
}
};
return clicksOutside;
}

Now we can use this as follows:

import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import Sidebar from './src/Components/Sidebar';
import clicksOutside from './src/Components/clicksOutside';
const SidebarComponent = clicksOutside(Sidebar);
function handleClickOutside() {
alert('You have clicked outside');
}
ReactDOM.render(
<SidebarComponent
links={links}
active={true}
onClickOutside={handleClickOutside}
/>,
document.getElementById('root')
);

Higher order components sound a lot harder than they actually are. In reality, they’re actually quite simple to implement, but I’m not sure the documentation is necessarily the best example to use since it’s a bit on the complex side.

2nd February 2019 8:45 pm

Creating Your Own Dependency Injection Container in PHP

Dependency injection can be a difficult concept to understand in the early stages. Even when you’re using it all the time, it can often seem like magic. However, it’s really not all that complicated once you actually get into the nuts and bolts of it, and building your own container is a good way to learn more about how it works and how to use it.

In this tutorial, I’ll walk you through creating a simple, minimal dependency injection container, using PHPSpec as part of a TDD workflow. While the end result isn’t necessarily something I’d be happy using in a production environment, it’s sufficient to understand the basic concept and make it feel less like a black box. Our container will be called Ernie (if you want to know why, it’s a reference to a 90’s era video game that had a character based on Eric Cantona called Ernie Container).

The first thing we need to do is set up our dependencies. Our container will implement PSR-11, so we need to include the interface that defines that. We’ll also use PHP CodeSniffer to ensure code quality, and PHPSpec for testing. Your composer.json should look something like this:

{
"name": "matthewbdaly/ernie",
"description": "Simple DI container",
"type": "library",
"require-dev": {
"squizlabs/php_codesniffer": "^3.3",
"phpspec/phpspec": "^5.0",
"psr/container": "^1.0"
},
"license": "MIT",
"authors": [
{
"name": "Matthew Daly",
"email": "450801+matthewbdaly@users.noreply.github.com"
}
],
"require": {},
"autoload": {
"psr-4": {
"Matthewbdaly\\Ernie\\": "src/"
}
}
}

We also need to put this in our phpspec.yml file:

suites:
test_suite:
namespace: Matthewbdaly\Ernie
psr4_prefix: Matthewbdaly\Ernie

With that done, we can start working on our implementation.

Creating the exceptions

The PSR-11 specification defines two interfaces for exceptions, which we will implement before actually moving on to the container itself. The first of these is Psr\Container\ContainerExceptionInterface. Run the following command to create a basic spec for the exception:

$ vendor/bin/phpspec desc Matthewbdaly/Ernie/Exceptions/ContainerException

The generated specification for it at spec/Exceptions/ContainerExceptionSpec.php will look something like this:

<?php
namespace spec\Matthewbdaly\Ernie;
use Matthewbdaly\Ernie\ContainerException;
use PhpSpec\ObjectBehavior;
use Prophecy\Argument;
class ContainerExceptionSpec extends ObjectBehavior
{
function it_is_initializable()
{
$this->shouldHaveType(ContainerException::class);
}
}

This is not sufficient for our needs. Our exception must also implement two interfaces:

  • Throwable
  • Psr\Container\ContainerExceptionInterface

The former can be resolved by inheriting from Exception, while the latter doesn’t require any additional methods. Let’s expand our spec to check for these:

<?php
namespace spec\Matthewbdaly\Ernie\Exceptions;
use Matthewbdaly\Ernie\Exceptions\ContainerException;
use PhpSpec\ObjectBehavior;
use Prophecy\Argument;
class ContainerExceptionSpec extends ObjectBehavior
{
function it_is_initializable()
{
$this->shouldHaveType(ContainerException::class);
}
function it_implements_interface()
{
$this->shouldImplement('Psr\Container\ContainerExceptionInterface');
}
function it_implements_throwable()
{
$this->shouldImplement('Throwable');
}
}

Now run the spec and PHPSpec will generate the boilerplate exception for you:

$ vendor/bin/phpspec run
Matthewbdaly/Ernie/Exceptions/ContainerException
11 - it is initializable
class Matthewbdaly\Ernie\Exceptions\ContainerException does not exist.
Matthewbdaly/Ernie/Exceptions/ContainerException
16 - it implements interface
class Matthewbdaly\Ernie\Exceptions\ContainerException does not exist.
Matthewbdaly/Ernie/Exceptions/ContainerException
21 - it implements throwable
class Matthewbdaly\Ernie\Exceptions\ContainerException does not exist.
100% 3
1 specs
3 examples (3 broken)
23ms
Do you want me to create `Matthewbdaly\Ernie\Exceptions\ContainerException`
for you?
[Y/n]
y
Class Matthewbdaly\Ernie\Exceptions\ContainerException created in /home/matthew/Projects/ernie-clone/src/Exceptions/ContainerException.php.
Matthewbdaly/Ernie/Exceptions/ContainerException
16 - it implements interface
expected an instance of Psr\Container\ContainerExceptionInterface, but got
[obj:Matthewbdaly\Ernie\Exceptions\ContainerException].
Matthewbdaly/Ernie/Exceptions/ContainerException
21 - it implements throwable
expected an instance of Throwable, but got
[obj:Matthewbdaly\Ernie\Exceptions\ContainerException].
33% 66% 3
1 specs
3 examples (1 passed, 2 failed)
36ms

It’s failing, but we expect that. We need to update our exception to extend the base PHP exception, and implement Psr\Container\ContainerExceptionInterface. Let’s do that now:

<?php
namespace Matthewbdaly\Ernie\Exceptions;
use Psr\Container\ContainerExceptionInterface;
use Exception;
class ContainerException extends Exception implements ContainerExceptionInterface
{
}

Let’s re-run the spec:

$ vendor/bin/phpspec run
100% 3
1 specs
3 examples (3 passed)
24ms

The second exception we need to implement is Psr\Container\NotFoundExceptionInterface and it’s a similar story. Run the following command to create the spec:

$ vendor/bin/phpspec desc Matthewbdaly/Ernie/Exceptions/NotFoundException

Again, the spec needs to be amended to verify that it’s a throwable and implements the required interface:

<?php
namespace spec\Matthewbdaly\Ernie\Exceptions;
use Matthewbdaly\Ernie\Exceptions\NotFoundException;
use PhpSpec\ObjectBehavior;
use Prophecy\Argument;
class NotFoundExceptionSpec extends ObjectBehavior
{
function it_is_initializable()
{
$this->shouldHaveType(NotFoundException::class);
}
function it_implements_interface()
{
$this->shouldImplement('Psr\Container\NotFoundExceptionInterface');
}
function it_implements_throwable()
{
$this->shouldImplement('Throwable');
}
}

For the sake of brevity I’ve left out the output, but if you run vendor/bin/phpspec run you’ll see it fail due to the fact that the generated class doesn’t implement the required interfaces. Amend src/Exceptions/NotFoundException as follows:

<?php
namespace Matthewbdaly\Ernie\Exceptions;
use Psr\Container\NotFoundExceptionInterface;
use Exception;
class NotFoundException extends Exception implements NotFoundExceptionInterface
{
}

Running vendor/bin/phpspec run should now see it pass. Now let’s move on to the container class…

Building the container

Run the following command to create the container spec:

$ vendor/bin/phpspec desc Matthewbdaly/Ernie/Container

However, the default generated spec isn’t sufficient. We need to check it implements the required interface:

<?php
namespace spec\Matthewbdaly\Ernie;
use Matthewbdaly\Ernie\Container;
use PhpSpec\ObjectBehavior;
use Prophecy\Argument;
class ContainerSpec extends ObjectBehavior
{
function it_is_initializable()
{
$this->shouldHaveType(Container::class);
}
function it_implements_interface()
{
$this->shouldImplement('Psr\Container\ContainerInterface');
}
}

Now, if we run PHPSpec, we’ll generate our class:

$ vendor/bin/phpspec run
Matthewbdaly/Ernie/Container
11 - it is initializable
class Matthewbdaly\Ernie\Container does not exist.
Matthewbdaly/Ernie/Container
16 - it implements interface
class Matthewbdaly\Ernie\Container does not exist.
75% 25% 8
3 specs
8 examples (6 passed, 2 broken)
404ms
Do you want me to create `Matthewbdaly\Ernie\Container` for you?
[Y/n]
y
Class Matthewbdaly\Ernie\Container created in /home/matthew/Projects/ernie-clone/src/Container.php.
Matthewbdaly/Ernie/Container
16 - it implements interface
expected an instance of Psr\Container\ContainerInterface, but got
[obj:Matthewbdaly\Ernie\Container].
87% 12% 8
3 specs
8 examples (7 passed, 1 failed)
40ms

Now, as we can see, this class doesn’t implement the interface. Let’s remedy that:

<?php
namespace Matthewbdaly\Ernie;
use Psr\Container\ContainerInterface;
class Container implements ContainerInterface
{
}

Now, if we run the tests, they should fail because the class needs to add the required methods:

$ vendor/bin/phpspec run
✘ Fatal error happened while executing the following
it is initializable
Class Matthewbdaly\Ernie\Container contains 2 abstract methods and must therefore be declared abstract or implement the remaining methods (Psr\Container\ContainerInterface::get, Psr\Container\ContainerInterface::has) in /home/matthew/Projects/ernie-clone/src/Container.php on line 7

If you use an editor or IDE that allows you to implement an interface automatically, you can run it to add the required methods. I use PHPActor with Neovim, and used the option in the Transform menu to implement the contract:

<?php
namespace Matthewbdaly\Ernie;
use Psr\Container\ContainerInterface;
class Container implements ContainerInterface
{
/**
* {@inheritDoc}
*/
public function get($id)
{
}
/**
* {@inheritDoc}
*/
public function has($id)
{
}
}

Running vendor/bin/phpspec run should now make the spec pass, but the methods don’t actually do anything yet. If you read the spec for PSR-11, you’ll see that has() returns a boolean to indicate whether a class can be instantiated or not, while get() will either return an instance of the specified class, or throw an exception. We will add specs that check that built-in classes can be returned by both, and unknown classes display the expected behaviour. We’ll do both at once, because in both cases, the functionality to actually resolve the required class will be deferred to a single resolver method, and these methods will not do all that much as a result:

function it_has_simple_classes()
{
$this->has('DateTime')->shouldReturn(true);
}
function it_does_not_have_unknown_classes()
{
$this->has('UnknownClass')->shouldReturn(false);
}
function it_can_get_simple_classes()
{
$this->get('DateTime')->shouldReturnAnInstanceOf('DateTime');
}
function it_returns_not_found_exception_if_class_cannot_be_found()
{
$this->shouldThrow('Matthewbdaly\Ernie\Exceptions\NotFoundException')
->duringGet('UnknownClass');
}

These tests verify that:

  • has() returns true when called with the always-present DateTime class
  • has() returns false for the undefined UnknownClass
  • get() successfully instantiates an instance of DateTime
  • get() throws an exception if you try to instantiate the undefined UnknownClass

Running the specs will raise errors:

$ vendor/bin/phpspec run
Matthewbdaly/Ernie/Container
21 - it has simple classes
expected true, but got null.
Matthewbdaly/Ernie/Container
26 - it does not have unknown classes
expected false, but got null.
Matthewbdaly/Ernie/Container
31 - it can get simple classes
expected an instance of DateTime, but got null.
Matthewbdaly/Ernie/Container
36 - it returns not found exception if class cannot be found
expected to get exception / throwable, none got.
66% 33% 12
3 specs
12 examples (8 passed, 4 failed)
98ms

Let’s populate these empty methods:

<?php
namespace Matthewbdaly\Ernie;
use Psr\Container\ContainerInterface;
use Matthewbdaly\Ernie\Exceptions\NotFoundException;
use ReflectionClass;
use ReflectionException;
class Container implements ContainerInterface
{
/**
* {@inheritDoc}
*/
public function get($id)
{
$item = $this->resolve($id);
return $this->getInstance($item);
}
/**
* {@inheritDoc}
*/
public function has($id)
{
try {
$item = $this->resolve($id);
} catch (NotFoundException $e) {
return false;
}
return $item->isInstantiable();
}
private function resolve($id)
{
try {
return (new ReflectionClass($id));
} catch (ReflectionException $e) {
throw new NotFoundException($e->getMessage(), $e->getCode(), $e);
}
}
private function getInstance(ReflectionClass $item)
{
return $item->newInstance();
}
}

As you can see, both the has() and get() methods need to resolve a string ID to an actual class, so that common functionality is stored in a private method called resolve(). This uses the PHP Reflection API to resolve the class name to an actual class. We pass the string ID into a constructor of ReflectionClass, and the resolve() method will either return the created instance of ReflectionClass, or throw an exception.

For the uninitiated, ReflectionClass allows you to reflect on the object whose fully qualified class name is passed to the constructor, in order to interact with that class programmatically. The methods we will use include:

  • isInstantiable - confirms whether or not the class can be instantiated (for instance, traits and abstract classes can’t)
  • newInstance - creates a new instance of the item in question, as long as it has no dependencies in the constructor
  • newInstanceArgs - creates a new instance, using the arguments passed in
  • getConstructor - allows you to get information about the constructor

The Reflection API is pretty comprehensive, and I would recommend reading the documentation linked to above if you want to know more.

For the has() method, we check that the resolved class is instantiable, and return the result of that. For the get() method, we use getInstance() to instantiate the item and return that, throwing an exception if that fails.

Registering objects

In its current state, the container doesn’t allow you to set an item. To be useful, we need to be able to specify that an interface or string should be resolved to a given class, or for cases where we need to pass in scalar parameters, such as a database object, to specify how a concrete instance of that class should be instantiated. To that end, we’ll create a new set() public method that will allow a dependency to be set. Here are the revised specs including this:

<?php
namespace spec\Matthewbdaly\Ernie;
use Matthewbdaly\Ernie\Container;
use PhpSpec\ObjectBehavior;
use Prophecy\Argument;
use DateTime;
class ContainerSpec extends ObjectBehavior
{
function it_is_initializable()
{
$this->shouldHaveType(Container::class);
}
function it_implements_interface()
{
$this->shouldImplement('Psr\Container\ContainerInterface');
}
function it_has_simple_classes()
{
$this->has('DateTime')->shouldReturn(true);
}
function it_does_not_have_unknown_classes()
{
$this->has('UnknownClass')->shouldReturn(false);
}
function it_can_get_simple_classes()
{
$this->get('DateTime')->shouldReturnAnInstanceOf('DateTime');
}
function it_returns_not_found_exception_if_class_cannot_be_found()
{
$this->shouldThrow('Matthewbdaly\Ernie\Exceptions\NotFoundException')
->duringGet('UnknownClass');
}
function it_can_register_dependencies()
{
$toResolve = new class {
};
$this->set('Foo\Bar', $toResolve)->shouldReturn($this);
}
function it_can_resolve_registered_dependencies()
{
$toResolve = new class {
};
$this->set('Foo\Bar', $toResolve);
$this->get('Foo\Bar')->shouldReturnAnInstanceOf($toResolve);
}
function it_can_resolve_registered_invokable()
{
$toResolve = new class {
public function __invoke() {
return new DateTime;
}
};
$this->set('Foo\Bar', $toResolve);
$this->get('Foo\Bar')->shouldReturnAnInstanceOf('DateTime');
}
function it_can_resolve_registered_callable()
{
$toResolve = function () {
return new DateTime;
};
$this->set('Foo\Bar', $toResolve);
$this->get('Foo\Bar')->shouldReturnAnInstanceOf('DateTime');
}
function it_can_resolve_if_registered_dependencies_instantiable()
{
$toResolve = new class {
};
$this->set('Foo\Bar', $toResolve);
$this->has('Foo\Bar')->shouldReturn(true);
}
}

This needs to handle quite a few scenarios, so there are several tests we have in place. These verify that:

  • The set() method returns an instance of the container class, to allow for method chaining
  • When a dependency is set, calling get() returns an instance of that class
  • When a concrete class that has the __invoke() magic method set is passed to set(), it is invoked and the response returned.
  • When the value passed through is a callback, the callback is resolved and the response returned
  • When a dependency is set, calling has() for it returns the right value

Note that we use anonymous classes for testing - I’ve written about these before and they’re very useful in this context because they allow us to create a simple class inline for testing purposes.

Running the specs should result in us being prompted to generate the set() method, and failing afterwards:

$ vendor/bin/phpspec run
Matthewbdaly/Ernie/Container
42 - it can register dependencies
method Matthewbdaly\Ernie\Container::set not found.
Matthewbdaly/Ernie/Container
49 - it can resolve registered dependencies
method Matthewbdaly\Ernie\Container::set not found.
Matthewbdaly/Ernie/Container
57 - it can resolve registered invokable
method Matthewbdaly\Ernie\Container::set not found.
Matthewbdaly/Ernie/Container
68 - it can resolve registered callable
method Matthewbdaly\Ernie\Container::set not found.
Matthewbdaly/Ernie/Container
77 - it can resolve if registered dependencies instantiable
method Matthewbdaly\Ernie\Container::set not found.
70% 29% 17
3 specs
17 examples (12 passed, 5 broken)
316ms
Do you want me to create `Matthewbdaly\Ernie\Container::set()` for you?
[Y/n]
y
Method Matthewbdaly\Ernie\Container::set() has been created.
Matthewbdaly/Ernie/Container
42 - it can register dependencies
expected [obj:Matthewbdaly\Ernie\Container], but got null.
Matthewbdaly/Ernie/Container
49 - it can resolve registered dependencies
exception [exc:Matthewbdaly\Ernie\Exceptions\NotFoundException("Class Foo\Bar does not exist")] has been thrown.
Matthewbdaly/Ernie/Container
57 - it can resolve registered invokable
exception [exc:Matthewbdaly\Ernie\Exceptions\NotFoundException("Class Foo\Bar does not exist")] has been thrown.
Matthewbdaly/Ernie/Container
68 - it can resolve registered callable
exception [exc:Matthewbdaly\Ernie\Exceptions\NotFoundException("Class Foo\Bar does not exist")] has been thrown.
Matthewbdaly/Ernie/Container
77 - it can resolve if registered dependencies instantiable
expected true, but got false.
70% 11% 17% 17
3 specs
17 examples (12 passed, 2 failed, 3 broken)
90ms

First, we need to set up the set() method properly, and define a property to contain the stored services:

private $services = [];
public function set(string $key, $value)
{
$this->services[$key] = $value;
return $this;
}

This fixes the first spec, but the resolver needs to be amended to handle cases where the ID is set manually:

private function resolve($id)
{
try {
$name = $id;
if (isset($this->services[$id])) {
$name = $this->services[$id];
if (is_callable($name)) {
return $name();
}
}
return (new ReflectionClass($name));
} catch (ReflectionException $e) {
throw new NotFoundException($e->getMessage(), $e->getCode(), $e);
}
}

This will allow us to resolve classes set with set(). However, we also want to resolve any callables, such as callbacks or classes that implement the __invoke() magic method, which means that sometimes resolve() will return the result of the callable instead of an instance of ReflectionClass. Under those circumstances we should return the item directly:

public function get($id)
{
$item = $this->resolve($id);
if (!($item instanceof ReflectionClass)) {
return $item;
}
return $this->getInstance($item);
}

Note that because the __invoke() method is automatically called in any concrete class specified in the second argument to set(), it’s only possible to resolve classes that define an __invoke() method if they are passed in as string representations. The following PsySh session should make it clear what this means:

>>> use Matthewbdaly\Ernie\Container;
>>> $c = new Container;
=> Matthewbdaly\Ernie\Container {#2307}
>>> class TestClass { public function __invoke() { return "Called"; }}
>>> $c->get('TestClass');
=> TestClass {#2319}
>>> $c->set('Foo\Bar', 'TestClass');
=> Matthewbdaly\Ernie\Container {#2307}
>>> $c->get('Foo\Bar');
=> TestClass {#2309}
>>> $c->set('Foo\Bar', new TestClass);
=> Matthewbdaly\Ernie\Container {#2307}
>>> $c->get('Foo\Bar');
=> "Called"

As you can see, if we pass in the fully qualified class name of a class that defines an __invoke() method, it can be resolved as expected. However, if we pass a concrete instance of it to set(), it will be called and will return the response from that. This may not be the behaviour you want for your own container.

According to this issue on the PHP League’s Container implementation, it was also an issue for them, so seeing as this is just a toy example I’m not going to lose any sleep over it. Just something to be aware of if you use this post as the basis for writing your own container.

Resolving dependencies

One thing is missing from our container. Right now it should be able to instantiate pretty much any class that has no dependencies, but these are quite firmly in the minority. To be useful, a container should be able to resolve all of the dependencies for a class automatically.

Let’s add a spec for that:

function it_can_resolve_dependencies()
{
$toResolve = get_class(new class(new DateTime) {
public $datetime;
public function __construct(DateTime $datetime)
{
$this->datetime = $datetime;
}
});
$this->set('Foo\Bar', $toResolve);
$this->get('Foo\Bar')->shouldReturnAnInstanceOf($toResolve);
}

Here we have to be a bit crafty. Anonymous classes are defined and instantiated at the same time, so we can’t pass it in as an anonymous class in the test. Instead, we call the anonymous class and get its name, then set that as the second argument to set(). Then we can verify that the returned object is an instance of the same class.

Running this throws an error:

$ vendor/bin/phpspec run
Matthewbdaly/Ernie/Container
86 - it can resolve dependencies
exception [err:ArgumentCountError("Too few arguments to function class@anonymous::__construct(), 0 passed and exactly 1 expected")] has been thrown.
94% 18
3 specs
18 examples (17 passed, 1 broken)
60ms

This is expected. Our test class accepts an instance of DateTime in the constructor as a mandatory dependency, so instantiating it fails. We need to update the getInstance() method so that it can handle pulling in any dependencies:

private function getInstance(ReflectionClass $item)
{
$constructor = $item->getConstructor();
if (is_null($constructor) || $constructor->getNumberOfRequiredParameters() == 0) {
return $item->newInstance();
}
$params = [];
foreach ($constructor->getParameters() as $param) {
if ($type = $param->getType()) {
$params[] = $this->get($type->getName());
}
}
return $item->newInstanceArgs($params);
}

Here, we use the Reflection API to get the constructor. If there’s no constructor, or it has no required parameters, we just return a new instance of the reflected class as before.

Otherwise, we loop through the required parameters. For each parameter, we get the string representation of the type specified for that parameter, and retrieve an instance of it from the container. Afterwards, we use those parameters to instantiate the object.

Let’s run the specs again:

$ vendor/bin/phpspec run
100% 18
3 specs
18 examples (18 passed)
51ms

Our container is now complete. We can:

  • Resolve simple classes out of the box
  • Set arbitrary keys to resolve to particular classes, or the result of callables, so as to enable mapping interfaces to concrete implementations, or resolve classes that require specific non-object parameters, such as PDO
  • Resolve complex classes with multiple dependencies

Not too bad for just over 100 lines of PHP…

Final thoughts

As I’ve said, this is a pretty minimal example of a dependency injection container, and I wouldn’t advise using this in production when there are so many existing, mature solutions available. I have no idea how the performance would stack up against existing solutions, or whether there are any issues with it, and quite frankly that’s besides the point - this is intended as a learning exercise to understand how dependency injection containers in general work, not as an actual useful piece of code for production. If you want an off-the-shelf container, I’d point you in the direction of league/container, which has served me well.

You can find the code for this tutorial on GitHub, so if you have any problems, you should take a look there to see where the problem lies. Of course, if you go on to create your own kick-ass container based on this, do let me know!

Recent Posts

Writing Golden Master Tests for Laravel Applications

How Much Difference Does Adding An Index to a Database Table Make?

Searching Content With Fuse.js

Higher-order Components in React

Creating Your Own Dependency Injection Container in PHP

About me

I'm a web and mobile app developer based in Norfolk. My skillset includes Python, PHP and Javascript, and I have extensive experience working with CodeIgniter, Laravel, Zend Framework, Django, Phonegap and React.js.