dependency injection in drupal 8
DESCRIPTION
TRANSCRIPT
Dependency Injection in Drupal 8
By Alexei Gorobetsasgorobets
Why talk about DI?
* Drupal 8 is coming…* DI is a commonly used pattern in software design
The problems in D7
1. Strong dependencies* Globals: - users
- database- language- paths
* EntityFieldQuery still assumes SQL in property queries* Views assumes SQL database.
The problems in D7
2. No way to reuse.
* Want to change a small part of contrib code? Copy the callback entirely and replace it with your code.
The problems in D7
3. A lot of clutter happening.
* Use of preprocess to do calculations.* Excessive use of alter hooks for some major replacements.* PHP in template files? Huh =)
The problems in D7
4. How can we test something?
* Pass dummy DB connection? NO.* Create mock objects? NO.* Want a simple test class?Bootstrap Drupal first.
How we want our code to look like?
How it actually looks?
This talk is about
* DI Design Pattern* DI in Symfony* DI in Drupal 8
Our goal:
Let’s take a wrong approach
Some procedural code
function foo_bar($foo) { global $user; if ($user->uid != 0) { $nodes = db_query("SELECT * FROM {node}")->fetchAll(); } // Load module file that has the function. module_load_include('inc', cool_module, 'cool.admin'); node_make_me_look_cool(NOW());}
Some procedural code
function foo_bar($foo) { global $user; if ($user->uid != 0) { $nodes = db_query("SELECT * FROM {node}")->fetchAll(); } // Load module file that has the function. module_load_include('inc', cool_module, 'cool.admin'); node_make_me_look_cool(NOW());}
* foo_bar knows about the user object.
* node_make_me_look_cool needs a function from another module.
* You need bootstrapped Drupal to use module_load_include, or at least include the file that declares it.
* foo_bar assumes some default database connection.
Let’s try an OO approach
User class uses sessions to store language
class SessionStorage{ function __construct($cookieName = 'PHP_SESS_ID') { session_name($cookieName); session_start(); } function set($key, $value) { $_SESSION[$key] = $value; } function get($key) { return $_SESSION[$key]; } // ...}
class User{ protected $storage; function __construct() { $this->storage = new SessionStorage(); } function setLanguage($language) { $this->storage->set('language', $language); } function getLanguage() { return $this->storage->get('language'); } // ...}
Working with User$user = new User();
$user->setLanguage('fr');
$user_language = $user->getLanguage();
Working with such code is damn easy.
What could possibly go wrong here?
What if?What if we want to change a session cookie name?
What if?What if we want to change a session cookie name?
class User{ function __construct() { $this->storage = new SessionStorage('SESSION_ID'); } // ...}
Hardcode the name in User’s constructor
What if?
class User{ function __construct() { $this->storage = new SessionStorage(STORAGE_SESSION_NAME); } // ...} define('STORAGE_SESSION_NAME', 'SESSION_ID');
Define a constant
What if we want to change a session cookie name?
What if?
class User{ function __construct($sessionName) { $this->storage = new SessionStorage($sessionName); } // ...} $user = new User('SESSION_ID');
Send cookie name as User argument
What if we want to change a session cookie name?
What if?
class User{ function __construct($storageOptions) { $this->storage = new SessionStorage($storageOptions['session_name']); } // ...} $user = new User(array('session_name' => 'SESSION_ID'));
Send an array of options as User argument
What if we want to change a session cookie name?
What if?What if we want to change a session storage?
For instance to store sessions in DB or files
What if?What if we want to change a session storage?
For instance to store sessions in DB or files
You need to change User class
Introducing Dependency Injection
Here it is:
class User{ function __construct($storage) { $this->storage = $storage; } // ...}
Instead of instantiating the storage in User, let’s just pass it from outside.
Here it is:
class User{ function __construct($storage) { $this->storage = $storage; } // ...}
Instead of instantiating the storage in User, let’s just pass it from outside.
$storage = new SessionStorage('SESSION_ID');$user = new User($storage);
Types of injections:class User{ function __construct($storage) { $this->storage = $storage; }}
class User{ function setSessionStorage($storage) { $this->storage = $storage; }}
class User{ public $sessionStorage;} $user->sessionStorage = $storage;
Constructor injection
Setter injection
Property injection
Ok, where does injection happen?
$storage = new SessionStorage('SESSION_ID');$user = new User($storage);
Where should this code be?
Ok, where does injection happen?
$storage = new SessionStorage('SESSION_ID');$user = new User($storage);
Where should this code be?
* Manual injection* Injection in a factory class* Using a container/injector
How frameworks do that?
Dependency Injection Container (DIC)or
Service container
What is a Service?
Service is any PHP object that performs some sort of "global"
task
Let’s say we have a class
class Mailer{ private $transport;
public function __construct($transport) { $this->transport = $transport; }
// ...}
How to make it a service?
Using YAMLparameters: mailer.class: Mailer mailer.transport: sendmail
services: mailer: class: "%mailer.class%" arguments: ["%my_mailer.transport%"]
Using YAMLparameters: mailer.class: Mailer mailer.transport: sendmail
services: mailer: class: "%mailer.class%" arguments: ["%my_mailer.transport%"]
Loading a service from yml file.
require_once '/PATH/TO/sfServiceContainerAutoloader.php';sfServiceContainerAutoloader::register(); $sc = new sfServiceContainerBuilder(); $loader = new sfServiceContainerLoaderFileYaml($sc);$loader->load('/somewhere/services.yml');
Use PHP Dumper to create PHP file once$name = 'Project'.md5($appDir.$isDebug.$environment).'ServiceContainer';$file = sys_get_temp_dir().'/'.$name.'.php'; if (!$isDebug && file_exists($file)){ require_once $file; $sc = new $name();}else{ // build the service container dynamically $sc = new sfServiceContainerBuilder(); $loader = new sfServiceContainerLoaderFileXml($sc); $loader->load('/somewhere/container.xml'); if (!$isDebug) { $dumper = new sfServiceContainerDumperPhp($sc); file_put_contents($file, $dumper->dump(array('class' => $name)); }}
Use Graphviz dumper for this beauty
Symfony terminology
Symfony terminology
Compile the containerThere is no reason to pull configurations on every
request. We just compile the container in a PHP class with hardcoded methods for each service.
container->compile();
Compiler passes
Compiler passes are classes that process the container, giving you an opportunity to manipulate existing service definitions.
Use them to:
● Specify a different class for a given serviceid ● Process“tagged”services
Compiler passes
Tagged services
There is a way to tag services for further processing in compiler passes.
This technique is used to register event subscribers to Symfony’s event dispatcher.
Tagged services
Event subscribers
Event subscribers
Changing registered service
1. Write OO code and get wired into the container.
2. In case of legacy procedural code you can use:
Drupal::service(‘some_service’);
Example:
Drupal 7:
$path = $_GET['q']
Drupal 8:
$request = Drupal::service('request');
$path = $request->attributes->get('_system_path');
Ways to use core’s services
● The Drupal Kernel : core/lib/Drupal/Core/DrupalKernel.php
● Services are defined in: core/core.services.yml
● Compiler passes get added in: core/lib/Drupal/Core/CoreServiceProvider.php
● Compiler pass classes are in: core/lib/Drupal/Core/DependencyInjection/Compiler/
Where the magic happens?
● Define module services in : mymodule/mymodule.services.yml
● Compiler passes get added in: mymodule/lib/Drupal/mymodule/MymoduleServiceProvider.php
● All classes including Compiler class classes live in: mymodule/lib/Drupal/mymodule
What about modules?
Resources:
● Fabien Potencier’s “What is Dependency Injection” series
● Symfony Service Container● Symfony Dependency Injection Component● WSCCI Conversion Guide● Change notice: Use Dependency Injection to
handle global PHP objects
$slide = new Questions();
$current_slide = new Slide($slide);