migrating to dependency injection
DESCRIPTION
"Dependency injection" (DI) seems like one of those hot buzzwords that will solve all your problems. But what is DI really? How does it help keep code clean and maintainable? And how do you take a legacy codebase and rewrite it to take advantage of DI? This talk takes an application written without DI and walks through the steps for "injecting" DI into the code. Learn the difference between "dependency injection" and "dependency injection containers". See how DI makes things like event-driven architectures simple to implement. And learn how DI leads to code that is easier to debug and test.TRANSCRIPT
Migrating toDependency Injection
@josh_adell
www.servicetrade.com
blog.everymansoftware.com
github.com/jadell/neo4jphp
https://joind.in/10419
Legacy Code
Began with PHP 5.0 (now 5.3)~80k LoC
Mixed PHP & HTML3 x functions.php with ~9k LoC eachmagic_quotes AND register_globals
No abstract classes or interfacesTightly coupled components
No tests
class UserRepository {
public function findUser($id) {
$db = new Database(/* connection params*/);
$userInfo = $db->query(/* User query with $id */);
$user = new User();
$user->setProperties($userInfo);
return $user;
}
}
========================================================
class UserController {
public function getUser() {
$repo = new UserRepository();
return $repo->findUser($_GET['id']);
}
}
Dependency Injection (DI)
Components should notcreate the other components
on which they depend.
Injector injects Dependencies into Consumers
class PageController {
public function __construct(Database $db) {
$this->repo = new Repository($db);
}
}
========================================================
class PageController {
public function __construct(Repository $repo) {
$this->repo = $repo;
}
}
DI Container
Wires objects together
I like Pimple: http://pimple.sensiolabs.org/
ParametersServices / Shared Objects
Factory ServicesFactory Callbacks
$di = new Pimple();
// Parameters
$di['dbHost'] = "localhost";
$di['dbUser'] = "username";
$di['dbPass'] = "password";
// Services / Shared Objects
$di['database'] = function ($di) {
return new Database($di['dbHost'], $di['dbUser'], $di['dbPass']);
};
$di['userRepository'] = function ($di) {
return new UserRepository($di['database]);
};
// Factory Services
$di['user'] = $di->factory(function () {
return new User();
});
// Factory Callbacks
$di['userFactory'] = $di->protect(function ($name) {
return new User($name);
});
Re-architect the applicationso that object instantiation
only occurs in the DI Container.
Re-architect the applicationso the DI Container
is only referenced from the DI Container.
Why Bother?
Testing / QualityMaintenance Extensibility
Flexibility
One Step at a Time1. Create a DI factory method for the class2. Replace "new" with calls to the DI factory method3.
a. Move all shared objects to the constructorb. Move all factory creations to anonymous function in the constructor
4.a. Pass in shared objects from the DI Containerb. Pass in factories callbacks from the DI Container
Repeat from Step 1 for all classes
class UserRepository {
public function findUser($id) {
$db = new Database(/* connection params*/);
$userInfo = $db->query(/* User query with $id */);
$user = new User();
$user->setProperties($userInfo);
return $user;
}
}
========================================================
class UserController {
public function getUser() {
$repo = new UserRepository();
return $repo->findUser($_GET['id']);
}
}
Step #1: DI Container method$di['userRepository'] = function () {
return new UserRepository();
};
Step #2: Replace “new”class UserController {
public function getUser() {
global $di;
$repo = $di['userRepository'];
return $repo->findUser($_GET['id']);
}
}
Step #3a: Move shared objectsclass UserRepository {
public function __construct() {
$this->db = new Database(/* connection params*/);
}
public function findUser($id) {
$userInfo = $this->db->query(/* User query with $id */);
$user = new User();
$user->setProperties($userInfo);
return $user;
}
}
Step #3b: Move factory creationclass UserRepository {
public function __construct() {
$this->db = new Database(/* connection params*/);
$this->userFactory = function () {
return new User();
};
}
public function findUser($id) {
$userInfo = $this->db->query(/* User query with $id */);
$user = call_user_func($this->userFactory);
$user->setProperties($userInfo);
return $user;
}
}
Step #4a: Pass in shared objects$di['database'] = function () {
return new Database();
};
$di['userRepository'] = function ($di) {
return new UserRepository($di['database']);
};
========================================================
class UserRepository {
public function __construct(Database $db) {
$this->db = $db;
$this->userFactory = function () {
return new User();
};
}
// ...
Step #4b: Pass in factory callbacks$di['userFactory'] = $di->protect(function () {
return new User();
});
$di['userRepository'] = function ($di) {
return new UserRepository($di['database'], $di['userFactory']);
};
========================================================
class UserRepository {
public function __construct(Database $db, callable $userFactory) {
$this->db = $db;
$this->userFactory = $userFactory;
}
// ...
Done with UserRepository!class UserRepository {
public function __construct(Database $db, callable $userFactory) {
$this->db = $db;
$this->userFactory = $userFactory;
}
public function findUser($id) {
$userInfo = $this->db->query(/* User query with $id */);
$user = call_user_func($this->userFactory);
$user->setProperties($userInfo);
return $user;
}
}
Repeat for UserController$di['userController'] = function ($di) {
return new UserController($di['userRepository']);
};
========================================================
class UserController {
public function __construct(UserRepository $repo) {
$this->userRepo = $repo;
}
public function getUser() {
global $di;
$repo = $di['userRepository'];
return $this->userRepo->findUser($_GET['id']);
}
}
That seems like a lot of steps
New Requirement
Every time a user submits a report,send an email.
Setup Event Publisherclass User {
public function __construct(EventEmitter $ee) {
$this->event = $ee;
}
public function submitReport(Report $report) {
// ...
$this->event->publish('newReport', $this, $report);
}
}
Setup Event Subscribers$di['eventEmitter'] = function () {
return new EventEmitter();
};
$di['emailService'] = function ($di) {
return new EmailService();
};
$di['userFactory'] = $di->protect(function () use ($di)) {
$di['eventEmitter']->subscribe('newReport', $di['emailService']);
return new User($di['eventEmitter']);
});
I have to admit, it's getting better
Questions?class Presentation {
public function __construct(Presenter $presenter) {
$this->presenter = $presenter;
}
public function askQuestion(Question $question) {
return $this->presenter->answer($question);
}
}
@josh_adell
www.servicetrade.com
blog.everymansoftware.comgithub.com/jadell/neo4jphp
http://pimple.sensiolabs.org/http://martinfowler.com/articles/injection.html
https://joind.in/10419