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

L'Open-Source au service de la campagne d'Emmanuel Macron

L'Open-Source au service de la campagne d'Emmanuel Macron

Ce retour d'expérience vous emmènera dans l'envers du décor technique de la campagne victorieuse d'Emmanuel Macron aux élections présidentielles françaises de 2017. Nous vous révèlerons comment les équipes d'En-Marche et de SensioLabs ont étroitement collaboré pour développer une plateforme collaborative et citoyenne Open-Source avec PHP 7 et Symfony pour structurer le mouvement.

En complément des enjeux techniques et politiques de ce projet, nous vous présenterons les solutions adoptées comme le déploiement continu avec Docker sous Kubernetes, l'intégration continue avec Circle CI, la gestion de projet SCRUM et Kanban, l'utilisation de Rabbitmq, les dernières nouveautés de Symfony, et bien plus encore.

Nous vous montrerons l'importance du choix de ces solutions pour répondre aux besoins d'agilité et de scalabilité des temps forts de la campagne.

Hugo Hamon

June 20, 2017
Tweet

More Decks by Hugo Hamon

Other Decks in Programming

Transcript

  1. L'Open-Source.
    Au service de
    la campagne
    d'Emmanuel
    Macron
    https://www.flickr.com/photos/enmarchefr/31544137633/
    AFSY Paris
    20 juin 2017

    View Slide

  2. Qui sommes-nous ?
    Hugo Hamon
    Software Architect
    SensioLabs
    Titouan Galopin
    Software Architect
    SensioLabs – En Marche !

    View Slide

  3. La plateforme En-Marche.fr
    https://www.flickr.com/photos/enmarchefr/32235874251/

    View Slide

  4. Les contraintes de la campagne
    1.  Respecter des délais incompressibles
    2.  S’adapter à l’actualité
    3.  Proposer des fonctionnalités innovantes
    4.  Riposter aux attaques des autres candidats

    View Slide

  5. Le besoin de la plateforme
    Accepter les dons
    Enregistrer les adhésions
    Faciliter la communication

    View Slide

  6. Les enjeux techniques
    •  Disponibilité continue de la plateforme
    •  Envois massifs d’e-mails
    •  Sécurité de l’infrastructure et des données

    View Slide

  7. Technologies

    View Slide

  8. Octobre 2016

    View Slide

  9. Octobre 2016
    Titouan rejoint le
    mouvement
    politique En-
    Marche et initie la
    refonte du site
    Internet sous
    Symfony.

    View Slide

  10. Octobre 2016
    Ancien site de publication de contenu

    View Slide

  11. Novembre 2016

    View Slide

  12. Novembre 2016
    Officialisation de la candidature d’E.M
    https://www.flickr.com/photos/enmarchefr/32355911745/

    View Slide

  13. Décembre 2016

    View Slide

  14. Décembre 2016
    Un site de contenu plus moderne
    Symfony et cloud hosting

    View Slide

  15. Décembre 2016
    Un site de contenu plus moderne
    Symfony et cloud hosting

    View Slide

  16. Décembre 2016
    Un site de contenu plus moderne
    Variables d’environnement avec Symfony 3.2
    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

  17. Décembre 2016
    Un site de contenu plus moderne
    Chargement d’images à la demande avec Glide

    View Slide

  18. Janvier 2017

    View Slide

  19. Janvier 2017
    L’ancienne plateforme révèle ses limites
    SensioLabs apporte son soutien

    View Slide

  20. Janvier 2017
    L’ancienne plateforme révèle ses limites
    Gestion de projet (très) agile

    View Slide

  21. trait EntityIdentityTrait
    {
    /**
    * @ORM\Id
    * @ORM\Column(type="integer", options={"unsigned": true})
    * @ORM\GeneratedValue
    */
    protected $id;
    /**
    * @ORM\Column(type="uuid")
    * @var Ramsey\Uuid\UuidInterface
    */
    protected $uuid;
    // ... + getters
    }
    Janvier 2017
    L’ancienne plateforme révèle ses limites
    Entités Doctrine : ID vs UUIDs

    View Slide

  22. Janvier 2017
    L’ancienne plateforme révèle ses limites
    Gestion des adhésions et donations
    class MembershipController extends Controller
    {
    // ...
    public function registerAction(Request $request): Response
    {
    $membership = MembershipRequest::createWithCaptcha($request->request->get('g-recaptcha-response'));
    $form = $this->createForm(MembershipRequestType::class, $membership);
    if ($form->handleRequest($request)->isSubmitted() && $form->isValid()) {
    $this->get('app.membership_request_handler')->handle($membership);
    return $this->redirectToRoute('app_membership_donate');
    }
    return $this->render('membership/register.html.twig', [
    'form' => $form->createView(),
    // ...
    ]);
    }
    }

    View Slide

  23. Janvier 2017
    L’ancienne plateforme révèle ses limites
    Intégration de Mailjet à la plateforme
    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

  24. Janvier 2017
    L’ancienne plateforme révèle ses limites
    Intégration de Mailjet à la plateforme
    class MailjetService
    {
    // ...
    public function sendMessage(MailjetMessage $message): bool
    {
    $email = $this->factory->createFromMailjetMessage($message);
    try {
    $this->dispatcher->dispatch(MailjetEvents::DELIVERY_MESSAGE, new MailjetEvent($message, $email));
    $this->transport->sendTemplateEmail($email);
    $this->dispatcher->dispatch(MailjetEvents::DELIVERY_SUCCESS, new MailjetEvent($message, $email));
    } catch (MailjetException $e) {
    $this->dispatcher->dispatch(MailjetEvents::DELIVERY_ERROR, new MailjetEvent($message, $email, $e));
    return false;
    }
    return true;
    }
    }

    View Slide

  25. Janvier 2017
    L’ancienne plateforme révèle ses limites
    Intégration de Mailjet à la plateforme
    class MembershipRequestHandler
    {
    // ...
    public function handle(MembershipRequest $membershipRequest)
    {
    $adherent = $this->adherentFactory->createFromMembershipRequest($membershipRequest);
    $token = AdherentActivationToken::generate($adherent);
    $this->manager->persist($adherent);
    $this->manager->persist($token);
    $this->manager->flush();
    $activationUrl = $this->generateMembershipActivationUrl($adherent, $token);
    $this->mailjet->sendMessage(AdherentAccountActivationMessage::createFromAdherent($adherent, $activationUrl));
    $event = new AdherentAccountWasCreatedEvent($adherent);
    $this->dispatcher->dispatch(AdherentEvents::REGISTRATION_COMPLETED, $event);
    }
    }

    View Slide

  26. Février 2017

    View Slide

  27. Février 2017
    La campagne bat son plein !
    Intensification des développements
    •  Gestion des comités et des événements
    •  Geocodage des comités, événements et adhérents
    •  Tests unitaires et fonctionnels
    •  Cache applicatif avec Redis

    View Slide

  28. View Slide

  29. Février 2017
    La campagne bat son plein !
    Gestion des comités
    class CommitteeManagementAuthority
    {
    // ...
    public function followCommittee(Adherent $adherent, Committee $committee)
    {
    $this->manager->followCommittee($adherent, $committee);
    if (!$hosts = $this->manager->getCommitteeHosts($committee)->toArray()) {
    return;
    }
    $this->mailjet->sendMessage(CommitteeNewFollowerMessage::create(
    $committee,
    $hosts,
    $adherent,
    $this->urlGenerator->getUrl('app_commitee_manager_list_members', $committee)
    ));
    }
    }

    View Slide

  30. Février 2017
    La campagne bat son plein !
    Gestion des comités
    /** @Route("/comites/{uuid}/{slug}", requirements={"uuid": "%pattern_uuid%"}) */
    class CommitteeController extends Controller
    {
    /**
    * @Route("/rejoindre", name="app_committee_follow", condition="request.request.has('token')")
    * @Method("POST")
    * @Security("is_granted('FOLLOW_COMMITTEE', committee)")
    */
    public function followCommitteeAction(Request $request, Committee $committee): Response
    {
    if (!$this->isCsrfTokenValid('committee.follow', $request->request->get('token'))) {
    throw $this->createAccessDeniedException('...');
    }
    $this->get('app.committee.authority')->followCommittee($this->getUser(), $committee);
    return new JsonResponse(/* ... */);
    }
    }

    View Slide

  31. Février 2017
    La campagne bat son plein !
    Gestion des comités
    src/Committee/Voter
    ├── AbstractCommitteeVoter.php
    ├── CreateCommitteeVoter.php
    ├── FollowCommitteeVoter.php
    ├── HostCommitteeVoter.php
    ├── ShowCommitteeVoter.php
    └── SuperviseCommitteeVoter.php

    View Slide

  32. Février 2017
    La campagne bat son plein !
    L’annotation @Entity
    class MembershipController extends Controller
    {
    /**
    * @Route(
    * path="/inscription/finaliser/{adherent_uuid}/{activation_token}",
    * name="app_membership_activate",
    * requirements={
    * "adherent_uuid": "%pattern_uuid%",
    * "activation_token": "%pattern_sha1%"
    * }
    * )
    * @Entity("adherent", expr="repository.findOneByUuid(adherent_uuid)")
    * @Entity("token", expr="repository.findByToken(activation_token)")
    */
    public function activateAction(Adherent $adherent, AdherentActivationToken $token)
    { ... }
    }

    View Slide

  33. Février 2017
    La campagne bat son plein !
    Geocodage d’adresses avec Google Maps
    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);
    }
    $geocoded = $addresses->first();
    return new Coordinates($geocoded->getLatitude(), $geocoded->getLongitude());
    }
    }

    View Slide

  34. Février 2017
    La campagne bat son plein !
    Geocodage d’adresses avec Google Maps
    class EntityAddressGeocodingSubscriber implements EventSubscriberInterface
    {
    // ...
    public function onCommitteeCreated(CommitteeWasCreatedEvent $event)
    {
    $this->updateGeocodableEntity($event->getCommittee());
    }
    private function updateGeocodableEntity(GeoPointInterface $geocodable)
    {
    if ($coordinates = $this->geocoder->geocode($geocodable->getGeocodableAddress())) {
    $geocodable->updateCoordinates($coordinates);
    $this->manager->flush();
    }
    }
    }

    View Slide

  35. View Slide

  36. View Slide

  37. Février 2017
    La campagne bat son plein !
    Tests fonctionnels
    /** @group functional */
    class CommitteeControllerTest extends MysqlWebTestCase
    {
    // ...
    public function testAnonymousUserIsNotAllowedToFollowCommittee()
    {
    $committeeUrl = sprintf('/comites/%s/%s', Data::COMMITTEE_3_UUID, 'en-marche-clichy');
    $crawler = $this->client->request(Request::METHOD_GET, $committeeUrl);
    $this->assertResponseStatusCode(Response::HTTP_OK, $this->client->getResponse());
    $this->assertFalse($this->seeFollowLink($crawler));
    $this->assertFalse($this->seeUnfollowLink($crawler));
    $this->assertTrue($this->seeRegisterLink($crawler));
    }
    }

    View Slide

  38. Février 2017
    La campagne bat son plein !
    Intégration continue

    View Slide

  39. Mars 2017

    View Slide

  40. Mars 2017
    Stabilisation de la plateforme
    Annonce du programme d’E.M

    View Slide

  41. Mars 2017
    Stabilisation de la plateforme
    Optimisation du référencement
    <br/>{<br/>"@context": "http://schema.org",<br/>"@type": "Organization",<br/>"name": "ClichyEnMarche",<br/>"url": "http://en-marche.fr/comites/8f2bbd50-0f53-5c42-854e-4b8b2765afff/clichyenmarche",<br/>"image": "http://en-marche.fr/images/default_sharer.jpg",<br/>"description": "[email protected]",<br/>"location": {<br/>"@type": "PostalAddress",<br/>"streetAddress": "60 boulevard Jean Jaurès",<br/>"addressLocality": "Clichy",<br/>"postalCode": "92110",<br/>"addressCountry": "FR"<br/>}<br/>}<br/>

    View Slide

  42. Mars 2017
    Stabilisation de la plateforme
    Feature flags
    # app/config/parameters.yml
    parameters:
    # Staging configuration
    env(ENABLE_CANARY): 1
    # Production configuration
    env(ENABLE_CANARY): 0

    View Slide

  43. Mars 2017
    Stabilisation de la plateforme
    Feature flags
    class PageController extends Controller
    {
    // ...
    public function ellesMarchentAction()
    {
    if (!((bool) $this->getParameter('enable_canary'))) {
    throw $this->createNotFoundException();
    }
    return $this->render('page/elles-marchent.html.twig');
    }
    }

    View Slide

  44. Mars 2017
    Stabilisation de la plateforme
    Système de demande de procuration

    View Slide

  45. Avril 2017

    View Slide

  46. Avril 2017
    En route vers le premier tour !
    Générateur Ton Macron

    View Slide

  47. View Slide

  48. View Slide

  49. framework:
    workflows:
    ton_macron_invitation:
    type: state_machine
    supports: [AppBundle\TonMacron\InvitationProcessor]
    places:
    - !php/const:AppBundle\TonMacron\InvitationProcessor::STATE_NEEDS_FRIEND_INFO
    - !php/const:AppBundle\TonMacron\InvitationProcessor::STATE_NEEDS_FRIEND_PROJECT
    - ...
    transitions:
    !php/const:AppBundle\TonMacron\InvitationProcessor::TRANSITION_FILL_INFO:
    from:
    - !php/const:AppBundle\TonMacron\InvitationProcessor::STATE_NEEDS_FRIEND_INFO
    to: !php/const:AppBundle\TonMacron\InvitationProcessor::STATE_NEEDS_FRIEND_PROJECT
    !php/const:AppBundle\TonMacron\InvitationProcessor::TRANSITION_FILL_PROJECT:
    # ...
    Avril 2017
    En route vers le premier tour !
    Générateur Ton Macron

    View Slide

  50. final class InvitationProcessorHandler
    {
    // ...
    public function __construct(..., StateMachine $stateMachine)
    {
    // ...
    $this->stateMachine = $stateMachine;
    }
    public function process(InvitationProcessor $processor): ?TonMacronFriendInvitation
    {
    if ($this->stateMachine->can($processor, InvitationProcessor::TRANSITION_SEND)) {
    // ...
    $this->stateMachine->apply($processor, InvitationProcessor::TRANSITION_SEND);
    // ...
    }
    $this->stateMachine->apply($processor, $this->getCurrentTransition($processor));
    if ($this->stateMachine->can($processor, InvitationProcessor::TRANSITION_SEND)) {
    // ...
    }
    }
    }

    View Slide

  51. Avril 2017
    En route vers le premier tour !
    Respect du code électoral

    View Slide

  52. Avril 2017
    En route vers le premier tour !
    Respect du code électoral
    class ArticleController extends Controller
    {
    /**
    * @Route(
    * "/articles/{category}/{page}",
    * requirements={"category"="\w+", "page"="\d+"},
    * defaults={"page"=1, "_enable_campaign_silence"=true}
    * )
    */
    public function actualitesAction(...): Response
    { ... }
    }

    View Slide

  53. Avril 2017
    En route vers le premier tour !
    Respect du code électoral
    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

  54. Avril 2017
    En route vers le premier tour !
    Respect du code électoral
    # 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

  55. Avril 2017
    En route vers le premier tour !
    Premier tour de la Présidentielle

    View Slide

  56. Mai 2017

    View Slide

  57. Mai 2017
    L’élection
    Bloquer les tentatives de DDOS
    # ...
    http {
    # ...
    server {
    # ...
    # Block WordPress Pingback DDoS attacks
    if ($http_user_agent ~* "WordPress") {
    return 403;
    }
    # ...
    }
    }

    View Slide

  58. Mai 2017
    L’élection
    Profiter du cache HTTP
    class AssetsController extends Controller
    {
    /**
    * @Route("/assets/{path}")
    * @Method("GET")
    * @Cache(maxage=900, smaxage=900)
    */
    public function assetAction(string $path, Request $request)
    {
    // ...
    }
    }

    View Slide

  59. Mai 2017
    L’élection
    L’après second tour de la Présidentielle

    View Slide

  60. https://www.flickr.com/photos/franceintheus/34578273235/
    Quel avenir pour le projet ?

    View Slide

  61. Merci !
    https://www.flickr.com/photos/enmarchefr/31534490113/
    @titouangalopin
    @hhamon
    https://github.com/enmarche/en-marche.fr

    View Slide