Building an Open-Source Campaign Platform for the new President of France

E2ed7c278c8c49bb3e7fe0b7de039997?s=47 Hugo Hamon
November 05, 2017

Building an Open-Source Campaign Platform for the new President of France

This case study will take you to the technical backdrop of Emmanuel Macron's victorious campaign in the French presidential elections of 2017. We will reveal how the En-Marche and SensioLabs teams worked closely together to develop a collaborative, Open-Source and citizen platform with PHP 7 and Symfony to structure the movement. In addition to the technical and political issues involved in this project, we'll present solutions such as continuous deployment with Docker under Kubernetes, continuous integration with Circle CI, SCRUM and Kanban project management, use of Rabbitmq, Symfony 3 new features, and more. We'll demonstrate the importance of choosing these solutions to meet the agility and scalability needs of the highlights of the campaign.

E2ed7c278c8c49bb3e7fe0b7de039997?s=128

Hugo Hamon

November 05, 2017
Tweet

Transcript

  1. 1.

    Building an Open-Source Campaign Platform for the President of France

    . PHP CE 2017 / Nov. 5th / Ossa / Poland Hugo Hamon
  2. 3.

    Hugo Hamon Senior Software Developer 15 years of PHP experience

    10 years of Symfony experience Conferences speaker @hhamon on social networks Books (co) author
  3. 4.

    Titouan Galopin Enrolls to En-Marche movement Initiates the Symfony migration

    Full-Stack Developer Setup the new technical stack @titouangalopin
  4. 14.

    Socialists, liberals, center Many Political Parties Revolutionists, workers, conservatives Ecologists,

    hunters & fishermen, etc. Citizens loosing confidence in politicians Mostly led by aged politicians Lack of diversity & equity Established parties have lots of money
  5. 16.
  6. 18.

    +

  7. 19.
  8. 21.
  9. 24.

    Response Web Design Full featured Admin area Users management Fined

    grained permissions Http caching Images lazy loading Content Management Unit & functional tests Mass emails management Maps & address geocoding Social networks integration Procurations management Media management Field actions reports Legislatives elections support Logging Search engine Etc.
  10. 28.
  11. 29.
  12. 31.
  13. 33.
  14. 34.
  15. 35.

    trait EntityIdentityTrait { /** * @ORM\Id * @ORM\Column(type="integer") * @ORM\GeneratedValue

    */ private $id; /** * @ORM\Column(type="uuid") * @var Ramsey\Uuid\UuidInterface */ private $uuid; // ... + getters } Custom UUIDs for Doctrine entities.
  16. 36.

    class MembershipController extends Controller { // ... public function registerAction(Request

    $request): Response { $membership = MembershipRequest::createWithCaptcha(...); $form = $this ->createForm(MembershipRequestType::class, $membership) ->handleRequest($request); if ($form->isSubmitted() && $form->isValid()) { $this->get('app.membership')->handle($membership); return $this->redirectToRoute('app_membership_donate'); } return $this->render('...', [ 'form' => $form->createView(), // ... ]); } } Thin & dumb controllers.
  17. 37.

    src/Mailjet/ ├── ApiClient.php ├── ClientInterface.php ├── EmailTemplate.php ├── EmailTemplateFactory.php ├──

    Event/ │ ├── MailjetEvent.php │ └── MailjetEvents.php ├── EventSubscriber/ │ └── EmailPersisterEventSubscriber.php ├── Exception/ │ └── MailjetException.php ├── MailjetService.php ├── MailjetUtils.php ├── Message/ │ ├── AdherentAccountActivationMessage.php │ ├── AdherentAccountConfirmationMessage.php │ ├── ... │ └── TonMacronFriendMessage.php └── Transport/ ├── ApiTransport.php ├── RabbitMQTransport.php └── TransportInterface.php
  18. 38.

    class MailjetService { // ... public function sendMessage(MailjetMessage $message): bool

    { $email = $this->factory->createFromMailjetMessage($message); $event = new MailjetEvent($message, $email); try { $this->dispatch(MailjetEvents::DELIVERY_MESSAGE, $event); $this->transport->sendTemplateEmail($email); $this->dispatch(MailjetEvents::DELIVERY_SUCCESS, $event); } catch (MailjetException $e) { $this->dispatch( MailjetEvents::DELIVERY_ERROR, new MailjetEvent($message, $email, $e) ); return false; } return true; } }
  19. 39.

    class MembershipRequestHandler { // ... public function handle(MembershipRequest $membershipRequest) {

    $adherent = $this->factory->createFromMembershipRequest($membershipRequest); $token = AdherentActivationToken::generate($adherent); $this->manager->persist($adherent); $this->manager->persist($token); $this->manager->flush(); $message = AdherentAccountActivationMessage::createFromAdherent( $adherent, $this->generateMembershipActivationUrl($adherent, $token) ); $this->mailjet->sendMessage($message); $e = new AdherentAccountWasCreatedEvent($adherent); $this->dispatch(..., $e); } }
  20. 41.
  21. 42.

    class CommitteeManagementAuthority { // ... function followCommittee(Adherent $adherent, Committee $committee):

    void { $this->manager->followCommittee($adherent, $committee); if (!$hosts = $this->manager->getCommitteeHosts($committee)) { return; } $this->mailjet->sendMessage(CommitteeNewFollowerMessage::create( $committee, $hosts, $adherent, $this->urlGenerator->getUrl('...', $committee) )); } } Service Layer
  22. 43.

    Service Layer Custom voters src/Committee/Voter ├─ AbstractCommitteeVoter.php ├─ CreateCommitteeVoter.php ├─

    FollowCommitteeVoter.php ├─ HostCommitteeVoter.php ├─ ShowCommitteeVoter.php └─ SuperviseCommitteeVoter.php
  23. 44.

    class Geocoder implements GeocoderInterface { // ... public function geocode(string

    $address): Coordinates { try { $addresses = $this->geocoder->geocode($address); } catch (\Exception $exception) { throw GeocodingException::create($address, $exception); } if (!count($addresses)) { throw GeocodingException::create($address); } $geo = $addresses->first(); return new Coordinates( $geo->getLatitude(), $geo->getLongitude() ); } } Geocoding addresses Based on Google Maps
  24. 45.

    class EntityAddressGeocodingSubscriber implements EventSubscriberInterface { // ... public function onCommitteeCreated(CommitteeWasCreatedEvent

    $event) { $this->updateGeocodableEntity($event->getCommittee()); } private function updateGeocodableEntity(GeoPointInterface $geocodable) { if ($coords = $this->geocode($geocodable->getGeocodableAddress())) { $geocodable->updateCoordinates($coords); $this->manager->flush(); } } } Doctrine listener to update coordinates of any Geocodable entity when it’s saved to the database.
  25. 46.
  26. 47.
  27. 48.

    /** * @group functional */ class CommitteeControllerTest extends MysqlWebTestCase {

    // ... function testAnonymousUserIsNotAllowedToFollowCommittee() { $crawler = $this->client->request('GET', ...); $this->assertResponseStatusCode(200, ...); $this->assertFalse($this->seeFollowLink($crawler)); $this->assertFalse($this->seeUnfollowLink($crawler)); $this->assertTrue($this->seeRegisterLink($crawler)); } } Functional Testing PHPUnit
  28. 52.

    <script type="application/ld+json"> { "@context": "http://schema.org", "@type": "WebSite", "url": "http://en-marche.fr", "name":

    "En Marche !", "image": "http://en-marche.fr/images/default_sharer.jpg", "description": "Pour ceux qui ... l'Europe.", "funder": { "@type": "Person", "givenName": "Emmanuel", "familyName": "Macron", "jobTitle": "President of France" } } </script> Schema.org JSON LD Metadata in HTML code
  29. 55.

    class PageController extends Controller { // ... public function ellesMarchentAction()

    { if (!$this->getParameter('enable_canary'))) { throw $this->createNotFoundException(); } return $this->render('... '); } } Feature Flags
  30. 62.

    UTC -3 Guyana Saint Pierre and Miquelon UTC -4 St.

    Martin St. Barthelemy Guadeloupe Martinique UTC -8 Clipperton UTC -9 / UTC -9,5 / UTC -10 French Polynesia https://commons.wikimedia.org/wiki/File%3AMapadefrancia.svg
  31. 63.

    UTC +3 Mayotte Europa Island https://commons.wikimedia.org/wiki/File%3AMapadefrancia.svg UTC +4 Crozet Islands

    Reunion Glorious Islands Tromelin Island Juan de Nova UTC +5 St. Paul and Amsterdam Kerguelen Islands UTC +10 Adelie Land UTC +10 New Caledonia UTC +12 Wallis and Futuna
  32. 65.

    {% if not app.request.attributes.get('_campaign_expired') and not user_is_adherent %} <a href="{{

    hwi_oauth_login_url('auth') }}" class="btn b__nudge--right-nano"> Connexion </a> <a href="{{ auth_register_url }}" class="btn b__nudge--right"> Inscription </a> {% endif %} Disabling links in Twig
  33. 66.

    class ArticleController extends Controller { /** * @Route( * "/articles/{category}/{page}",

    * requirements={"category"="\w+", "page"="\d+"}, * defaults={ "_enable_campaign_silence": true } * ) */ public function actualitesAction(...): Response { ... } } Marking a mutable action
  34. 67.

    class CampaignSilenceSubscriber implements EventSubscriberInterface { // ... public function onKernelController(FilterControllerEvent

    $event) { // ... $expired = $this->processor->isCampaignExpired($request); $request->attributes->set('_campaign_expired', $expired); if (!$expired || $request->attributes->get('_enable_campaign_silence', false)) { return; } $event->setController(function () { return new Response($this->twig->render('campaign_silent.html.twig')); }); } }
  35. 68.

    April 2017 The road to the first round! Fullfil government

    rules & regulations # app/config/config_prod.yml services: app.mailjet.transport.transactional: class: 'AppBundle\Mailjet\Transport\MailjetNullTransport' arguments: ['@?logger'] public: false app.mailjet.transport.campaign: class: 'AppBundle\Mailjet\Transport\MailjetNullTransport' arguments: ['@?logger'] public: false
  36. 71.
  37. 72.

    # ... http { # ... server { # ...

    # Block WP Pingback DDoS attacks if ($http_user_agent ~* "WordPress") { return 403; } # ... } } WordPress pingback DDoS attacks
  38. 73.

    class AssetsController extends Controller { /** * @Route("/assets/{path}") * @Method("GET")

    * @Cache(maxage=900, smaxage=900) */ public function assetAction(string $path, ...) { // ... } } Http caching for static assets
  39. 76.