Skip to content
nyeholt edited this page Oct 18, 2011 · 2 revisions

How DI could be used in an existing framework - a SilverStripe example

This was written as a response to a question on the mailing list about how / why DI would be introduced in SilverStripe. As it explains some core meanings behind DI, I've reproduced it here

So, where you might currently write code like

class Page extends DataObject {

	public function Items() {
		return DataObject::get('Page', '"RelationID" = ' . $this->ID);
	}
}

you might instead write

class Page extends DataObject {

	public function Items() {
		return $this->dataSource->get('Page', '"RelationID" = ' . $this->ID);
	}
}

The fact that dataSource has been injected into Page via a DI container (as opposed to created in a constructor or set explicitly) is completely transparent as far as the end developer is concerned.

To elaborate on what Sam means about testing, consider the following which could be an example of how code is written with statics, or localised creates

class CustomPage extends Page {

	public function ImportantPeople() {
		$list = new ArrayObject();

		// localised create
		$ldap = new LdapServer('details');
		$people = $ldap->find('cn=SomeBranch');

		// or using a static
		$people = LdapServer::find('cn=SomeBranch');

		foreach ($people as $person) {
			// filter it out or something
		}

		return $list;
	}
}

Add to this the fact that your _config.php now has stuff like

LdapServer::$host = 'ldap.server.com';
LdapServer::$user = 'user';

etc

How do you write a test for that? You can't do a true 'unit' test, because you're relying on external data, which you have no control over. You can write an integration test, but it means that your ldap server needs to be running while testing which is a massive pain if you're wanting to use continuous integration to manage things.

Instead, how about

class CustomPage extends Page {

	public $ldapServer;

	public static $injections = array(
		'ldapServer' => 'LdapServer'
	);

	public function ImportantPeople() {
		$list = new ArrayObject();
		$people = $this->ldapServer->find('cn=SomeBranch');

		foreach ($people as $person) {
			// filter out plebs
		}

		return $list;
	}
}

Not a huge change, but it means now that for testing purposes (either when writing actual test cases, or running on a developer machine), you can simply substitue $ldapServer with a dummy or mock instance.

public function testImportantPeople() {
	$page = new CustomPage(); 
	$page->ldapServer = new MyDummyLdapServer();

	// or, more correctly
	$mockLdap = $this->getMock('LdapConnector');
	$mockLdap->expects($this->once())
			->method('find')
			->with($this->equalTo('cn=SomeBranch')
			->will($this->returnValue(array('fake, controlled ldap data')));
	$page->ldapServer = $mockLdap;
}

But there's a bit of an issue - in SilverStripe, page objects are instantiated automatically by the framework, meaning you don't get a chance to set this dependency. Also, what if you had an authenticator using the same Ldap connection? Wouldn't it make sense to have both objects use the same connection?

So, assuming an appropriate annotations structure, we define the objects like

class CustomPage extends Page {
	/** @Inject LdapServer */
	public $ldapServer;
}

class LdapAuthenticator {
	/** @Inject LdapServer */
	public $ldapServer;
}

The DI container takes care of injecting both these properties automatically with the class registered as LdapServer, which could be done via configuration as follows

$injector->load(array(
	'LdapServer'	=> array(
		'class'		=> 'LdapConnector',
		'properties'	=> array(
			'host'		=> 'ldap.server.com',
			'user'		=> 'user',
			'pass'		=> 'pass',
			'dn'		=> 'dn=com,cn=silverstripe'
		)
	)
	// or using a more straightforward approach
	'LdapServer'	=> "LdapConnector('ldap.server.com', 'user', 'pass', 'dn=com,cn=silverstripe')"
));