IoC with PHP Chris Weldon Dallas TechFest 2011
Before We Begin http://bit.ly/rf1pxR git://github.com/neraath/ioc-php-talk.git
Your Guide: Chris Weldon • Fightin’ Texas Aggie • .Net and PHP Developer • UNIX and Windows Sysadmin • Senior Consultant at Improving Enterprises • Contact Me: chris@chrisweldon.net
Agile, Microsoft, Open Technologies, UX Applied Training, Coaching, Mentoring Certified Consulting Rural Sourcing Recruiting Services
Before We Get to IoC... <?php class Authenticator { private $_repository; public function __construct() { $this->_repository = new DataAccessLayer(); } public function authenticate($username, $password) { $hashedPassword = md5($password); $user = $this->_repository->findByUsernameAndPassword( $username, $hashedPassword); return $user === null; } }
What are the problems?
What are the problems? • Strongly coupled to DataAccessLayer
What are the problems? • Strongly coupled to DataAccessLayer Authenticator authenticate() : bool DataAccessLayer findByUsernameAndPassword : array
What are the problems? • Strongly coupled to DataAccessLayer • Authenticator Very inflexible authenticate() : bool DataAccessLayer findByUsernameAndPassword : array
What are the problems? • Strongly coupled to DataAccessLayer • Authenticator Very inflexible authenticate() : bool • How to configure DataAccessLayer? DataAccessLayer findByUsernameAndPassword : array
What are the problems? • Strongly coupled to DataAccessLayer • Authenticator Very inflexible authenticate() : bool • How to configure DataAccessLayer? • DataAccessLayer Let it read configs? findByUsernameAndPassword : array
What are the problems? • Strongly coupled to DataAccessLayer • Authenticator Very inflexible authenticate() : bool • How to configure DataAccessLayer? • DataAccessLayer Let it read configs? findByUsernameAndPassword : array • How to test the Authenticator?
Let’s solve it • What are our goals? • Decrease coupling • Increase configurability
<?php interface IUserRepository { function findByUsernameAndPassword($username, $password); } class DataAccessLayer implements IUserRepository { private $_configParams; private $_database; public function __construct(array $configParams) { $this->_configParams = $configParams; $this->_database = Zend_Db::factory('Pdo_Mysql', $this->_configParams); } public function findByUsernameAndPassword($username, $password) { $query = 'SELECT * FROM users WHERE username = ? AND password = ?'; $result = $this->_database->fetchAll($query, $username, $password); return $result; } }
Our Updated Authenticator <?php class Authenticator { private $_repository; public function __construct(IUserRepository $repository) { $this->_repository = $repository; } public function authenticate($username, $password) { $hashedPassword = md5($password); $user = $this->_repository->findByUsernameAndPassword( $username, $hashedPassword); return $user === null; } }
Time to Consume <?php class LoginController { public function login($username, $password) { $configuration = Zend_Registry::get('dbconfig'); $dal = new DataAccessLayer($configuration); $authenticator = new Authenticator($dal); if ($authenticator->authenticate($username, $password)) { // Do something to log the user in. } } }
Goal Recap • What were our goals? • Decrease coupling • Increase configurability
Goal Recap • What were our goals? • Decrease coupling • Increase configurability
Goal Recap • What were our goals? • Decrease coupling • Increase configurability
What You Saw Was IoC • Inversion of Control changes direction of responsibility • Someone else responsible for creating and providing my dependencies • Most commonly applied pattern: Dependency Injection • Follows Dependency Inversion Principle from SOLID • Culture War: IoC vs. DI vs. Naming vs. Principles vs. Ideology
Dependency Inversion • “High-level modules should not depend upon low level modules. They should depend upon abstractions. • “Abstractions should not depend upon details. Details should depend upon abstractions.” Robert Martin
Let’s Draw
Let’s Draw Authenticator authenticate() : bool DataAccessLayer findByUsernameAndPassword : array
Let’s Draw Authenticator authenticate() : bool DataAccessLayer findByUsernameAndPassword : array
Let’s Draw Authenticator authenticate() : bool Authenticator authenticate() : bool IUserRepository findByUsernameAndPassword : array DataAccessLayer findByUsernameAndPassword : array DataAccessLayer findByUsernameAndPassword : array
Benefit: Flexibility <?php class WebServiceUserRepository implements IUserRepository { public function findByUsernameAndPassword($username, $password) { // Fetch our user through JSON or SOAP } } class OAuthRepository implements IUserRepository { public function findByUsernameAndPassword($username, $password) { // Connect to your favorite OAuth provider } }
Benefit: Testable <?php class WhenAuthenticating extends PHPUnit_Framework_TestCase { public function testGivenInvalidUsernameAndPasswordShouldReturnFalse() { $stub = $this->getMock('IUserRepository'); $stub->expects($this->any()) ->method('findByUsernameAndPassword') ->will($this->returnValue(null)); $authenticator = new Authenticator($stub); $this->assertFalse($authenticator->authenticate('user', 'pass')); } }
Dependency Injection • Now we can inject our dependencies to our consumer classes • Still requires some other class to be tightly coupled to both of those • Need a container that can help abstract the relationship between the interface and implementation
<?php class UserRepositoryContainer { /** @return IUserRepository **/ public function getRepository() { $container = new DataAccessLayer(array( 'dsn' => 'mysql://localhost/database', 'username' => 'user', 'password' => 'pass' )); return $container; } } class LoginController { public function login($username, $password) { $container = new UserRepositoryContainer(); $repository = $container->getRepository(); $authenticator = new Authenticator($repository); // ... } }
Container Woes • No uniform interface by which to access services • Still tightly coupled with dependencies • Configurability of the container difficult • How to auto-inject dependency for configured consumer classes?
Symfony Dependency Injection Container • Two Ways to Setup and Use sfServiceContainer • Create subclass • Manual registration via code or config
sfServiceContainer Subclass <?php class UserRepositoryContainer extends sfServiceContainer { protected function getUserRepositoryService() { $container = new DataAccessLayer($this['repository.config']); return $container; } }
Consuming the Container <?php class LoginController { public function login($username, $password) { $configuration = Zend_Registry::get('dbconfig'); $container = new UserRepositoryContainer(array( 'repository.config' => $configuration )); $repository = $container->userRepository; $authenticator = new Authenticator($repository); if ($authenticator->authenticate($username, $password)) { // Do something to log the user in. } } }
That’s Pretty Nice • Configurability a lot easier • Uniform interface for accessing services • How does this scale when there are lots of dependencies? • Aren’t we still coupling the container to the implementation at compile time?
The Builder • Provides a uniform way of describing services, without custom containers • For each service description, we have the flexibility to configure an object: • At instantiation (Constructor Injection) • Post-instantiation (Setter/Method Injection)
How to Describe a Service • Code-based or Config-based • Code-based allows for run-time changing of injection parameters • Config-based provides a way to change parameters between environments with no code changes • Not mutually exclusive
Code-Based Description <?php // Imagine this is a bootstrap file. $configuration = Zend_Registry::get('dbconfig'); $builder = new sfServiceContainerBuilder(); $builder->register('user_repository', 'DataAccessLayer') ->addArgument($configuration) // OR ->addArgument('%repository.config%') from earlier ->setShared(false); $builder->register('authenticator', 'Authenticator') ->addArgument(new sfServiceReference('user_repository')); Zend_Registry::set('di_container', $builder);
Config-Based Description <?xml version="1.0" ?> <container xmlns="http://symfony-project.org/2.0/container"> <parameters> <parameter key="user_repository.dsn">mysql://localhost/database</parameter> <parameter key="user_repository.username">username</parameter> <parameter key="user_repository.password">password</parameter> </parameters> <services> <service id="user_repository" class="DataAccessLayer" shared="false"> <argument type="collection"> <argument key="dsn">%user_repository.dsn%</argument> <argument key="username">%user_repository.username%</argument> <argument key="password">%user_repository.password%</argument> </argument> </service> <service id="authenticator" class="Authenticator"> <argument type="service" id="user_repository" /> </service> </services> </container>
Loading the Config <?php // Imagine this is a bootstrap file. $builder = new sfServiceContainerBuilder(); $loader = new sfServiceContainerLoaderFileXml($builder); $loader->load('/pathTo/services.xml'); Zend_Registry::set('di_container', $builder);
Using the Container <?php class LoginController { public function login($username, $password) { $container = Zend_Registry::get('di_container'); $authenticator = $container->authenticator; if ($authenticator->authenticate($username, $password)) { // Do something to log the user in. } } }
High Level Picture DataAccessLayer findByUsernameAndPassword : array IUserRepository findByUsernameAndPassword : array sfServiceContainerBuilder getService : object setService : void hasService : bool Authenticator authenticate() : bool IAuthenticator authenticate() : bool sfServiceContainerInterface getService : object LoginController setService : void login() : void hasService : bool
When to Use a DI Container
When to Use a DI Container • Not for model objects (e.g. Orders, Documents, etc.)
When to Use a DI Container • Not for model objects (e.g. Orders, Documents, etc.) • Great for resource requirements (e.g. repositories, loggers, etc.)
When to Use a DI Container • Not for model objects (e.g. Orders, Documents, etc.) • Great for resource requirements (e.g. repositories, loggers, etc.) • Really great for plugin-type architecture
When to Use a DI Container • Not for model objects (e.g. Orders, Documents, etc.) • Great for resource requirements (e.g. repositories, loggers, etc.) • Really great for plugin-type architecture • But not necessary to use Dependency Injection!
Other Considerations
Other Considerations • Learning curve
Other Considerations • Learning curve • Tracking dependencies
Other Considerations • Learning curve • Tracking dependencies • Dependency changes
Other Considerations • Learning curve • Tracking dependencies • Dependency changes • Setter/method vs. constructor injection
Service Lifetimes • setShared() allows you to specify context persistence • If shared, acts like a singleton • Useful if construction is expensive or state persistence required
Let’s Code
Thank You! http://bit.ly/rf1pxR git://github.com/neraath/ioc-php-talk.git

IoC with PHP

  • 1.
    IoC with PHP Chris Weldon Dallas TechFest 2011
  • 2.
    Before We Begin http://bit.ly/rf1pxR git://github.com/neraath/ioc-php-talk.git
  • 3.
    Your Guide: ChrisWeldon • Fightin’ Texas Aggie • .Net and PHP Developer • UNIX and Windows Sysadmin • Senior Consultant at Improving Enterprises • Contact Me: chris@chrisweldon.net
  • 4.
    Agile, Microsoft, OpenTechnologies, UX Applied Training, Coaching, Mentoring Certified Consulting Rural Sourcing Recruiting Services
  • 5.
    Before We Getto IoC... <?php class Authenticator { private $_repository; public function __construct() { $this->_repository = new DataAccessLayer(); } public function authenticate($username, $password) { $hashedPassword = md5($password); $user = $this->_repository->findByUsernameAndPassword( $username, $hashedPassword); return $user === null; } }
  • 6.
    What are theproblems?
  • 7.
    What are theproblems? • Strongly coupled to DataAccessLayer
  • 8.
    What are theproblems? • Strongly coupled to DataAccessLayer Authenticator authenticate() : bool DataAccessLayer findByUsernameAndPassword : array
  • 9.
    What are theproblems? • Strongly coupled to DataAccessLayer • Authenticator Very inflexible authenticate() : bool DataAccessLayer findByUsernameAndPassword : array
  • 10.
    What are theproblems? • Strongly coupled to DataAccessLayer • Authenticator Very inflexible authenticate() : bool • How to configure DataAccessLayer? DataAccessLayer findByUsernameAndPassword : array
  • 11.
    What are theproblems? • Strongly coupled to DataAccessLayer • Authenticator Very inflexible authenticate() : bool • How to configure DataAccessLayer? • DataAccessLayer Let it read configs? findByUsernameAndPassword : array
  • 12.
    What are theproblems? • Strongly coupled to DataAccessLayer • Authenticator Very inflexible authenticate() : bool • How to configure DataAccessLayer? • DataAccessLayer Let it read configs? findByUsernameAndPassword : array • How to test the Authenticator?
  • 13.
    Let’s solve it • What are our goals? • Decrease coupling • Increase configurability
  • 14.
    <?php interface IUserRepository { function findByUsernameAndPassword($username, $password); } class DataAccessLayer implements IUserRepository { private $_configParams; private $_database; public function __construct(array $configParams) { $this->_configParams = $configParams; $this->_database = Zend_Db::factory('Pdo_Mysql', $this->_configParams); } public function findByUsernameAndPassword($username, $password) { $query = 'SELECT * FROM users WHERE username = ? AND password = ?'; $result = $this->_database->fetchAll($query, $username, $password); return $result; } }
  • 15.
    Our Updated Authenticator <?php classAuthenticator { private $_repository; public function __construct(IUserRepository $repository) { $this->_repository = $repository; } public function authenticate($username, $password) { $hashedPassword = md5($password); $user = $this->_repository->findByUsernameAndPassword( $username, $hashedPassword); return $user === null; } }
  • 16.
    Time to Consume <?php classLoginController { public function login($username, $password) { $configuration = Zend_Registry::get('dbconfig'); $dal = new DataAccessLayer($configuration); $authenticator = new Authenticator($dal); if ($authenticator->authenticate($username, $password)) { // Do something to log the user in. } } }
  • 17.
    Goal Recap • What were our goals? • Decrease coupling • Increase configurability
  • 18.
    Goal Recap • What were our goals? • Decrease coupling • Increase configurability
  • 19.
    Goal Recap • What were our goals? • Decrease coupling • Increase configurability
  • 20.
    What You SawWas IoC • Inversion of Control changes direction of responsibility • Someone else responsible for creating and providing my dependencies • Most commonly applied pattern: Dependency Injection • Follows Dependency Inversion Principle from SOLID • Culture War: IoC vs. DI vs. Naming vs. Principles vs. Ideology
  • 21.
    Dependency Inversion • “High-level modules should not depend upon low level modules. They should depend upon abstractions. • “Abstractions should not depend upon details. Details should depend upon abstractions.” Robert Martin
  • 22.
  • 23.
    Let’s Draw Authenticator authenticate() : bool DataAccessLayer findByUsernameAndPassword : array
  • 24.
    Let’s Draw Authenticator authenticate() : bool DataAccessLayer findByUsernameAndPassword : array
  • 25.
    Let’s Draw Authenticator authenticate() : bool Authenticator authenticate() : bool IUserRepository findByUsernameAndPassword : array DataAccessLayer findByUsernameAndPassword : array DataAccessLayer findByUsernameAndPassword : array
  • 26.
    Benefit: Flexibility <?php class WebServiceUserRepositoryimplements IUserRepository { public function findByUsernameAndPassword($username, $password) { // Fetch our user through JSON or SOAP } } class OAuthRepository implements IUserRepository { public function findByUsernameAndPassword($username, $password) { // Connect to your favorite OAuth provider } }
  • 27.
    Benefit: Testable <?php class WhenAuthenticatingextends PHPUnit_Framework_TestCase { public function testGivenInvalidUsernameAndPasswordShouldReturnFalse() { $stub = $this->getMock('IUserRepository'); $stub->expects($this->any()) ->method('findByUsernameAndPassword') ->will($this->returnValue(null)); $authenticator = new Authenticator($stub); $this->assertFalse($authenticator->authenticate('user', 'pass')); } }
  • 28.
    Dependency Injection • Now we can inject our dependencies to our consumer classes • Still requires some other class to be tightly coupled to both of those • Need a container that can help abstract the relationship between the interface and implementation
  • 29.
    <?php class UserRepositoryContainer { /** @return IUserRepository **/ public function getRepository() { $container = new DataAccessLayer(array( 'dsn' => 'mysql://localhost/database', 'username' => 'user', 'password' => 'pass' )); return $container; } } class LoginController { public function login($username, $password) { $container = new UserRepositoryContainer(); $repository = $container->getRepository(); $authenticator = new Authenticator($repository); // ... } }
  • 30.
    Container Woes • No uniform interface by which to access services • Still tightly coupled with dependencies • Configurability of the container difficult • How to auto-inject dependency for configured consumer classes?
  • 31.
    Symfony Dependency Injection Container • Two Ways to Setup and Use sfServiceContainer • Create subclass • Manual registration via code or config
  • 32.
    sfServiceContainer Subclass <?php class UserRepositoryContainerextends sfServiceContainer { protected function getUserRepositoryService() { $container = new DataAccessLayer($this['repository.config']); return $container; } }
  • 33.
    Consuming the Container <?php classLoginController { public function login($username, $password) { $configuration = Zend_Registry::get('dbconfig'); $container = new UserRepositoryContainer(array( 'repository.config' => $configuration )); $repository = $container->userRepository; $authenticator = new Authenticator($repository); if ($authenticator->authenticate($username, $password)) { // Do something to log the user in. } } }
  • 34.
    That’s Pretty Nice • Configurability a lot easier • Uniform interface for accessing services • How does this scale when there are lots of dependencies? • Aren’t we still coupling the container to the implementation at compile time?
  • 35.
    The Builder • Provides a uniform way of describing services, without custom containers • For each service description, we have the flexibility to configure an object: • At instantiation (Constructor Injection) • Post-instantiation (Setter/Method Injection)
  • 36.
    How to Describea Service • Code-based or Config-based • Code-based allows for run-time changing of injection parameters • Config-based provides a way to change parameters between environments with no code changes • Not mutually exclusive
  • 37.
    Code-Based Description <?php // Imaginethis is a bootstrap file. $configuration = Zend_Registry::get('dbconfig'); $builder = new sfServiceContainerBuilder(); $builder->register('user_repository', 'DataAccessLayer') ->addArgument($configuration) // OR ->addArgument('%repository.config%') from earlier ->setShared(false); $builder->register('authenticator', 'Authenticator') ->addArgument(new sfServiceReference('user_repository')); Zend_Registry::set('di_container', $builder);
  • 38.
    Config-Based Description <?xml version="1.0"?> <container xmlns="http://symfony-project.org/2.0/container"> <parameters> <parameter key="user_repository.dsn">mysql://localhost/database</parameter> <parameter key="user_repository.username">username</parameter> <parameter key="user_repository.password">password</parameter> </parameters> <services> <service id="user_repository" class="DataAccessLayer" shared="false"> <argument type="collection"> <argument key="dsn">%user_repository.dsn%</argument> <argument key="username">%user_repository.username%</argument> <argument key="password">%user_repository.password%</argument> </argument> </service> <service id="authenticator" class="Authenticator"> <argument type="service" id="user_repository" /> </service> </services> </container>
  • 39.
    Loading the Config <?php //Imagine this is a bootstrap file. $builder = new sfServiceContainerBuilder(); $loader = new sfServiceContainerLoaderFileXml($builder); $loader->load('/pathTo/services.xml'); Zend_Registry::set('di_container', $builder);
  • 40.
    Using the Container <?php classLoginController { public function login($username, $password) { $container = Zend_Registry::get('di_container'); $authenticator = $container->authenticator; if ($authenticator->authenticate($username, $password)) { // Do something to log the user in. } } }
  • 41.
    High Level Picture DataAccessLayer findByUsernameAndPassword : array IUserRepository findByUsernameAndPassword : array sfServiceContainerBuilder getService : object setService : void hasService : bool Authenticator authenticate() : bool IAuthenticator authenticate() : bool sfServiceContainerInterface getService : object LoginController setService : void login() : void hasService : bool
  • 42.
    When to Usea DI Container
  • 43.
    When to Usea DI Container • Not for model objects (e.g. Orders, Documents, etc.)
  • 44.
    When to Usea DI Container • Not for model objects (e.g. Orders, Documents, etc.) • Great for resource requirements (e.g. repositories, loggers, etc.)
  • 45.
    When to Usea DI Container • Not for model objects (e.g. Orders, Documents, etc.) • Great for resource requirements (e.g. repositories, loggers, etc.) • Really great for plugin-type architecture
  • 46.
    When to Usea DI Container • Not for model objects (e.g. Orders, Documents, etc.) • Great for resource requirements (e.g. repositories, loggers, etc.) • Really great for plugin-type architecture • But not necessary to use Dependency Injection!
  • 47.
  • 48.
  • 49.
    Other Considerations • Learning curve • Tracking dependencies
  • 50.
    Other Considerations • Learning curve • Tracking dependencies • Dependency changes
  • 51.
    Other Considerations • Learning curve • Tracking dependencies • Dependency changes • Setter/method vs. constructor injection
  • 52.
    Service Lifetimes • setShared() allows you to specify context persistence • If shared, acts like a singleton • Useful if construction is expensive or state persistence required
  • 53.
  • 54.
    Thank You! http://bit.ly/rf1pxR git://github.com/neraath/ioc-php-talk.git