Track: PHP Feedback: /session/series-fortunate-events Twitter: @matthiasnoback Matthias Noback A Series of Fortunate Events
Nov 27, 2014
Track: PHPFeedback: /session/series-fortunate-events
Twitter: @matthiasnoback
Matthias Noback
A Series of Fortunate Events
What are events, really?
Things that happen
They trigger actions
Just now...Attendees arrived,
triggered me to turn on microphone,
which triggered you to stop talking,
which triggered me to start talking
Events in software Events model what happened in a system
Other parts of the system can respond to what happened
Imperative programmingOnly commands
doThis();
doThat();
updateSomething($something);
return $something;
Extracting eventsdoThis();// this was done
doThat();// that was done
updateSomething($something)// something was updated
return $something;
Starting positionclass PostService{ ... function addComment($postId, $comment) { $post = $this>fetchPost($postId); $post>addComment($comment); $this>save($post);
$this>logger>info('New comment'); $this>mailer>send('New comment'); }}
Starting positionclass PostService { function __construct( Mailer $mailer, Logger $logger ) { $this>mailer = $mailer; $this>logger = $logger; }
function addComment($postId, $comment) { ... }}
Making events explicitclass PostService { function addComment($postId, $comment) { ... $this>newCommentAdded(); }
function newCommentAdded() { $this>logger>info('New comment'); $this>mailer>send('New comment'); }}
Dependency graph
PostServicePostService
Mailer
Logger
Design issues (1)I don't think the PostService should know how to use a Mailer and a Logger
Design issues (2)I want to change the behavior of PostService without modifying the class itself
Fix the problems
By introducing events!(later)
Observer patternNotify other parts of the application when a change occurs
class PostService { function newCommentAdded() { foreach ($this>observers as $observer) { $observer>notify(); } }}
Observer contract
interface Observer{ function notify();}
Subject knows nothing about its observers, except their very simple interface
Concrete observersclass LoggingObserver implements Observer{ function __construct(Logger $logger) { $this>logger = $logger; }
function notify() { $this>logger>info('New comment'); }}
Concrete observers
class NotificationMailObserver implements Observer{ function __construct(Mailer $mailer) { $this>mailer = $mailer; }
function notify() { $this>mailer>send('New comment'); }}
Configurationclass PostService{ function __construct(array $observers) { $this>observers = $observers; }}
$postService = new PostService( array( new LoggingObserver($logger), new NotificationMailObserver($mailer) ));
Before
PostServicePostService
Mailer
Logger
After
NotificationMailObserver
Observer
Observer
LoggingObserver
Mailer
Logger
PostService
Design Principles Party
Single responsibilityEach class has one small,
well-defined responsibility
Single responsibility● PostService:
“add comments to posts”
● LoggingObserver: “write a line to the log”
● NotificationMailObserver: “send a notification mail”
Single responsibilityWhen a change is required, it can be isolated to just a small part of the application
Single responsibility● “Capitalize the comment!”: PostService
● “Use a different logger!”: LoggerObserver
● “Add a timestamp to the notification mail!”: NotificationMailObserver
Dependency inversionDepend on abstractions, not on concretions
Dependency inversionFirst PostService depended on something concrete: the Mailer, the Logger.
Mailer
LoggerPostService
Dependency inversionNow it depends on something abstract: an Observer
Observer
Observer
PostService
Dependency inversionOnly the concrete observers depend on concrete things like Mailer and Logger
NotificationMailObserver
LoggingObserver
Mailer
Logger
Open/closedA class should be open for extension and closed for modification
Open/closedYou don't need to modify the class to change its behavior
Observer
Observer Observer
PostService
Open/closedWe made it closed for modification,
open for extension
Event data
Mr. Boddy was murdered!● By Mrs. Peacock● In the dining room● With a candle stick
Currently missing!
class LogNewCommentObserver implements Observer{ function notify() { // we'd like to be more specific $this>logger>info('New comment'); }}
Event objectclass CommentAddedEvent { public function __construct($postId, $comment) { $this>postId = $postId; $this>comment = $comment; }
function comment() { return $this>comment; }
function postId() { return $this>postId; }}
Event object
We use the event object to store the context of the event
From observer...
interface Observer{ function notify();}
… to event handler
interface CommentAddedEventHandler{ function handle(CommentAddedEvent $event);}
Event handlersclass LoggingEventHandler implements CommentAddedEventHandler{ function __construct(Logger $logger) { $this>logger = $logger; }
public function handle(CommentAddedEvent $event) { $this>logger>info( 'New comment' . $event>comment() ); }}
Event handlers
class NotificationMailEventHandler implements CommentAddedEventHandler{ function __construct(Mailer $mailer) { $this>mailer = $mailer; }
public function handle(CommentAddedEvent $event) { $this>mailer>send( 'New comment: ' . $event>comment(); ); }}
Configuration
class PostService{ function __construct(array $eventHandlers) { $this>eventHandlers = $eventHandlers; }}
$postService = new PostService( array( new LoggingEventHandler($logger), new NotificationMailEventHandler($mailer) ));
Looping over event handlersclass PostService{ public function addComment($postId, $comment) { $this>newCommentAdded($postId, $comment); }
function newCommentAdded($postId, $comment) { $event = new CommentAddedEvent( $postId, $comment );
foreach ($this>eventHandlers as $eventHandler) { $eventHandler>handle($event); } }}
Introducing a MediatorInstead of talking to the event handlers
Let's leave the talking to a mediator
Mediators for events● Doctrine, Zend: Event manager
● The PHP League: Event emitter
● Symfony: Event dispatcher
Before
LoggingEventHandler::handle()
NotificationMailEventHandler::handle()
PostService
After
LoggingEventHandler::handle()
NotificationMailEventHandler::handle()
EventDispatcherPostService
In codeclass PostService{ function __construct(EventDispatcherInterface $dispatcher) { $this>dispatcher = $dispatcher; }
function newCommentAdded($postId, $comment) { $event = new CommentAddedEvent($postId, $comment);
$this>dispatcher>dispatch( 'comment_added', $event ); }}
Event class
use Symfony\Component\EventDispatcher\Event;
class CommentAddedEvent extends Event{ ...}
Custom event classes should extend Symfony Event class:
Configurationuse Symfony\Component\EventDispatcher\Event;
$dispatcher = new EventDispatcher();
$loggingEventHandler = new LoggingEventHandler($logger);
$dispatcher>addListener( 'comment_added', array($loggingEventHandler, 'handle'));...
$postService = new PostService($dispatcher);
Symfony2 - and Drupal8!● An event dispatcher is available as the event_dispatcher service
● You can register event listeners using service tags
Inject the event dispatcher# yourmodulename.service.yml
services:
post_service:class: PostServicearguments: [@event_dispatcher]
Register your listeners# yourmodulename.service.yml
services: ...
logging_event_handler:class: LoggingEventHandlerarguments: [@logger]tags:
{ name: kernel.event_listenerevent: comment_addedmethod: handle
}
Events and application flowSymfony2 uses events to generate response for any given HTTP request
The HttpKernel$request = Request::createFromGlobals();
// $kernel is in an instance of HttpKernelInterface
$response = $kernel>handle($request);
$response>send();
Kernel events
kernel.request● Route matching
● Authentication
kernel.controller● Replace the controller
● Do some access checks
kernel.view● Render a template
kernel.response● Modify the response
● E.g. inject the Symfony toolbar
kernel.exception● Generate a response
● Render a nice page with the stack trace
Special types of events● Kernel events are not merely
notifications
● They allow other parts of the application to step in and modify or override behavior
Chain of responsibility
Handler 3Handler 1 Handler 2
Some sort of request
Some sort of request
Response
Some sort of request
Symfony example
Listener 3Listener 1 Listener 2
Exception! Exception!
Response
I've got an exception! What should I tell the user?
Propagationclass HandleExceptionListener{
function onKernelException(GetResponseForExceptionEvent $event
) {$event>setResponse(new Response('Error!'));
// this is the best response ever, don't let// other spoil it!
$event>stopPropagation();}
}
Priorities$dispatcher = new EventDispatcher();
$dispatcher>addListener( 'comment_added', array($object, $method), // priority 100);
Concerns
Concern 1: Hard to understand“Click-through understanding” impossible
$event = new CommentAddedEvent($postId, $comment);
$this>dispatcher>dispatch('comment_added',$event
);
interface EventDispatcherInterface{ function dispatch($eventName, Event $event = null);
...}
SolutionUse Xdebug
Concern 2: Out-of-domain concepts● “Comment”
● “PostId”
● “Add comment to post”
● “Dispatcher” (?!)
We did a good thing
We fixed coupling issues
She's called Cohesion
But this guy, Coupling, has a sister
Cohesion● Belonging together
● Concepts like “dispatcher”, “event listener”, even “event”, don't belong in your code
Solutions (1)Descriptive, explicit naming:
● NotificationMailEventListener becomes SendNotificationMailWhenCommentAdded
● CommentAddedEvent becomes CommentAdded
● onCommentAdded becomes whenCommentAdded
Solutions (1)
This also hides implementation details!
Solutions (2)Use an event dispatcher for things
that are not naturally cohesive anyway
Solutions (2)Use something else
when an event dispatcher causes low cohesion
Example: resolving the controller
$event = new GetResponseEvent($request);
$dispatcher>dispatch('kernel.request', $event);
$controller = $request>attributes>get('_controller');
$controller = $controllerResolver>resolve($request);
Concern 3: Loss of control● You rely on event listeners to do some really
important work● How do you know if they are in place
and do their job?
Solution● “Won't fix”
● You have to learn to live with it
It's good
Inversion of control
exercise control
give up control!
Just like...● A router determines the right controller
● The service container injects the right constructor arguments
● And when you die, someone will bury your body for you
Sometimes I'mterrified,
mortified,petrified,stupefied,
by inversion of control too
But it will● lead to better design
● require less change
● make maintenance easier
PresentationFinishedAskQuestionsWhenPresentationFinished
SayThankYouWhenNoMoreQuestions
Symfony, service definitions, kernel events
leanpub.com/a-year-with-symfony/c/drupalcon
Get a 30% discount!
Class and package design principles
leanpub.com/principles-of-php-package-design/c/drupalcon
Get a $10 discount!
Design patterns● Observer
● Mediator
● Chain of responsibility
● ...
Design Patterns by “The Gang of Four”
SOLID principles● Single responsibility
● Open/closed
● Dependency inversion
● ...
Agile Software Development by Robert C. Martin
Images● www.ohiseered.com/2011_11_01_archive.html
● Mrs. Peacock, Candlestick:www.cluecult.com
● Leonardo DiCaprio: screenrant.com/leonardo-dicaprio-defends-wolf-wall-street-controversy/
● Book covers:Amazon
● Party:todesignoffsite.com/events-2/to-do-closing-party-with-love-design/
● Russell Crowe: malinaelena.wordpress.com/2014/04/18/top-8-filme-cu-russell-crowe/
Twitter: @matthiasnoback
https://amsterdam2014.drupal.org/session/series-fortunate-events
What did you think?