Upgrade to Pro — share decks privately, control downloads, hide ads and more …

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.

E2ed7c278c8c49bb3e7fe0b7de039997?s=128

Hugo Hamon

June 20, 2017
Tweet

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
  2. Qui sommes-nous ? Hugo Hamon Software Architect SensioLabs Titouan Galopin

    Software Architect SensioLabs – En Marche !
  3. La plateforme En-Marche.fr https://www.flickr.com/photos/enmarchefr/32235874251/

  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
  5. Le besoin de la plateforme Accepter les dons Enregistrer les

    adhésions Faciliter la communication
  6. Les enjeux techniques •  Disponibilité continue de la plateforme • 

    Envois massifs d’e-mails •  Sécurité de l’infrastructure et des données
  7. Technologies

  8. Octobre 2016

  9. Octobre 2016 Titouan rejoint le mouvement politique En- Marche et

    initie la refonte du site Internet sous Symfony.
  10. Octobre 2016 Ancien site de publication de contenu

  11. Novembre 2016

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

  13. Décembre 2016

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

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

    cloud hosting
  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
  17. Décembre 2016 Un site de contenu plus moderne Chargement d’images

    à la demande avec Glide
  18. Janvier 2017

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

    soutien
  20. Janvier 2017 L’ancienne plateforme révèle ses limites Gestion de projet

    (très) agile
  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
  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(), // ... ]); } }
  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
  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; } }
  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); } }
  26. Février 2017

  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
  28. None
  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) )); } }
  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(/* ... */); } }
  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
  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) { ... } }
  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()); } }
  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(); } } }
  35. None
  36. None
  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)); } }
  38. Février 2017 La campagne bat son plein ! Intégration continue

  39. Mars 2017

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

  41. Mars 2017 Stabilisation de la plateforme Optimisation du référencement <script

    type="application/ld+json"> { "@context": "http://schema.org", "@type": "Organization", "name": "ClichyEnMarche", "url": "http://en-marche.fr/comites/8f2bbd50-0f53-5c42-854e-4b8b2765afff/clichyenmarche", "image": "http://en-marche.fr/images/default_sharer.jpg", "description": "clichy.enmarche@gmail.com", "location": { "@type": "PostalAddress", "streetAddress": "60 boulevard Jean Jaurès", "addressLocality": "Clichy", "postalCode": "92110", "addressCountry": "FR" } } </script>
  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
  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'); } }
  44. Mars 2017 Stabilisation de la plateforme Système de demande de

    procuration
  45. Avril 2017

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

    Ton Macron
  47. None
  48. None
  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
  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)) { // ... } } }
  51. Avril 2017 En route vers le premier tour ! Respect

    du code électoral
  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 { ... } }
  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')); }); } }
  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
  55. Avril 2017 En route vers le premier tour ! Premier

    tour de la Présidentielle
  56. Mai 2017

  57. Mai 2017 L’élection Bloquer les tentatives de DDOS # ...

    http { # ... server { # ... # Block WordPress Pingback DDoS attacks if ($http_user_agent ~* "WordPress") { return 403; } # ... } }
  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) { // ... } }
  59. Mai 2017 L’élection L’après second tour de la Présidentielle

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

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