matters of state
Post on 12-Jan-2017
9.203 Views
Preview:
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