drupaleuven-2015-06-04



drupaleuven-2015-06-04

0 0


drupaleuven-2015-06-04


On Github cyberwolf / drupaleuven-2015-06-04

Drupal 8 Dependency Injection

Kristof Coomans

2dotstwice

How we used to build stuff (1/2)

function twitter_fetch_user_timeline($id) {
  $account = twitter_account_load($id);
  $since = db_query("...", array(':screen_name' => $account->screen_name))
    ->fetchField();

  $twitter = twitter_connect($account);
  $params = $since ? array('since_id' => $since) : array();

  $statuses = $twitter->user_timeline($id, $params);
  foreach ($statuses as $status) {
    twitter_status_save($status);
  }

  if (count($statuses) > 0) {
    twitter_account_save($statuses[0]->user);
  }
}

How we used to build stuff (2/2)

function twitter_connect($account) {
  $auth = $account->get_auth();

  return new Twitter(
    variable_get('twitter_consumer_key'),
    variable_get('twitter_consumer_secret'),
    $auth['oauth_token'],
    $auth['oauth_token_secret']
  );
}

What's wrong with that?

Is this code easily testable and maintainable?

SOLID: 5 basic principles of object oriented programming and design

(Robert C. Martin)

  • Single responsibility principle: every class should have responsibility over a single part of the functionality
  • Dependency inversion principle: depend on abstractions, not on a specific implementation

What is a service?

  • A PHP object responsible for a particular task
  • Should do just one job
  • ++Testability
  • ++Configureability

What is a service / dependency injection container?

  • Manages the instantiation of services
  • Injects the needed arguments, either scalar values or other services
  • Popularized in the PHP world mainly by Fabien Potencier (Symfony)

Defining services in Drupal 8

Definition in YAML inside your module named example:

example.services.yml

Simplest form:

services:
  asset.css.optimizer:
      class: Drupal\Core\Asset\CssOptimizer

Equivalent to:

new \Drupal\Core\Asset\CssOptimizer();

Constructor injection

services:
  database:
   ...

  config.storage.active:
    class: Drupal\Core\Config\DatabaseStorage
    arguments: ['@database', config]

  config.storage.snapshot:
    class: Drupal\Core\Config\DatabaseStorage
    arguments: ['@database', config_snapshot]

Equivalent to:

$database = ...

$activeConfigStorage = new \Drupal\Core\Config\DatabaseStorage(
  $database,
  'config'
);

$snapshotConfigStorage = new \Drupal\Core\Config\DatabaseStorage(
  $database,
  'config_snapshot'
);

Setter injection

theme.manager:
  class: Drupal\Core\Theme\ThemeManager
  arguments: ...
  calls:
    - [setThemeRegistry, ['@theme.registry']]

theme.registry:
  class: Drupal\Core\Theme\Registry
  arguments: ...
  calls:
    - [setThemeManager, ['@theme.manager']]

Equivalent to:

  $themeManager = new \Drupal\Core\Theme\ThemeManager(...);

  $themeRegistry = new \Drupal\Core\Theme\Registry(...);
  $themeRegistry->setThemeManager($themeManager);

  $themeManager->setThemeRegistry($themeRegistry);

Code smell!

Construction by a factory (1/2)

Creation by a static factory method

services:
  database:
    class: Drupal\Core\Database\Connection
    factory_class: Drupal\Core\Database\Database
    factory_method: getConnection
    arguments: [default]

Equivalent to:

$database = \Drupal\Core\Database\Database::getConnection('default');

Construction by a factory (2/2)

OR by a factory service

services:
  cache_factory:
    ...

  cache.menu:
    class: Drupal\Core\Cache\CacheBackendInterface
    factory_method: get
    factory_service: cache_factory
    arguments: [menu]

Equivalent to:

$cacheFactory = ...;
$menuCache = $cacheFactory::get('menu');

How to access a service?

$container = \Drupal::getContainer();
$languageManager = $container->get('language_manager');

However, avoid using \Drupal::getContainer(), use dependency injection instead!

Dependency injection into non-services

Implement a factory method interface!

  • Plugins (Blocks, ...): ContainerFactoryPluginInterface
  • Controllers, forms: ContainerInjectionInterface

Example: Block (1/2)

/**
 * Provides a 'Language switcher' block.
 * ...
 */
class LanguageBlock
    extends BlockBase
    implements ContainerFactoryPluginInterface
{
  public function __construct(
      array $configuration,
      $plugin_id,
      $plugin_definition,
      LanguageManagerInterface $language_manager,
      PathMatcherInterface $path_matcher
    ) {
    ...
  }

Example: Block (2/2)

  public static function create(
    ContainerInterface $container,
    array $configuration,
    $plugin_id,
    $plugin_definition
  ) {
    return new static(
      $configuration,
      $plugin_id,
      $plugin_definition,
      $container->get('language_manager'),
      $container->get('path.matcher')
    );
  }
}

Example: controller (1/2)

class UserController extends ControllerBase {

  public function __construct(
    DateFormatter $date_formatter,
    UserStorageInterface $user_storage,
    UserDataInterface $user_data
  ) {
    $this->dateFormatter = $date_formatter;
    $this->userStorage = $user_storage;
    $this->userData = $user_data;
  }

Example: controller (2/2)

  public static function create(
    ContainerInterface $container
  ) {
    return new static(
      $container->get('date.formatter'),
      $container->get('entity.manager')->getStorage('user'),
      $container->get('user.data')
    );
  }
}

Why is there so much service configuration code needed?

Faster to write it like in the old days?

$database = \Drupal\Core\Database\Database::getConnection('default');
services:
  database:
    class: Drupal\Core\Database\Connection
    factory_class: Drupal\Core\Database\Database
    factory_method: getConnection
    arguments: [default]

Remember:

  • S & D of SOLID
  • ++Testability

Object Calisthenics

Technique to improve your code, by applying far stricter coding standards, among which:

  • maximum one level of indentation
  • keep your classes small, maximum 50 lines
  • maximum 2 class instance attributes

How to check? PHP CodeSniffer ruleset

Resources

Challenge

List all upcoming events of the Drupal User Group Belgium announced at their Meetup page in a Block on a Drupal 8 website, by using their API.

Bonus: add caching on the API requests.