matters of state
TRANSCRIPT
![Page 1: Matters of State](https://reader031.vdocuments.net/reader031/viewer/2022030304/5876b4f41a28abad1a8b579f/html5/thumbnails/1.jpg)
Matters of StateUsing Symfony’s Event Dispatcher to
turn your application into a simple state machine
![Page 2: Matters of State](https://reader031.vdocuments.net/reader031/viewer/2022030304/5876b4f41a28abad1a8b579f/html5/thumbnails/2.jpg)
Photo by jeff_golden
Bon anniversaire Symfony!
![Page 3: Matters of State](https://reader031.vdocuments.net/reader031/viewer/2022030304/5876b4f41a28abad1a8b579f/html5/thumbnails/3.jpg)
About me
• Portland
• twitter.com/kriswallsmith
• github.com/kriswallsmith
• kriswallsmith.net
![Page 4: Matters of State](https://reader031.vdocuments.net/reader031/viewer/2022030304/5876b4f41a28abad1a8b579f/html5/thumbnails/4.jpg)
![Page 5: Matters of State](https://reader031.vdocuments.net/reader031/viewer/2022030304/5876b4f41a28abad1a8b579f/html5/thumbnails/5.jpg)
React
Nicolas Raymond
![Page 6: Matters of State](https://reader031.vdocuments.net/reader031/viewer/2022030304/5876b4f41a28abad1a8b579f/html5/thumbnails/6.jpg)
![Page 7: Matters of State](https://reader031.vdocuments.net/reader031/viewer/2022030304/5876b4f41a28abad1a8b579f/html5/thumbnails/7.jpg)
var HelloWorld = React.createClass({ render: function() { return ( <div>Hello World</div> ) } })
![Page 8: Matters of State](https://reader031.vdocuments.net/reader031/viewer/2022030304/5876b4f41a28abad1a8b579f/html5/thumbnails/8.jpg)
var HelloWorld = React.createClass({ render: function() { return ( <div>Hello World</div> ) } })
JSX
![Page 9: Matters of State](https://reader031.vdocuments.net/reader031/viewer/2022030304/5876b4f41a28abad1a8b579f/html5/thumbnails/9.jpg)
Just the view
![Page 10: Matters of State](https://reader031.vdocuments.net/reader031/viewer/2022030304/5876b4f41a28abad1a8b579f/html5/thumbnails/10.jpg)
(there is no MVC to violate)
![Page 11: Matters of State](https://reader031.vdocuments.net/reader031/viewer/2022030304/5876b4f41a28abad1a8b579f/html5/thumbnails/11.jpg)
One-way data flow
![Page 12: Matters of State](https://reader031.vdocuments.net/reader031/viewer/2022030304/5876b4f41a28abad1a8b579f/html5/thumbnails/12.jpg)
(everything flows from state)
![Page 13: Matters of State](https://reader031.vdocuments.net/reader031/viewer/2022030304/5876b4f41a28abad1a8b579f/html5/thumbnails/13.jpg)
Virtual DOM
![Page 14: Matters of State](https://reader031.vdocuments.net/reader031/viewer/2022030304/5876b4f41a28abad1a8b579f/html5/thumbnails/14.jpg)
(DOM changesets)
![Page 15: Matters of State](https://reader031.vdocuments.net/reader031/viewer/2022030304/5876b4f41a28abad1a8b579f/html5/thumbnails/15.jpg)
“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
![Page 16: Matters of State](https://reader031.vdocuments.net/reader031/viewer/2022030304/5876b4f41a28abad1a8b579f/html5/thumbnails/16.jpg)
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> ) } })
![Page 17: Matters of State](https://reader031.vdocuments.net/reader031/viewer/2022030304/5876b4f41a28abad1a8b579f/html5/thumbnails/17.jpg)
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> ) } })
![Page 18: Matters of State](https://reader031.vdocuments.net/reader031/viewer/2022030304/5876b4f41a28abad1a8b579f/html5/thumbnails/18.jpg)
<button id="click_button"> 0 click(s) </button>
![Page 19: Matters of State](https://reader031.vdocuments.net/reader031/viewer/2022030304/5876b4f41a28abad1a8b579f/html5/thumbnails/19.jpg)
$('#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);})
![Page 20: Matters of State](https://reader031.vdocuments.net/reader031/viewer/2022030304/5876b4f41a28abad1a8b579f/html5/thumbnails/20.jpg)
$('#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…
![Page 21: Matters of State](https://reader031.vdocuments.net/reader031/viewer/2022030304/5876b4f41a28abad1a8b579f/html5/thumbnails/21.jpg)
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> ) } })
![Page 22: Matters of State](https://reader031.vdocuments.net/reader031/viewer/2022030304/5876b4f41a28abad1a8b579f/html5/thumbnails/22.jpg)
Paradigm shift
![Page 23: Matters of State](https://reader031.vdocuments.net/reader031/viewer/2022030304/5876b4f41a28abad1a8b579f/html5/thumbnails/23.jpg)
Imperative vs. DeclarativePhoto by
![Page 24: Matters of State](https://reader031.vdocuments.net/reader031/viewer/2022030304/5876b4f41a28abad1a8b579f/html5/thumbnails/24.jpg)
Imperative programming
![Page 25: Matters of State](https://reader031.vdocuments.net/reader031/viewer/2022030304/5876b4f41a28abad1a8b579f/html5/thumbnails/25.jpg)
(code that explains how to reach your goal)
![Page 26: Matters of State](https://reader031.vdocuments.net/reader031/viewer/2022030304/5876b4f41a28abad1a8b579f/html5/thumbnails/26.jpg)
Declarative programming
![Page 27: Matters of State](https://reader031.vdocuments.net/reader031/viewer/2022030304/5876b4f41a28abad1a8b579f/html5/thumbnails/27.jpg)
(code that describes what your goal is)
![Page 28: Matters of State](https://reader031.vdocuments.net/reader031/viewer/2022030304/5876b4f41a28abad1a8b579f/html5/thumbnails/28.jpg)
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
![Page 29: Matters of State](https://reader031.vdocuments.net/reader031/viewer/2022030304/5876b4f41a28abad1a8b579f/html5/thumbnails/29.jpg)
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
![Page 30: Matters of State](https://reader031.vdocuments.net/reader031/viewer/2022030304/5876b4f41a28abad1a8b579f/html5/thumbnails/30.jpg)
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
![Page 31: Matters of State](https://reader031.vdocuments.net/reader031/viewer/2022030304/5876b4f41a28abad1a8b579f/html5/thumbnails/31.jpg)
$('#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);})
![Page 32: Matters of State](https://reader031.vdocuments.net/reader031/viewer/2022030304/5876b4f41a28abad1a8b579f/html5/thumbnails/32.jpg)
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> ) } })
![Page 33: Matters of State](https://reader031.vdocuments.net/reader031/viewer/2022030304/5876b4f41a28abad1a8b579f/html5/thumbnails/33.jpg)
How vs. What
![Page 34: Matters of State](https://reader031.vdocuments.net/reader031/viewer/2022030304/5876b4f41a28abad1a8b579f/html5/thumbnails/34.jpg)
SELECT * FROM users WHERE username="kriswallsmith";
![Page 35: Matters of State](https://reader031.vdocuments.net/reader031/viewer/2022030304/5876b4f41a28abad1a8b579f/html5/thumbnails/35.jpg)
PHP controllers
![Page 36: Matters of State](https://reader031.vdocuments.net/reader031/viewer/2022030304/5876b4f41a28abad1a8b579f/html5/thumbnails/36.jpg)
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()] );}
![Page 37: Matters of State](https://reader031.vdocuments.net/reader031/viewer/2022030304/5876b4f41a28abad1a8b579f/html5/thumbnails/37.jpg)
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()] );}
![Page 38: Matters of State](https://reader031.vdocuments.net/reader031/viewer/2022030304/5876b4f41a28abad1a8b579f/html5/thumbnails/38.jpg)
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!
![Page 39: Matters of State](https://reader031.vdocuments.net/reader031/viewer/2022030304/5876b4f41a28abad1a8b579f/html5/thumbnails/39.jpg)
Why stop there?
![Page 40: Matters of State](https://reader031.vdocuments.net/reader031/viewer/2022030304/5876b4f41a28abad1a8b579f/html5/thumbnails/40.jpg)
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()] );}
![Page 41: Matters of State](https://reader031.vdocuments.net/reader031/viewer/2022030304/5876b4f41a28abad1a8b579f/html5/thumbnails/41.jpg)
Excuse me Kris, but what about the email?
![Page 42: Matters of State](https://reader031.vdocuments.net/reader031/viewer/2022030304/5876b4f41a28abad1a8b579f/html5/thumbnails/42.jpg)
Photo by
![Page 43: Matters of State](https://reader031.vdocuments.net/reader031/viewer/2022030304/5876b4f41a28abad1a8b579f/html5/thumbnails/43.jpg)
Event Dispatcher
Photo by rhodesj
![Page 44: Matters of State](https://reader031.vdocuments.net/reader031/viewer/2022030304/5876b4f41a28abad1a8b579f/html5/thumbnails/44.jpg)
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
![Page 45: Matters of State](https://reader031.vdocuments.net/reader031/viewer/2022030304/5876b4f41a28abad1a8b579f/html5/thumbnails/45.jpg)
public function addListener( $eventName, $listener, $priority = 0);
public function dispatch( $eventName, Event $event = null);
![Page 46: Matters of State](https://reader031.vdocuments.net/reader031/viewer/2022030304/5876b4f41a28abad1a8b579f/html5/thumbnails/46.jpg)
$dispatcher->dispatch('user.create');
![Page 47: Matters of State](https://reader031.vdocuments.net/reader031/viewer/2022030304/5876b4f41a28abad1a8b579f/html5/thumbnails/47.jpg)
$dispatcher->dispatch( 'user.create', new UserEvent($user));
![Page 48: Matters of State](https://reader031.vdocuments.net/reader031/viewer/2022030304/5876b4f41a28abad1a8b579f/html5/thumbnails/48.jpg)
$dispatcher->dispatch( UserEvents::CREATE, new UserEvent($user));
![Page 49: Matters of State](https://reader031.vdocuments.net/reader031/viewer/2022030304/5876b4f41a28abad1a8b579f/html5/thumbnails/49.jpg)
$dispatcher->addListener( UserEvents::CREATE, function(UserEvent $event) { echo "Welcome, {$event->getUserName()}!"; });
![Page 50: Matters of State](https://reader031.vdocuments.net/reader031/viewer/2022030304/5876b4f41a28abad1a8b579f/html5/thumbnails/50.jpg)
$dispatcher->addListener( UserEvents::CREATE, [$listener, 'onUserCreate']);
![Page 51: Matters of State](https://reader031.vdocuments.net/reader031/viewer/2022030304/5876b4f41a28abad1a8b579f/html5/thumbnails/51.jpg)
Something just happened or is about to happen
![Page 52: Matters of State](https://reader031.vdocuments.net/reader031/viewer/2022030304/5876b4f41a28abad1a8b579f/html5/thumbnails/52.jpg)
Photo by Simon Law
Reactive Listeners
![Page 53: Matters of State](https://reader031.vdocuments.net/reader031/viewer/2022030304/5876b4f41a28abad1a8b579f/html5/thumbnails/53.jpg)
class UserListener{ public function onUserCreate(UserEvent $event) { $user = $event->getUser();
$this->mailer->sendWelcomeEmail($user); }}
![Page 54: Matters of State](https://reader031.vdocuments.net/reader031/viewer/2022030304/5876b4f41a28abad1a8b579f/html5/thumbnails/54.jpg)
class ActivityFeedListener{ public function onUserCreate(UserEvent $event) { $user = $event->getUser();
$this->feed->logRegistration($user); }}
![Page 55: Matters of State](https://reader031.vdocuments.net/reader031/viewer/2022030304/5876b4f41a28abad1a8b579f/html5/thumbnails/55.jpg)
class ActivityFeedListener{ public function onFollow(UserUserEvent $event) { $user = $event->getUser(); $otherUser = $event->getOtherUser();
$this->feed->logFollow($user, $otherUser); }}
![Page 56: Matters of State](https://reader031.vdocuments.net/reader031/viewer/2022030304/5876b4f41a28abad1a8b579f/html5/thumbnails/56.jpg)
Testable
![Page 57: Matters of State](https://reader031.vdocuments.net/reader031/viewer/2022030304/5876b4f41a28abad1a8b579f/html5/thumbnails/57.jpg)
Reusable
![Page 58: Matters of State](https://reader031.vdocuments.net/reader031/viewer/2022030304/5876b4f41a28abad1a8b579f/html5/thumbnails/58.jpg)
Readable
![Page 59: Matters of State](https://reader031.vdocuments.net/reader031/viewer/2022030304/5876b4f41a28abad1a8b579f/html5/thumbnails/59.jpg)
State Events
![Page 60: Matters of State](https://reader031.vdocuments.net/reader031/viewer/2022030304/5876b4f41a28abad1a8b579f/html5/thumbnails/60.jpg)
Where do they come from?
![Page 61: Matters of State](https://reader031.vdocuments.net/reader031/viewer/2022030304/5876b4f41a28abad1a8b579f/html5/thumbnails/61.jpg)
Option 1: Controller dispatch
![Page 62: Matters of State](https://reader031.vdocuments.net/reader031/viewer/2022030304/5876b4f41a28abad1a8b579f/html5/thumbnails/62.jpg)
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()] );}
![Page 63: Matters of State](https://reader031.vdocuments.net/reader031/viewer/2022030304/5876b4f41a28abad1a8b579f/html5/thumbnails/63.jpg)
Option 2: Service dispatch
![Page 64: Matters of State](https://reader031.vdocuments.net/reader031/viewer/2022030304/5876b4f41a28abad1a8b579f/html5/thumbnails/64.jpg)
class UserManager{ public function persistUser(User $user) { $this->em->persist($user);
$this->dispatcher->dispatch( UserEvents::CREATE, new UserEvent($user) ); }}
![Page 65: Matters of State](https://reader031.vdocuments.net/reader031/viewer/2022030304/5876b4f41a28abad1a8b579f/html5/thumbnails/65.jpg)
Option 3: Flush dispatch
![Page 66: Matters of State](https://reader031.vdocuments.net/reader031/viewer/2022030304/5876b4f41a28abad1a8b579f/html5/thumbnails/66.jpg)
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) ); } } } } }}
![Page 67: Matters of State](https://reader031.vdocuments.net/reader031/viewer/2022030304/5876b4f41a28abad1a8b579f/html5/thumbnails/67.jpg)
a simple state machine
![Page 68: Matters of State](https://reader031.vdocuments.net/reader031/viewer/2022030304/5876b4f41a28abad1a8b579f/html5/thumbnails/68.jpg)
Cool, but…
![Page 69: Matters of State](https://reader031.vdocuments.net/reader031/viewer/2022030304/5876b4f41a28abad1a8b579f/html5/thumbnails/69.jpg)
That flush listener is ugly…
![Page 70: Matters of State](https://reader031.vdocuments.net/reader031/viewer/2022030304/5876b4f41a28abad1a8b579f/html5/thumbnails/70.jpg)
…and imperative!
![Page 71: Matters of State](https://reader031.vdocuments.net/reader031/viewer/2022030304/5876b4f41a28abad1a8b579f/html5/thumbnails/71.jpg)
What if…
Photo by Jared Cherup
What if…
![Page 72: Matters of State](https://reader031.vdocuments.net/reader031/viewer/2022030304/5876b4f41a28abad1a8b579f/html5/thumbnails/72.jpg)
/** @On\Create(UserEvents::CREATE) */class User{}
![Page 73: Matters of State](https://reader031.vdocuments.net/reader031/viewer/2022030304/5876b4f41a28abad1a8b579f/html5/thumbnails/73.jpg)
/** @On\Change(UserEvents::CHANGE) */class User{}
![Page 74: Matters of State](https://reader031.vdocuments.net/reader031/viewer/2022030304/5876b4f41a28abad1a8b579f/html5/thumbnails/74.jpg)
/** @On\Destroy(UserEvents::DESTROY) */class User{}
![Page 75: Matters of State](https://reader031.vdocuments.net/reader031/viewer/2022030304/5876b4f41a28abad1a8b579f/html5/thumbnails/75.jpg)
/** * @On\Create(UserEvents::CREATE) * @On\Update(UserEvents::UPDATE) * @On\Destroy(UserEvents::DESTROY) */class User{}
![Page 76: Matters of State](https://reader031.vdocuments.net/reader031/viewer/2022030304/5876b4f41a28abad1a8b579f/html5/thumbnails/76.jpg)
Property events
![Page 77: Matters of State](https://reader031.vdocuments.net/reader031/viewer/2022030304/5876b4f41a28abad1a8b579f/html5/thumbnails/77.jpg)
class User{ /** @On\Change(UserEvents::USERNAME_CHANGE) */ private $username;}
![Page 78: Matters of State](https://reader031.vdocuments.net/reader031/viewer/2022030304/5876b4f41a28abad1a8b579f/html5/thumbnails/78.jpg)
class User{ /** * @On\Add(UserEvents::FAVORITE) * @On\Remove(UserEvents::UNFAVORITE) */ private $favorites;}
![Page 79: Matters of State](https://reader031.vdocuments.net/reader031/viewer/2022030304/5876b4f41a28abad1a8b579f/html5/thumbnails/79.jpg)
Conditional events
![Page 80: Matters of State](https://reader031.vdocuments.net/reader031/viewer/2022030304/5876b4f41a28abad1a8b579f/html5/thumbnails/80.jpg)
class User{ /** * @On\Add( * UserEvents::FIRST_FAVORITE, * unless="old_value()" * ) */ private $favorites;}
![Page 81: Matters of State](https://reader031.vdocuments.net/reader031/viewer/2022030304/5876b4f41a28abad1a8b579f/html5/thumbnails/81.jpg)
Custom event objects
![Page 82: Matters of State](https://reader031.vdocuments.net/reader031/viewer/2022030304/5876b4f41a28abad1a8b579f/html5/thumbnails/82.jpg)
class User{ /** * @On\Add( * UserEvents::FOLLOW, * class=UserUserEvent::class, * arguments="[object, added_value()]" * ) */ private $follows;}
![Page 83: Matters of State](https://reader031.vdocuments.net/reader031/viewer/2022030304/5876b4f41a28abad1a8b579f/html5/thumbnails/83.jpg)
Event dependencies
![Page 84: Matters of State](https://reader031.vdocuments.net/reader031/viewer/2022030304/5876b4f41a28abad1a8b579f/html5/thumbnails/84.jpg)
class User{ /** * @On\Add( * UserEvents::FOLLOW, * before=ActivityEvents::CREATE * ) */ private $follows;}
![Page 85: Matters of State](https://reader031.vdocuments.net/reader031/viewer/2022030304/5876b4f41a28abad1a8b579f/html5/thumbnails/85.jpg)
kriswallsmith/state-events
![Page 86: Matters of State](https://reader031.vdocuments.net/reader031/viewer/2022030304/5876b4f41a28abad1a8b579f/html5/thumbnails/86.jpg)
(coming soon)
![Page 87: Matters of State](https://reader031.vdocuments.net/reader031/viewer/2022030304/5876b4f41a28abad1a8b579f/html5/thumbnails/87.jpg)
Merci!