$30 off During Our Annual Pro Sale. View Details »

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

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.

Hugo Hamon

November 05, 2017
Tweet

More Decks by Hugo Hamon

Other Decks in Technology

Transcript

  1. Building an Open-Source
    Campaign Platform
    for the President of
    France .
    PHP CE 2017 / Nov. 5th / Ossa / Poland
    Hugo Hamon

    View Slide

  2. Hugo Hamon

    View Slide

  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

    View Slide

  4. Titouan Galopin
    Enrolls to En-Marche movement
    Initiates the Symfony migration
    Full-Stack Developer
    Setup the new technical stack
    @titouangalopin

    View Slide

  5. Innovative
    e-learning solution
    targeting Symfony teams.
    https://university.sensiolabs.com

    View Slide

  6. https://clujcon2017.symfony.com/

    View Slide

  7. What is this talk
    trully about?

    View Slide

  8. French Political System
    Open-Source Technologies
    Project Management
    Presidential Campaign Strategy
    Real World Project Case Study

    View Slide

  9. The
    Context

    View Slide

  10. Why is the
    Presidential election
    such a challenging
    environment?

    View Slide

  11. Why building a
    Citizen Collaborative
    Platform?

    View Slide

  12. Accept donations
    Register adherents
    Facilitate communication
    Structure movement

    View Slide

  13. What was the
    French Politic
    landscape?

    View Slide

  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

    View Slide

  15. April
    2016

    View Slide

  16. View Slide

  17. Summer
    2016

    View Slide

  18. +

    View Slide

  19. View Slide

  20. October
    2016

    View Slide

  21. View Slide

  22. https://github.com/enmarche/en-marche.fr

    View Slide

  23. https://en-marche.fr

    View Slide

  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.

    View Slide

  25. November
    2016

    View Slide

  26. Emmanuel Macron
    formalizes his
    candidacy for the
    presidential election.

    View Slide

  27. December
    2016

    View Slide

  28. View Slide

  29. View Slide

  30. # app/config/parameters.yml
    parameters:
    env(DATABASE_HOST): db
    env(DATABASE_PORT): 3306
    env(DATABASE_SOCKET): null
    env(DATABASE_NAME): enmarche
    env(DATABASE_USER): root
    env(DATABASE_PASSWORD): root

    View Slide

  31. View Slide

  32. January
    2017

    View Slide

  33. View Slide

  34. View Slide

  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.

    View Slide

  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.

    View Slide

  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

    View Slide

  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;
    }
    }

    View Slide

  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);
    }
    }

    View Slide

  40. February
    2017

    View Slide

  41. View Slide

  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

    View Slide

  43. Service
    Layer
    Custom voters
    src/Committee/Voter
    ├─ AbstractCommitteeVoter.php
    ├─ CreateCommitteeVoter.php
    ├─ FollowCommitteeVoter.php
    ├─ HostCommitteeVoter.php
    ├─ ShowCommitteeVoter.php
    └─ SuperviseCommitteeVoter.php

    View Slide

  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

    View Slide

  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.

    View Slide

  46. View Slide

  47. View Slide

  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

    View Slide

  49. Continuous Integration
    Pull-Requests
    SensioLabsInsight
    Style CI
    Circle CI

    View Slide

  50. March
    2017

    View Slide

  51. Program announced!
    +6,000 users live
    +300,000 daily

    View Slide

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

    View Slide

  53. Optimizing natural indexing in
    search engines.

    View Slide

  54. # app/config/parameters.yml
    parameters:
    # Staging configuration
    env(ENABLE_CANARY): 1
    # Production configuration
    env(ENABLE_CANARY): 0
    Feature Flags
    Env vars

    View Slide

  55. class PageController extends Controller
    {
    // ...
    public function ellesMarchentAction()
    {
    if (!$this->getParameter('enable_canary'))) {
    throw $this->createNotFoundException();
    }
    return $this->render('... ');
    }
    }
    Feature
    Flags

    View Slide

  56. Procuration System

    View Slide

  57. April
    2017

    View Slide

  58. Official regulations require
    candidates to stop
    proactive propaganda 24h
    before each ballot.

    View Slide

  59. Notifications
    Committees & events
    Make the website read-only!
    Contents publication
    Emails

    View Slide

  60. Who’s trully
    concerned?

    View Slide

  61. https://commons.wikimedia.org/wiki/File%3AMapadefrancia.svg

    View Slide

  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

    View Slide

  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

    View Slide

  64. How to make the
    platform fullfil the
    official regulations?

    View Slide

  65. {%
    if not app.request.attributes.get('_campaign_expired')
    and not user_is_adherent
    %}
    class="btn b__nudge--right-nano">
    Connexion

    class="btn b__nudge--right">
    Inscription

    {% endif %}
    Disabling links in Twig

    View Slide

  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

    View Slide

  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'));
    });
    }
    }

    View Slide

  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

    View Slide

  69. April 23th
    1st Ballot

    View Slide

  70. ~500,000 connections
    to the website.

    View Slide

  71. May
    2017

    View Slide

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

    View Slide

  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

    View Slide

  74. May 7th
    2nd Ballot

    View Slide

  75. ~650,000 connections
    to the website.

    View Slide

  76. Wrap Up

    View Slide

  77. Thank you!
    https://www.flickr.com/photos/enmarchefr/31534490113/
    @titouangalopin
    @hhamon
    @sensiolabs

    View Slide