Dependency Injection in
Drupal 8
By Alexei Gorobets
asgorobets
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 YAML
parameters:
mailer.class: Mailer
mailer.transport: sendmail
services:
mailer:
class: "%mailer.class%"
arguments: ["%my_mailer.transport%"]
Using YAML
parameters:
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 container
There 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/Mymodule
ServiceProvider.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);
Dependency injection in Drupal 8

Dependency injection in Drupal 8

  • 1.
    Dependency Injection in Drupal8 By Alexei Gorobets asgorobets
  • 2.
    Why talk aboutDI? * Drupal 8 is coming… * DI is a commonly used pattern in software design
  • 3.
    The problems inD7 1. Strong dependencies * Globals: - users - database - language - paths * EntityFieldQuery still assumes SQL in property queries * Views assumes SQL database.
  • 4.
    The problems inD7 2. No way to reuse. * Want to change a small part of contrib code? Copy the callback entirely and replace it with your code.
  • 5.
    The problems inD7 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 =)
  • 6.
    The problems inD7 4. How can we test something? * Pass dummy DB connection? NO. * Create mock objects? NO. * Want a simple test class? Bootstrap Drupal first.
  • 7.
    How we wantour code to look like?
  • 9.
  • 11.
    This talk isabout * DI Design Pattern * DI in Symfony * DI in Drupal 8
  • 12.
  • 13.
    Let’s take awrong approach
  • 14.
    Some procedural code functionfoo_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()); }
  • 15.
    Some procedural code functionfoo_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.
  • 16.
    Let’s try anOO approach
  • 17.
    User class usessessions 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'); } // ... }
  • 18.
    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?
  • 19.
    What if? What ifwe want to change a session cookie name?
  • 20.
    What if? What ifwe want to change a session cookie name? class User { function __construct() { $this->storage = new SessionStorage('SESSION_ID'); } // ... } Hardcode the name in User’s constructor
  • 21.
    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?
  • 22.
    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?
  • 23.
    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?
  • 24.
    What if? What ifwe want to change a session storage? For instance to store sessions in DB or files
  • 25.
    What if? What ifwe want to change a session storage? For instance to store sessions in DB or files You need to change User class
  • 26.
  • 28.
    Here it is: classUser { function __construct($storage) { $this->storage = $storage; } // ... } Instead of instantiating the storage in User, let’s just pass it from outside.
  • 29.
    Here it is: classUser { 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);
  • 30.
    Types of injections: classUser { 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
  • 31.
    Ok, where doesinjection happen? $storage = new SessionStorage('SESSION_ID'); $user = new User($storage); Where should this code be?
  • 32.
    Ok, where doesinjection 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
  • 33.
    How frameworks dothat? Dependency Injection Container (DIC) or Service container
  • 34.
    What is aService?
  • 35.
    Service is anyPHP object that performs some sort of "global" task
  • 36.
    Let’s say wehave a class class Mailer { private $transport; public function __construct($transport) { $this->transport = $transport; } // ... }
  • 37.
    How to makeit a service?
  • 38.
    Using YAML parameters: mailer.class: Mailer mailer.transport:sendmail services: mailer: class: "%mailer.class%" arguments: ["%my_mailer.transport%"]
  • 39.
    Using YAML parameters: 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');
  • 40.
    Use PHP Dumperto 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)); } }
  • 41.
    Use Graphviz dumperfor this beauty
  • 42.
  • 43.
  • 44.
    Compile the container Thereis 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();
  • 45.
    Compiler passes Compiler passesare 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
  • 46.
  • 47.
    Tagged services There isa way to tag services for further processing in compiler passes. This technique is used to register event subscribers to Symfony’s event dispatcher.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
    1. Write OOcode 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
  • 53.
    ● The DrupalKernel : 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?
  • 54.
    ● Define moduleservices in : mymodule/mymodule.services.yml ● Compiler passes get added in: mymodule/lib/Drupal/mymodule/Mymodule ServiceProvider.php ● All classes including Compiler class classes live in: mymodule/lib/Drupal/mymodule What about modules?
  • 55.
    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
  • 56.
    $slide = newQuestions(); $current_slide = new Slide($slide);