matters of state

Post on 12-Jan-2017

9.203 Views

Category:

Internet

0 Downloads

Preview:

Click to see full reader

TRANSCRIPT

Matters of StateUsing Symfony’s Event Dispatcher to

turn your application into a simple state machine

Photo by jeff_golden

Bon anniversaire Symfony!

About me

• Portland

• twitter.com/kriswallsmith

• github.com/kriswallsmith

• kriswallsmith.net

React

Nicolas Raymond

var HelloWorld = React.createClass({ render: function() { return ( <div>Hello World</div> ) } })

var HelloWorld = React.createClass({ render: function() { return ( <div>Hello World</div> ) } })

JSX

Just the view

(there is no MVC to violate)

One-way data flow

(everything flows from state)

Virtual DOM

(DOM changesets)

“React thinks of UIs as simple state machines. By thinking of a UI as being in various states and rendering

those states, it's easy to keep your UI consistent.”

https://facebook.github.io/react/docs/interactivity-and-dynamic-uis.html#components-are-just-state-machines

var Analytics = React.createClass({ getInitialState: function() { return { clicks: 0 } },

incrementClicks: function() { var clicks = this.state.clicks; this.setState({ clicks: ++clicks }); },

render: function() { var clicks = this.state.clicks; return ( <button onClick={this.incrementClicks}> {clicks} click{clicks == 1 ? '' : 's'} </button> ) } })

var Analytics = React.createClass({ getInitialState: function() { return { clicks: 0 } },

incrementClicks: function() { var clicks = this.state.clicks; this.setState({ clicks: ++clicks }); },

render: function() { var clicks = this.state.clicks; return ( <button onClick={this.incrementClicks}> {clicks} click{clicks == 1 ? '' : 's'} </button> ) } })

<button id="click_button"> 0 click(s) </button>

$('#my_button').click(function() { var $btn = $(this); var clicks = $btn.text().match(/\d+/)[0]; var text = clicks + ' click'; if (clicks != 1) text += 's'; $btn.text(text);})

$('#my_button').click(function() { var $btn = $(this); var clicks = $btn.text().match(/\d+/)[0]; var text = clicks + ' click'; if (clicks != 1) text += 's'; $btn.text(text);})

oops…

var Analytics = React.createClass({ getInitialState: function() { return { clicks: 0 } },

incrementClicks: function() { var clicks = this.state.clicks; this.setState({ clicks: ++clicks }); },

render: function() { var clicks = this.state.clicks; return ( <button onClick={this.incrementClicks}> {clicks} click{clicks == 1 ? '' : 's'} </button> ) } })

Paradigm shift

Imperative vs. DeclarativePhoto by

Imperative programming

(code that explains how to reach your goal)

Declarative programming

(code that describes what your goal is)

Take the next right• Depress the gas pedal and increase your speed to 10mph

• Engage your right turn signal

• As you approach the intersection, remove your foot from the gas pedal and gently apply the brake

• Check for pedestrians crossing the intersection

• Check your mirrors and blind spot for bicyclists

• Lift your foot from the brake and slowly increase speed by depressing the gas pedal

• Turn the steering wheel to the right

Take the next right• Depress the gas pedal and increase your speed to 10mph

• Engage your right turn signal

• As you approach the intersection, remove your foot from the gas pedal and gently apply the brake

• Check for pedestrians crossing the intersection

• Check your mirrors and blind spot for bicyclists

• Lift your foot from the brake and slowly increase speed by depressing the gas pedal

• Turn the steering wheel to the right

declarative

Take the next right• Depress the gas pedal and increase your speed to 10mph

• Engage your right turn signal

• As you approach the intersection, remove your foot from the gas pedal and gently apply the brake

• Check for pedestrians crossing the intersection

• Check your mirrors and blind spot for bicyclists

• Lift your foot from the brake and slowly increase speed by depressing the gas pedal

• Turn the steering wheel to the right

imperative

$('#my_button').click(function() { var $btn = $(this); var clicks = $btn.text().match(/\d+/)[0]; var text = clicks + ' click'; if (clicks != 1) text += 's'; $btn.text(text);})

var Analytics = React.createClass({ getInitialState: function() { return { clicks: 0 } },

incrementClicks: function() { var clicks = this.state.clicks; this.setState({ clicks: ++clicks }); },

render: function() { var clicks = this.state.clicks; return ( <button onClick={this.incrementClicks}> {clicks} click{clicks == 1 ? '' : 's'} </button> ) } })

How vs. What

SELECT * FROM users WHERE username="kriswallsmith";

PHP controllers

public function registerAction(Request $request){ $form = $this->createForm('user'); $form->handleRequest($request);

if ($form->isValid()) { $user = $form->getData();

$message = \Swift_Message::newInstance() ->setTo($user->getEmail()) ->setSubject('Welcome!') ->setBody($this->renderView( 'AppBundle:Email:welcome.html.twig', ['user' => $user] ));

$mailer = $this->get('mailer'); $mailer->send($message);

$em = $this->get('doctrine')->getManager(); $em->persist($user); $em->flush();

return $this->redirectToRoute('home'); }

return $this->render( 'AppBundle:User:register.html.twig', ['form' => $form->createView()] );}

public function registerAction(Request $request){ $form = $this->createForm('user'); $form->handleRequest($request);

if ($form->isValid()) { $user = $form->getData();

$mailer = $this->get('app.mailer'); $mailer->sendWelcomeEmail($user);

$em = $this->get('doctrine')->getManager(); $em->persist($user); $em->flush();

return $this->redirectToRoute('home'); }

return $this->render( 'AppBundle:User:register.html.twig', ['form' => $form->createView()] );}

public function registerAction(Request $request){ $form = $this->createForm('user'); $form->handleRequest($request);

if ($form->isValid()) { $user = $form->getData();

$mailer = $this->get('app.mailer'); $mailer->sendWelcomeEmail($user);

$em = $this->get('doctrine')->getManager(); $em->persist($user); $em->flush();

return $this->redirectToRoute('home'); }

return $this->render( 'AppBundle:User:register.html.twig', ['form' => $form->createView()] );}

declarative!

Why stop there?

public function registerAction(Request $request){ $form = $this->createForm('user'); $form->handleRequest($request);

if ($form->isValid()) { $em = $this->get('doctrine')->getManager(); $em->persist($form->getData()); $em->flush();

return $this->redirectToRoute('home'); }

return $this->render( 'AppBundle:User:register.html.twig', ['form' => $form->createView()] );}

Excuse me Kris, but what about the email?

Photo by

Event Dispatcher

Photo by rhodesj

The Event Dispatcher component provides tools that allow your application components

to communicate with each other by dispatching events and listening to them.

symfony.com

public function addListener( $eventName, $listener, $priority = 0);

public function dispatch( $eventName, Event $event = null);

$dispatcher->dispatch('user.create');

$dispatcher->dispatch( 'user.create', new UserEvent($user));

$dispatcher->dispatch( UserEvents::CREATE, new UserEvent($user));

$dispatcher->addListener( UserEvents::CREATE, function(UserEvent $event) { echo "Welcome, {$event->getUserName()}!"; });

$dispatcher->addListener( UserEvents::CREATE, [$listener, 'onUserCreate']);

Something just happened or is about to happen

Photo by Simon Law

Reactive Listeners

class UserListener{ public function onUserCreate(UserEvent $event) { $user = $event->getUser();

$this->mailer->sendWelcomeEmail($user); }}

class ActivityFeedListener{ public function onUserCreate(UserEvent $event) { $user = $event->getUser();

$this->feed->logRegistration($user); }}

class ActivityFeedListener{ public function onFollow(UserUserEvent $event) { $user = $event->getUser(); $otherUser = $event->getOtherUser();

$this->feed->logFollow($user, $otherUser); }}

Testable

Reusable

Readable

State Events

Where do they come from?

Option 1: Controller dispatch

public function registerAction(Request $request){ $form = $this->createForm('user'); $form->handleRequest($request);

if ($form->isValid()) { $user = $form->getData(); $dispatcher = $this->get('event_dispatcher'); $dispatcher->dispatch(UserEvents::CREATE, new UserEvent($user)); $em = $this->get('doctrine')->getManager(); $em->persist(); $em->flush(); return $this->redirectToRoute('home'); } return $this->render( 'AppBundle:Register:register.html.twig', ['form' => $form->createView()] );}

Option 2: Service dispatch

class UserManager{ public function persistUser(User $user) { $this->em->persist($user);

$this->dispatcher->dispatch( UserEvents::CREATE, new UserEvent($user) ); }}

Option 3: Flush dispatch

class FlushListener{ public function preFlush(PreFlushEventArgs $event) { $em = $event->getEntityManager(); $uow = $em->getUnitOfWork();

foreach ($uow->getIdentityMap() as $class => $objects) { if (User::class === $class) { foreach ($objects as $user) { if ($uow->isScheduledForInsert($user)) { $this->dispatcher->dispatch( UserEvents::CREATE, new UserEvent($user) ); } } } } }}

a simple state machine

Cool, but…

That flush listener is ugly…

…and imperative!

What if…

Photo by Jared Cherup

What if…

/** @On\Create(UserEvents::CREATE) */class User{}

/** @On\Change(UserEvents::CHANGE) */class User{}

/** @On\Destroy(UserEvents::DESTROY) */class User{}

/** * @On\Create(UserEvents::CREATE) * @On\Update(UserEvents::UPDATE) * @On\Destroy(UserEvents::DESTROY) */class User{}

Property events

class User{ /** @On\Change(UserEvents::USERNAME_CHANGE) */ private $username;}

class User{ /** * @On\Add(UserEvents::FAVORITE) * @On\Remove(UserEvents::UNFAVORITE) */ private $favorites;}

Conditional events

class User{ /** * @On\Add( * UserEvents::FIRST_FAVORITE, * unless="old_value()" * ) */ private $favorites;}

Custom event objects

class User{ /** * @On\Add( * UserEvents::FOLLOW, * class=UserUserEvent::class, * arguments="[object, added_value()]" * ) */ private $follows;}

Event dependencies

class User{ /** * @On\Add( * UserEvents::FOLLOW, * before=ActivityEvents::CREATE * ) */ private $follows;}

kriswallsmith/state-events

(coming soon)

Merci!

top related