Retour d'expérience - Veolys
D'une migration progressive
Un outils de gestion d'ensemble immobilier
1er commit git- Gilles 2007
(une précédente version éxistait)
Legacy: 200 000 lignes de codes
Symfony3: 65 000 lignes de codes
500 routes hors webservice/api
PHP 5.3
Apache 2.2
Mysql 5.5
Memcache
PHP 7
Apache 2.4
Percona Server - Mysql 5.7
Redis
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
Maintenir une application PHP tiers au sein d'une application Symfony
<?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);
Symfony2
Symfony2
symfony1
Symfony3
Supporte: symfony1.4 / symfony1.5 / CodeIgniter
et custom kernel
Symfony3 cherche un controller
qui match la request
HTTP Request
Si aucune route Sf3, 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;
}
}
}
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);
}
// ...
}
// \sfContext
# 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%'
Lecture de session: TheodoEvolutionSessionBundle
Authentification:
PreAuthenticatedListener custom
Authentification géré par l'application legacy
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];
}
}
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];
}
}
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)
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;
}
}
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
{
}
Reverse engineering du legacy (symfony1)
Estimation: il faudra {{ random(20) + 20 }} jours
+ 3 jours de récup
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 ?
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($objectSF3)
{
// Map the doctrine entity to the propel model
$legacyObject = $this->transformToLegacy($objectSF3);
// Call the symfony1 method to get a UserBroadcastList
$userBroadCastList = $legacyObject->getOrCreateUserBroadcastList();
// Call the legacy method notify
try {
$broadCastList->notify($legacyObject);
} catch (\Exception $e) {
}
}
// ...
}
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;
}
}
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;
}
}
// 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) {
}
}
// ...
}
/** @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)