Blackfire

Migration progressive

Retour d'expérience - Veolys vs ProwebCE

Clément Bertillon

Quelle type de migration ?

- From Scratch

- Progressive

Problématiques

  • Comment migrer une application existante vers Symfony3 dans les meilleures conditions
  • Comment faire cohabiter 2 applicatifs différents sur un même projet pendant un laps de temps plus ou moins long
  • Comment exploiter des fonctionnalités ou modules de l'ancien système avec le nouveau sans pénaliser l’applicatif
  • Comment développer des cycles courts et un niveau d'efficacité optimum tout en conservant un code de qualité

D'une migration progressive

Veolys

Un outils de gestion d'ensemble immobilier

 

BPM

Backoffice HM et refonte du SI ProwebCE

L'existant

La nouvelle application

La (petite) liste de features veolys

L'existant

La nouvelle application

Statistiques

1er commit git- Gilles 2007
(une précédente version éxistait)

Legacy: 200 000 lignes de codes

Symfony3: 60 000 lignes de codes

500 routes hors webservice/api

Phpmetrics
Veolys : 61674 lignes
BPM : 61730 lignes

Migration de la stack

PHP 5.3

Apache 2.2

Mysql 5.5

Memcache

PHP 7

Apache 2.4

Percona Server - Mysql 5.7

Redis

Mentions honorables

symfony 1.0 (beta) avec vendor commité

Mot de passe en clair parce que c'est plus facile à lire/modifier pour le client

Un webservice en SOAP, avec un seul point d'entré qui accepte un fichier XML sans règles de validation

clement.bertillon - Francoisgueguen - Jules Pietri - Jérémy DERUSSE - Jérémy Derussé - Loïc Chardonnet - Mathieu MARCHOIS - Michel ROCA D'HUYTEZA - Olga ROTACH - PaskR - Quentin GUILLEMINEAU - Romain GAUTIER - Salah MEHARGA - Sylvain Jaune - alexandre.salome - fabien - fred - fx.deguillebon - geoffrey.bachelet - gilles - gregoire.hubert - gregoire.pineau - illusionOfParadise - inal.djafar - itkg-nanne - jeremy.derusse - julien.levasseur - loic.chardonnet - mRoca - mathieu.cavanne - maxime.douailin - pierre.cahard - romain.dorgueil - romain.gautier - roupen.torossian - sensio - simon.franquet - sofany.ong - thibaut.prim - vincent.dravert - xavier.briand - yohann.giarelli

git shortlog --summary
Christophe NGUYEN, Philippe MARQUINE, Stephane THEVENOUD, Philippe BEAUVAIS, Thibault DANTIGNY, Hugues GOBET, akeriel, bkaciousalah, Jonathan LEQUIN, Damien OGIER-DENIS, stephane thevenoud, Patrick SIMON, Adrien MADET, Antonin CLAUZIER, fmartin, Catherine GROUBON, Patrick CLOUZEAU, camille hernoux, Jonathan Lequin, François MARTIN-BROSSAT, Camille HERNOUX, Stephane BIZOLLON, ppailley, vmasia, kevin renaud, Ludovic LASQUELLEC, Amandine THERY, Adrien Madet, Jean-Jacques Parrenin, Ousmane KANTE, Hugues HG. GOBET, Jean Jacques Parrenin, marecauxg, Franck Sun, Christophe Nguyen, DIDIER UNGAR, gstorchi, Ousmane KANTE, Hugues Gobet, laurenta, Jean-Jacques PARRENIN, Damien LORTHIOIS, Francois MARTIN-BROSSAT, Antonin Clauzier, bialoruckio, Jean-Marie Le Bonniec (externe - smile), Yani Makouf (ext), Kevin RENAUD, traissardjm, Kévin ARBOUIN, Linda Bouzid, Boris FOURNIER, Maxime BERNARD, Zakaria Harrou, Fabien PIRON, Kevin ARBOUIN, Franck SUN, Christophe PYREE, Mehdi Tebourbi, Zakaria HARROU, llasquellec, Linda BOUZID, Thomas TALBOT, Mahmoud CHARFEDDINE, Jenkins HM, Ulas ATILA, asabbagh-e, Julien VANDROMME, Christophe DENIVET, Anis SABBAGH, Thibault Dantigny, anefoussi-e, Laetitia MONTEFERRARIO, perassia, Tayeb CHIKHI, Hakim BOUCHEMA, Kevin Renaud, Hugo Mallaroni-Cosentino, Yani Makouf, Ali CANSEV, agkerros, adminweb, Hugo MALLARONI-COSENTINO, Clement Bertillon, Antoine DI BARTOLOMEO, Boris Fournier, Jean Jacques Parrenin, Seifallah Azzabi, Di Bartolomeo, administrateur, Stéphanie SAINT-MARCEL, Alexandre VINET, Leo RIGER, Administrator, Stephane BEKALE, Thomas Talbot, Smaine Milianni, Emilie BAUDUIN, Seifallah AZZABI, Lara DUFOUR, clement.bertillon, cvs2svn, ddelot, Pascal BUISSON, Julien Manganne, Aroussi NEFOUSSI, Julien Maulny, pmichalet, tchikhi-e, Thibault, Yani MAKOUF, Charlie ARTO, Smaine MILIANNI, Badr HAKKARI, Safouane RKIK, Clement Passevant, Safouane REKIK, apaixao, Romain Gautier, root, ZaKaRiA, Christophe DEVINET, Stephane BEKALE, Fabien PAPET
Christophe NGUYEN, Philippe MARQUINE, Stephane THEVENOUD, Philippe BEAUVAIS, Thibault DANTIGNY, Hugues GOBET, akeriel, bkaciousalah, Jonathan LEQUIN, Damien OGIER-DENIS, stephane thevenoud, Patrick SIMON, Adrien MADET, Antonin CLAUZIER, fmartin, Catherine GROUBON, Patrick CLOUZEAU, camille hernoux, Jonathan Lequin, François MARTIN-BROSSAT, Camille HERNOUX, Stephane BIZOLLON, ppailley, vmasia, kevin renaud, Ludovic LASQUELLEC, Amandine THERY, Adrien Madet, Jean-Jacques Parrenin, Ousmane KANTE, Hugues HG. GOBET, Jean Jacques Parrenin, marecauxg, Franck Sun, Christophe Nguyen, DIDIER UNGAR, gstorchi, Ousmane KANTE, Hugues Gobet, laurenta, Jean-Jacques PARRENIN, Damien LORTHIOIS, Francois MARTIN-BROSSAT, Antonin Clauzier, bialoruckio, Jean-Marie Le Bonniec (externe - smile), Yani Makouf (ext), Kevin RENAUD, traissardjm, Kévin ARBOUIN, Linda Bouzid, Boris FOURNIER, Maxime BERNARD, Zakaria Harrou, Fabien PIRON, Kevin ARBOUIN, Franck SUN, Christophe PYREE, Mehdi Tebourbi, Zakaria HARROU, llasquellec, Linda BOUZID, Thomas TALBOT, Mahmoud CHARFEDDINE, Jenkins HM, Ulas ATILA, asabbagh-e, Julien VANDROMME, Christophe DENIVET, Anis SABBAGH, Thibault Dantigny, anefoussi-e, Laetitia MONTEFERRARIO, perassia, Tayeb CHIKHI, Hakim BOUCHEMA, Kevin Renaud, Hugo Mallaroni-Cosentino, Yani Makouf, Ali CANSEV, agkerros, adminweb, Hugo MALLARONI-COSENTINO, Clement Bertillon, Antoine DI BARTOLOMEO, Boris Fournier, Jean Jacques Parrenin, Seifallah Azzabi, Di Bartolomeo, administrateur, Stéphanie SAINT-MARCEL, Alexandre VINET, Leo RIGER, Administrator, Stephane BEKALE, Thomas Talbot, Smaine Milianni, Emilie BAUDUIN, Seifallah AZZABI, Lara DUFOUR, clement.bertillon, cvs2svn, ddelot, Pascal BUISSON, Julien Manganne, Aroussi NEFOUSSI, Julien Maulny, pmichalet, tchikhi-e, Thibault, Yani MAKOUF, Charlie ARTO, Smaine MILIANNI, Badr HAKKARI, Safouane RKIK, Clement Passevant, Safouane REKIK, apaixao, Romain Gautier, root, ZaKaRiA, Christophe DEVINET, Stephane BEKALE, Fabien PAPET
Christophe NGUYEN, Philippe MARQUINE, Stephane THEVENOUD, Philippe BEAUVAIS, Thibault DANTIGNY, Hugues GOBET, akeriel, bkaciousalah, Jonathan LEQUIN, Damien OGIER-DENIS, stephane thevenoud, Patrick SIMON, Adrien MADET, Antonin CLAUZIER, fmartin, Catherine GROUBON, Patrick CLOUZEAU, camille hernoux, Jonathan Lequin, François MARTIN-BROSSAT, Camille HERNOUX, Stephane BIZOLLON, ppailley, vmasia, kevin renaud, Ludovic LASQUELLEC, Amandine THERY, Adrien Madet, Jean-Jacques Parrenin, Ousmane KANTE, Hugues HG. GOBET, Jean Jacques Parrenin, marecauxg, Franck Sun, Christophe Nguyen, DIDIER UNGAR, gstorchi, Ousmane KANTE, Hugues Gobet, laurenta, Jean-Jacques PARRENIN, Damien LORTHIOIS, Francois MARTIN-BROSSAT, Antonin Clauzier, bialoruckio, Jean-Marie Le Bonniec (externe - smile), Yani Makouf (ext), Kevin RENAUD, traissardjm, Kévin ARBOUIN, Linda Bouzid, Boris FOURNIER, Maxime BERNARD, Zakaria Harrou, Fabien PIRON, Kevin ARBOUIN, Franck SUN, Christophe PYREE, Mehdi Tebourbi, Zakaria HARROU, llasquellec, Linda BOUZID, Thomas TALBOT, Mahmoud CHARFEDDINE, Jenkins HM, Ulas ATILA, asabbagh-e, Julien VANDROMME, Christophe DENIVET, Anis SABBAGH, Thibault Dantigny, anefoussi-e, Laetitia MONTEFERRARIO, perassia, Tayeb CHIKHI, Hakim BOUCHEMA, Kevin Renaud, Hugo Mallaroni-Cosentino, Yani Makouf, Ali CANSEV, agkerros, adminweb, Hugo MALLARONI-COSENTINO, Clement Bertillon, Antoine DI BARTOLOMEO, Boris Fournier, Jean Jacques Parrenin, Seifallah Azzabi, Di Bartolomeo, administrateur, Stéphanie SAINT-MARCEL, Alexandre VINET, Leo RIGER, Administrator, Stephane BEKALE, Thomas Talbot, Smaine Milianni, Emilie BAUDUIN, Seifallah AZZABI, Lara DUFOUR, clement.bertillon, cvs2svn, ddelot, Pascal BUISSON, Julien Manganne, Aroussi NEFOUSSI, Julien Maulny, pmichalet, tchikhi-e, Thibault, Yani MAKOUF, Charlie ARTO, Smaine MILIANNI, Badr HAKKARI, Safouane RKIK, Clement Passevant, Safouane REKIK, apaixao, Romain Gautier, root, ZaKaRiA, Christophe DEVINET, Stephane BEKALE, Fabien PAPET
Christophe NGUYEN, Philippe MARQUINE, Stephane THEVENOUD, Philippe BEAUVAIS, Thibault DANTIGNY, Hugues GOBET, akeriel, bkaciousalah, Jonathan LEQUIN, Damien OGIER-DENIS, stephane thevenoud, Patrick SIMON, Adrien MADET, Antonin CLAUZIER, fmartin, Catherine GROUBON, Patrick CLOUZEAU, camille hernoux, Jonathan Lequin, François MARTIN-BROSSAT, Camille HERNOUX, Stephane BIZOLLON, ppailley, vmasia, kevin renaud, Ludovic LASQUELLEC, Amandine THERY, Adrien Madet, Jean-Jacques Parrenin, Ousmane KANTE, Hugues HG. GOBET, Jean Jacques Parrenin, marecauxg, Franck Sun, Christophe Nguyen, DIDIER UNGAR, gstorchi, Ousmane KANTE, Hugues Gobet, laurenta, Jean-Jacques PARRENIN, Damien LORTHIOIS, Francois MARTIN-BROSSAT, Antonin Clauzier, bialoruckio, Jean-Marie Le Bonniec (externe - smile), Yani Makouf (ext), Kevin RENAUD, traissardjm, Kévin ARBOUIN, Linda Bouzid, Boris FOURNIER, Maxime BERNARD, Zakaria Harrou, Fabien PIRON, Kevin ARBOUIN, Franck SUN, Christophe PYREE, Mehdi Tebourbi, Zakaria HARROU, llasquellec, Linda BOUZID, Thomas TALBOT, Mahmoud CHARFEDDINE, Jenkins HM, Ulas ATILA, asabbagh-e, Julien VANDROMME, Christophe DENIVET, Anis SABBAGH, Thibault Dantigny, anefoussi-e, Laetitia MONTEFERRARIO, perassia, Tayeb CHIKHI, Hakim BOUCHEMA, Kevin Renaud, Hugo Mallaroni-Cosentino, Yani Makouf, Ali CANSEV, agkerros, adminweb, Hugo MALLARONI-COSENTINO, Clement Bertillon, Antoine DI BARTOLOMEO, Boris Fournier, Jean Jacques Parrenin, Seifallah Azzabi, Di Bartolomeo, administrateur, Stéphanie SAINT-MARCEL, Alexandre VINET, Leo RIGER, Administrator, Stephane BEKALE, Thomas Talbot, Smaine Milianni, Emilie BAUDUIN, Seifallah AZZABI, Lara DUFOUR, clement.bertillon, cvs2svn, ddelot, Pascal BUISSON, Julien Manganne, Aroussi NEFOUSSI, Julien Maulny, pmichalet, tchikhi-e, Thibault, Yani MAKOUF, Charlie ARTO, Smaine MILIANNI, Badr HAKKARI, Safouane RKIK, Clement Passevant, Safouane REKIK, apaixao, Romain Gautier, root, ZaKaRiA, Christophe DEVINET, Stephane BEKALE, Fabien PAPET

Workflow

Analyse

Audit d'architecture

Etude de migration
progressive

On garde un ensemble de contraintes qui vont faciliter la migration

- Les mêmes URL

- La même version de PHP

- Le même server

- Le même domaine

Documentation officiel 

Problématiques

- Modèle

- Tests

- Cohabitation

- Sécurité

- Modèle

- Tests

- Cohabitation

- Sécurité

Cohabitation

Maintenir une application PHP tiers au sein d'une application Symfony

Legacy

<?php

// web/index.php

require_once __DIR__.'/../Symfony2/app/bootstrap.php.cache';
require_once __DIR__.'/../Symfony2/app/StaticKernel.php';
$staticKernel = StaticKernel::getInstance();

$request = Request::createFromGlobals();
$matcher = $staticKernel->getUrlMatcher($request);

try {
    $attributes = $matcher->match($request->getPathInfo());
} catch (ResourceNotFoundException $e) {
    chdir(__DIR__.'/../symfony1/web');
    require_once __DIR__.'/../symfony1/web/index.php';

    return;
} catch (MethodNotAllowedException $e) {
}

$kernel = $staticKernel->getKernel();
$kernel->loadClassCache();
$response = $kernel->handle($request);
$response->send();
$kernel->terminate($request, $response);

V1 : Passer par le front controller

Symfony2

Symfony2

symfony1

Inconvénients

  • Si on tombe sur une route Symfony4, on doit appeler le routing à deux reprises
  • Les erreurs client/serveur ne peuvent pas être controlé
  • On ajoute pleins de règles spécifiques dans un script PHP qui ne resemble plus à rien
  • On peut difficilement partager un context ou des informations entre notre nouvelle application et le legacy

Solution

C'est la nouvelle application qui prend la main et qui va encapsuler le legacy

Veolys-reboot

Symfony3

TheodoEvolutionLegacyWrapperBundle

Supporte: symfony1.4 / symfony1.5 / CodeIgniter
et custom kernel

Comment ça marche ?

Symfony4 cherche un controller
qui match la request

HTTP Request

Si aucune route Sf4, on passe la main au legacy pour faire le même travail

/**
 * @param GetResponseEvent $event
 *
 * @return GetResponseEvent
 */
public function onKernelRequest(GetResponseEvent $event)
{
    try {
        $this->routerListener->onKernelRequest($event);
    } catch (NotFoundHttpException $e) {
        // Log ...

        $response = $this->legacyKernel->handle($event->getRequest(), 
            $event->getRequestType(), true);

        if ($response->getStatusCode() !== 404) {
            $event->setResponse($response);

            return $event;
        }
    }
}

Router Listener

class Symfony10Kernel extends LegacyKernel
{
    /** @var ContainerInterface */
    private $container;

    public function handle(Request $request, $type = self::MASTER_REQUEST, $catch = true)
    {
        $context = $this->setUpContext($request);

        ob_start();
        // Dispatch request
        try {
            $context->getController()->dispatch();
        } catch (\sfStopException $e) {
        } catch (\Exception $e) {
            throw $e;
        } finally {
            $context->shutdown();
        }
        $stdout = ob_get_contents();
        ob_end_clean();

        return $this->convertResponse($context->getResponse(), $stdout);
    }

    // ...
}

Custom kernel Veolys

// \sfContext

Custom kernel BPM

class ProwebLegacyKernel extends LegacyKernel
{    
    public function handle(Request $request, $type = self::MASTER_REQUEST, $catch = true)
    {
        // There is 3 type of legacy: Zend, homemade Framework and procedural php
        $legacyType = $this->getLegacyType($request->getUri());

        // Start the buffer and call the right legacy
        ob_start();
        switch ($legacyType) {
            case self::LEGACY_TYPE_HOMEPAGE:
                require_once $this->getRootDir().'/../legacy/hm.php';
                break;
            case self::LEGACY_TYPE_ZEND:
                require_once $this->getRootDir().'/../legacy/hm2/public/index.php';
                break;
            case self::LEGACY_TYPE_HOMEMADE_FRAMEWORK:
                require_once $this->getRootDir().'/../legacy/front.php';
                break;
            case self::LEGACY_TYPE_PROCEDURAL_PHP:
                putenv('PHP_SELF=/'.implode('/', $pathParts));
                require_once $this->getRootDir().'/../legacy/'.implode('/', $pathParts);
                break;
        }
        $response = ob_get_clean();

        return new Response($response, 200, $header);
    }
}
# app/config/config.yml

theodo_evolution_legacy_wrapper:
    root_dir: %kernel.root_dir%/../legacy
    kernel:
        id: legacy.kernel.symfony10
        options:
            application: frontend
            environment: '%kernel.environment%'
            debug:       '%kernel.debug%'

Configuration

Authentification cross application

Lecture de session: TheodoEvolutionSessionBundle

 

Authentification:
PreAuthenticatedListener custom

Authentification géré par l'application legacy

TheodoEvolutionSessionBundle

Supporte: symfony1.x / CodeIgniter

class BagConfiguration implements BagManagerConfigurationInterface
{
    private $namespaces = array(
        BagManagerConfigurationInterface::LAST_REQUEST_NAMESPACE => 'symfony/user/sfUser/lastRequest',
        BagManagerConfigurationInterface::AUTH_NAMESPACE         => 'symfony/user/sfUser/authenticated',
        BagManagerConfigurationInterface::CREDENTIAL_NAMESPACE   => 'symfony/user/sfUser/credentials',
        BagManagerConfigurationInterface::ATTRIBUTE_NAMESPACE    => 'symfony/user/sfUser/attributes',
        BagManagerConfigurationInterface::CULTURE_NAMESPACE      => 'symfony/user/sfUser/culture',
    );

    /**
     * {@inheritdoc}
     */
    public function getNamespaces()
    {
        return $this->namespaces;
    }

    /**
     * {@inheritdoc}
     */
    public function getNamespace($key)
    {
        return $this->namespaces[$key];
    }
}

PreAuthenticatedListener

namespace Veolys\LegacyBundle\Security\Firewall;

/**
 * Retrieves the user ID from the symfony1 session.
 */
class Symfony1SessionListener extends AbstractPreAuthenticatedListener
{
    /**
     * Gets the user and credentials from the Request.
     */
    protected function getPreAuthenticatedData(Request $request)
    {
        if (!$this->veolysSession->isAuthenticated()) {
            // For call the entryPoint and redirect the user to /
            throw new BadCredentialsException();
        }

        return [(string) $this->veolysSession->getUserId(), null];
    }
}

Security
=
Authentication
+
???

Authorization

mysql> select id, name from veolys_permission;

+-----+-------------------------------------------------+
| id  | name                                            |
+-----+-------------------------------------------------+
|   1 | Building/Edit                                   |
|   3 | HotlineRequest/View                             |
|   5 | HotlineRequest/ViewAll                          |
|   6 | HotlineRequest/Post                             |
|   7 | HotlineRequest/PostForAll                       |
|   8 | HotlineRequest/DetailsChangeDescription         |
|   9 | HotlineRequest/DetailsDecideConclusion          |
|  16 | HotlineRequest/IsDispatcher                     |

# ...

| 158 | HotlineRequest/DelayAlert                       |
| 159 | HotlineRequest/Indicators                       |
| 160 | HotlineRequest/Lots                             |
+-----+-------------------------------------------------+
149 rows in set (0.00 sec)

Permissions utilisateurs

namespace Veolys\LegacyBundle\Security\Authentication\Provider;

/**
 * Retrieves the user and his rights from the database
 */
class Symfony1SessionProvider implements AuthenticationProviderInterface
{
    public function authenticate(TokenInterface $token)
    {
        $username = $token->getUsername();
        $user = $this->userProvider
            ->loadUserByUsername($username) // l'Id qui vient du PreAuthenticatedListener
        ;

        if (!$user) {
            throw new AuthenticationException('The symfony1 session authentication failed.');
        }

        $roles = $this->em
            ->getRepository('VeolysApplicationBundle:VeolysUser')
            ->getRoles($user)
        ;
        $user->setRoles($roles);

        $authenticatedToken = new PreAuthenticatedToken($user, null, 'veolys', $roles);

        return $authenticatedToken;
    }
}

AuthenticationProvider

    public function getRoles($user)
    {
        $permissions = $this->getUserPermissions();
  
        $roles = array_map(
            function ($permission) {
                preg_match_all('#((?:^|[A-Z])[a-z]+)/?#', $permission['name'], $matches);
                $name = strtoupper(implode('_', $matches[1]));

                return 'ROLE_'.$name;
            },
            $permissions
        );

        if ($user->isSuperAdmin()) {
            $roles[] = 'ROLE_SUPER_ADMIN';
        }

        $roles[] = 'ROLE_USER';

        return $roles;
    }
HotlineRequest/Post => ROLE_HOTLINE_REQUEST_POST
/**
 * @Route("/edit", name="i_love_veolys")
 *
 * @Security("has_role('ROLE_HOTLINE_REQUEST_POST')")
 */
public function editAction(Request $request): Response
{
}

Base legacy

Nouvelle base

class RoleDecoder
{
    /**
     * Encode an array to a string
     *  [
     *      ROLE_CE_LISTE_READ_ONLY,
     *      ROLE_GESTION_PASSERELLE_UPDATE,
     *      ROLE_GESTION_MARQUE_BLANCHE_UPDATE,
     *  ].
     *
     * "|ce_liste=c|gestion_passerelle=m|gestion_marque_blanche=m|"
     */
    public function encode(array $roles): string
    {
        $legacyRight = self::BEGIN_LEGACY_PERMISSION;

        foreach ($roles as $role) {
            $rolesToAdd[] = $role;

            foreach ($rolesToAdd as $roleToAdd) {
                $roleToAdd = str_replace('ROLE_', '', $roleToAdd);

                if (0 < strpos($roleToAdd, '_READ_ONLY')) {
                    $legacyRight .= self::formatterLegacyPermission($roleToAdd, '_READ_ONLY',
                        strtolower('C'));
                } elseif (0 < strpos($roleToAdd, '_UPDATE')) {
                    $legacyRight .= self::formatterLegacyPermission($roleToAdd, '_UPDATE',
                        strtolower('M'));
                }
            }
        }

        return $legacyRight;
    }
}
// Legacy
"|onglet_parametres_acances=m|ce_liste=c|" 


// Symfony4
['ROLE_ONGLET_PARAMETRES_AVANCES_UPDATE', 'ROLE_CE_LISTE_READ_ONLY']

Cas d'étude
Première migration

La migration, par où commence-t-on ?

Scrum, mais pas forcément de SPECS

Première étape

Reverse engineering du legacy (symfony1)

Legacy - Type de notification

Legacy - Permissions utilisateurs

Deux options

  • On migre tout maintenant
Estimation: il faudra {{ random(20) + 20 }} jours 
+ 3 jours de récup 
  • On migre pas, enfin on migrera plus tard...

Solution

Pas de migration, on va appeler le legacy

// src/LegacyBundle/Kernel/Symfony10Kernel.php

class Symfony10Kernel extends LegacyKernel
{
    public function handle(Request $request)
    {
    }

    public function notify($object)
    {
    }

    // ...
}

Comment ?

Deuxième étape

Identifier le code legacy que l'on souhaite appeler

  // legacy/plugins/appVeolysHotlineModule/lib/model/HotlineRequest.php

  public function notifyChange()
  {
    // ...

    $broadcastList = $this->getOrCreateUserBroadcastList();
    $broadcastList->notify($this)
  }
// src/LegacyBundle/Kernel/Symfony10Kernel.php

class Symfony10Kernel extends LegacyKernel
{
    /**
     * Call the notify method from the legacy UserBroadCastList model.
     */
    public function notify($objectSF4)
    {
        // Map the doctrine entity to the propel model
        $legacyObject = $this->transformToLegacy($objectSF4);

        // Call the symfony1 method to get a UserBroadcastList
        $userBroadCastList = $legacyObject->getOrCreateUserBroadcastList();

        // Call the legacy method notify
        try {
            $broadCastList->notify($legacyObject);
        } catch (\Exception $e) {
        }
    }

    // ...
}

Prototype

Workflow

namespace AppBundle\Event;

class NotifyObjectEvent extends Event
{
    const NAME = 'veolys.notify_object';

    protected $object;

    public function __construct(NotifiableObjectInterface $object)
    {
        $this->object = $object;
    }

    public function getObject()
    {
        return $this->object;
    }
}

Evènement

class NotifyObjectListener implements EventSubscriberInterface
{
    /** @var LoggerInterface */
    protected $logger;

    /** @var Symfony10Kernel */
    protected $kernel;

    /** @var Request */
    protected $request;

    public function notify(NotifyObjectEvent $event)
    {
        $object = $event->getObject();
        if (!$this->supports($object)) {
            return;
        }

        $this->kernel->notify($this->request, $object);

        $this->logger->notice('Legacy notification', [
            'object' => get_class($object),
        ]);
    }

    public function supports($object): bool
    {
        return $object instanceof NotifiableObjectInterface;
    }
}

EventListener

Fin 

Bonus

// src/LegacyBundle/Kernel/Symfony10Kernel.php

class Symfony10Kernel extends LegacyKernel
{
    /**
     * Call the notify method from the legacy UserBroadCastList model.
     */
    public function notify(Request $request, NotifiableObjectInterface $object)
    {
        // Map the doctrine entity to the propel model
        $legacyObject = $this->transformToLegacy($object);

        // Call the symfony1 method to get a UserBroadcastList
        $userBroadCastList = $legacyObject->getOrCreateUserBroadcastList();

        // Call the legacy method notify
        try {
            $broadCastList->notify($legacyObject);
        } catch (\Exception $e) {
        }
    }

    // ...
}

Prototype (rappel)

/** @var NotifiableObjectInterface $object */
$id = $object->getId();

$legacyRepositoryName = sprintf('%sPeer', (new \ReflectionClass($object))->getShortName());
if (!class_exists($legacyRepositoryName)) {
    throw new \Exception(sprintf('Invalid legacy repository peer : %s', $legacyRepositoryName));
}

$legacyObject = $legacyRepositoryName::retrieveByPK($id);
if (empty($legacyObject)) {
    throw new \Exception(sprintf('Can\'t found the object with id: %s and legacy repository peer: %s', 
        $id, $legacyRepositoryName));
}

if (!$legacyObject instanceof \Notifiable) {
    throw new \Exception(sprintf('The object % should implement Notifiable interface from legacy', 
        get_class($legacyObject)));
}

// Call the method with Reflection Api because the method getOrCreateUserBroadcastList can be private...
$method = new \ReflectionMethod(get_class($legacyObject), 'getOrCreateUserBroadcastList');
$method->setAccessible(true);

/** @var \UserBroadcastList $broadCastList */
$broadCastList = $method->invoke($legacyObject);
public function notify(Request $request, NotifiableObjectInterface $object)

Passer de Doctrine à Propel

Migration progressive 2020 Updated

By skigun

Migration progressive 2020 Updated

  • 307