Drupal 8 and the Symfony Event Dispatcher – Me – Hooks



Drupal 8 and the Symfony Event Dispatcher – Me – Hooks

1 0


eventdispatcher


On Github ericgsmith / eventdispatcher

Drupal 8 and the Symfony Event Dispatcher

Eric Smith / @ericgsmith / @cameronwildinghttp://ericgsmith.github.io/eventdispatcher

Adventure time!

Me

  • Drupal Developer at
Also introduce yourself...

Agenda

Background What is it & why it's useful Creating / dispatching an event Responding to an event Timing, structure, detours as this is an adventure.

Hooks

Hook system - one of the first things we learn about drupal. A lot of invoked hooks are gone. Might be completely gone for D9.

Info Hooks

  • hook_block_info
  • hook_field_info
  • hook_filter_info
Plugin system - ignoring this for today.

Hooks

  • hook_node_save
  • hook_help
  • hook_preprocess_...
Some old friends still around. We even have new hooks (reference Tassos' talk). These might not be around forever...

Invoked hooks

  • hook_boot
  • hook_init
  • hook_exit
Gone! Use events instead.

Events

... an event is an action or occurrence recognised by software that may be handled by the software.

Think javascript. Responding to events is something we often do. These events can be anything.

Events

Something happened

A form submitted, users logged in, web service called. Refer back to boot, init, exit.

Event Dispatcher

The EventDispatcher component provides tools that allow your application components to communicate with each other by dispatching events and listening to them.

A component - drupal is more modern and component based. Component - a bit of functionality we can use. Object the encapsulates the communication of other objects. Example of the mediator design pattern.

Symfony Event Dispatcher

Imagine an airport without a mediator... Mediator promotes loose coupling by keeping objects from referring to each other explicitly Drupal is using ContainerAwareEventDispatcher 3 step process - listen, notify, execute.

Symfony Event Dispatcher

Objects communicate with it by sending and subscribing to events. We will go into how to do this in the coming slides.

Symfony Event Dispatcher

Create an event object.

Symfony Event Dispatcher

Use case

We have a form that collects a name and email address.

  • Send an email to the user
  • Log the submission
  • Add the user to our CRM
  • Add the user to our newsletter
  • Tweet their details to the company fridge
We are doing a lot of things .. and thats just what we know now that we want to do.

In a world without mediators

public function submitForm(array &$form, FormStateInterface $form_state) {
  $name = $form_state->getValue('name');
  $email = $form_state->getValue('email');

  $this->mailer->mail('example', 'signup_form', $email, .... ['name' => $name]);

  $this->logger->log('notice', 'Registration of interest submitted...');

  $this->crmManager->subscribe($name, $email);

  $this->mailChimpSubscriptionManager->add($name, $email);

  $tweet = TweetFactory::create('blah blah ' . $name . ' blah blah');
  $this->tweeter->tweet($tweet);
}
Fictional classes and methods.

Problems

  • Submit method is doing too many things
  • Form class knows too many things
  • Hard to maintain
  • Hard to reuse
Clarify this is opinionated. Classes with well defined responsibilities are more flexible and extendable. Form class now nes what the form is, and a whole heap of actions when the form is submitted.

Detour: Service container

Before we go further, lets look at the container. Helps you instantiate, organize and retrieve the many objects of your application

Service:

Any PHP object that performs some sort of "global" task.

http://symfony.com/doc/current/book/service_container.html

Dependency Injection:

Inject dependencies.

Not Dependency Injection:

public function myMethod() {
  $mailer = new Mailhandler();
  $mailer->send('This is bad.');
}
Tight coupling

Dependency Injection:

protected $mailer;

public function __construct(MailHandlerInterface $mailer) {
  $this->mailer = $mailer;
}

public function myMethod() {
  $this->mailer->send('This is better.');
}
Class is constructed outside the object. Injected through either constructor or setter method. So, how do we get our object with all its dependencies created outside of it.

Step 1: Configuration

Step 2: Magic

Step 3: Service Container

Takes care of dependencies. Services are defined with their dependencies. Container is the object that lets us get services. We can reach in and grab what we want, and not worry about what that object needs.

Dispatching an event

Static event class Extend the event class Dispatch the event You have a module, doing something, you want to tell the world about it. Maybe it needs to be altered. Maybe its your own code and you need to keep it clean and tidy.

The Static Events Class

final class ExampleModuleEvents {

  /** docBlock */
  const SIGNUP_FORM_SUBMIT = 'module_name.signup_form_submit';
}
This doesn't do anything... Used for docs / console. Remember how we had modulename.api

Extend the event class

use Symfony\Component\EventDispatcher\Event;

class SignupFormEvent extends Event {

  protected $submittedName;
  protected $submittedEmail;

  public function __construct($name, $email) {
    $this->submittedName = $name;
    $this->submittedEmail = $email;
  }

  public function getSubmittedName() {
    return $this->submittedName;
  }

  public function getSubmittedEmail() { return
    $this->submittedEmail;
  }
}
This is the object we will create and pass around. It has the data we want to share. Extend the event class - it doesn't do a lot by itself. Just keeps the event name and stopPropogation method that we will discuss later. In our example we are not allowing data to be overridden, only notified.

Dispatch the event

public function submitForm(array &$form, FormStateInterface $form_state) {
  $name = $form_state->getValue('name');
  $email = $form_state->getValue('email');

  $event = new SignupFormEvent($name, $email);

  $this->eventDispatcher->dispatch(ExampleModuleEvents::SIGNUP_FORM_SUBMIT, $event);
}
Easy as that. But how do we get the event dispatcher?

Detour: Forms

I wanted to take this detour as there is a bit of misinformation about the service container online using the Drupal class.

FormBase

  • Implements FormInterface
  • Implements ContainerInjectionInterface
This is the class to extend from when creating custom forms. This class has a container method that is private - so don't use it.

ContainerInjectionInterface

public static function create(ContainerInterface $container) {
  return new static();
}
Not just for forms Gives a common factory method for creation of objects with dependencies in the service container.

ContainerInjectionInterface

protected $eventDispatcher;

public function __construct(EventDispatcherInterface $eventDispatcher) {
  $this->eventDispatcher = $eventDispatcher;
}

public static function create(ContainerInterface $container) {
  return new static($container->get('event_dispatcher'));
}
This is back on the form class. We depend on the event dispatcher, so inject it as a dependency. Can use getters and setters if you want

FormInterface

public function buildForm(array $form, FormStateInterface $form_state);

public function validateForm(array &$form, FormStateInterface $form_state);

public function submitForm(array &$form, FormStateInterface $form_state);

Listen for events

  • Implement EventSubscriberInterface
  • Implement getSubscribers method
  • Add to services.yaml
Now to recieve. Remember we are using the container aware dispatcher.

Implement EventSubscriberInterface

use Symfony\Component\EventDispatcher\EventSubscriberInterface;

class RegistrationMailer implements EventSubscriberInterface {
  protected $mailManager;
  protected $languageManager;

  public function __construct(MailManagerInterface $mailManager, LanguageManagerInterface $languageManager) {
    $this->mailManager = $mailManager;
    $this->languageManager = $languageManager;
  };

  public static function getSubscribedEvents() {
    $events[ExampleModuleEvents::SIGNUP_FORM_SUBMIT][] = array('onRegister', 0);
    return $events;
  }

  public function onRegister(SignupFormEvent $event) { ... }
}
Mention priority here Mention ability to subscribe multiple times getSubscribedEvents is called by the dispatcher, it is where we tell it about the callback to use.
public function onRegister($event) {
  $module = 'event_demo';
  $key = 'register_interest';
  $to = $event->getSubmittedEmail();
  $params = ['name' => $event->getSubmittedName()];
  $language = $this->languageManager->getDefaultLanguage();
  $this->mailManager->mail($module, $key, $to, $language, $params);
}
This is just an expanded version of what we were doing in the form submit - now in its own decoupled class.

services.yaml

services:
  example_module.registration_mailer:
    class: "Drupal\example_crm_module\EventSubscriber\RegistrationMailer"
    arguments: ["@plugin.manager.mail", "@language_manager"]
    tags:
      - { name: event_subscriber }
The tag is how the container aware dispatcher will register our listener. Explain upcoming video.
Here is where we are now. More classes, but they are silod. Easier to modifiy and extend.

Why is this good?

  • Single Responsibility Principle
  • Open / Closed
  • Decoupled Modular Code
  • Testable
Classes are decoupled, more flexible. Open for extension, closed for modification. We no longer need to modify the form class to change what is happening after the submission.

Why is this better than hooks?

  • Control
  • External Components
We saw the example of weight / order. This is a lot nicer that module implements alters. We also have even more control (chaos?) by stopping events.

Stopping an event

$event->stopPropagation()
Sometimes, when a event can modify the data associated to it, it is a good idea to stop the event. This is the chain of responsibiliy - go through the observers until we find an object that is responsible for this situation.

What is Core doing...

  • ConfigEvents::DELETE, IMPORT, SAVE..
  • EntityTypeEvents::CREATE, UPDATE, DELETE..
  • KernelEvents::REQUEST, RESPONSE, TERMINATE..
  • RoutingEvents::ALTER, DYNAMIC, ETC..
Not too many custom events, lets look at the kernel events more closely.

Kernel Events

  • kernel.request
  • kernel.controller
  • kernel.view
  • kernel.response
  • kernel.exception
  • kernel.terminate
gets a request. executes a controller. converts controller results to a response. returns a response object. Explain upcoming video on handling a 404 exception.

kernel.exception

This event allows you to create a response for a thrown exception or to modify the thrown exception. The event listener method receives a ...\GetResponseForExceptionEvent instance.

Fast 404 listener

Fast 404 listener

$request = $event->getRequest();

$config = $this->configFactory->get('system.performance');
$exclude_paths = $config->get('fast_404.exclude_paths');
if ($config->get('fast_404.enabled') && $exclude_paths && !preg_match($exclude_paths, $request->getPathInfo())) {
  $fast_paths = $config->get('fast_404.paths');
    if ($fast_paths && preg_match($fast_paths, $request->getPathInfo())) {
      $fast_404_html = strtr($config->get('fast_404.html'), ['@path' => Html::escape($request->getUri())]);
      $response = new Response($fast_404_html, Response::HTTP_NOT_FOUND);
      $event->setResponse($response);
    }
  }
}

The Future

  • Pre / Post events for all the things... Maybe?

Rules module

  • Works with events
  • Adds events for many core hooks
/**
 * Implements hook_user_login().
 */
function rules_user_login($account) {
  // Set the account twice on the event: as the main subject but also in the
  // list of arguments.
  $event = new UserLoginEvent($account);
  $event_dispatcher = \Drupal::service('event_dispatcher');
  $event_dispatcher->dispatch(UserLoginEvent::EVENT_NAME, $event);
}

Questions?

Eric Smith / @ericgsmith / @cameronwilding

Drupal 8 and the Symfony Event Dispatcher Eric Smith / @ericgsmith / @cameronwilding http://ericgsmith.github.io/eventdispatcher Adventure time!