Context
- We have a 15 old years ecommerce project
- We have to modernize it
- We have to keep business going
- We want to use Service Oriented Architecture
Challenges
- Keeping the production up while migrating
- How to set up a migration path from a technical point a view
- How do we manage the daily basis organization (monolith vs services)
Zero downtime ?
1 - Development
2 - Delivery
3 - Deplyment
Clément Bertillon
https://twitter.com/BERTILLONClment
https://github.com/skigun
The dream
I want to do some Symfony 6 already
- Without creating a new repository
- Without touching the Legacy code
- Without waiting the end of the migration
- Without waiting to migrate a part of the application
- In a multiple decoupled (
micro)services - Still without creating any new repository
- Without deployement dependency hell
- In just one line of code
- Fuel
- Laravel
- CakePHP
- Zend1 or 2
- Home made framwork
- Slim
- symfony1
- Symfony2
What we already migrated
Ecommerce
Ecommerce
Stock
Order
Before
After
Current directory structure
... and 867 other php files
Where do we start?
Strangler Fig Pattern
# We assume our repository is inside an ecommerce directory
# We move everything in a temporary directory
$ mkdir tmp_legacy && mv ecommerce/* tmp_legacy
# From the existing repository create a new Symfony
$ symfony new ecommerce && mkdir ecommerce/legacy
# Move the whole existing legacy in a legacy folder
$ mv tmp_legacy/* ecommerce/legacy
Current directory structure
New directory structure
New structure
Symfony 6
Legacy
?
Symfony 6
Legacy
Symfony 6
Legacy
Router
HTTP Request
?
Symfony 6
Legacy
Router
HTTP Request
Controller A
Controller B
Controller C
Symfony 6
Legacy
Router
HTTP Request
Controller A
Controller B
Controller C
NotFoundException
class RouterListener
{
public function onKernelRequest(RequestEvent $event)
{
try {
$this->routerListener->onKernelRequest($event);
} catch (NotFoundHttpException $e) {
$response = $this->legacyKernel->handle($event->getRequest());
if ($response->getStatusCode() !== 404) {
$event->setResponse($response);
return $event;
}
throw $e;
}
}
}
Override Router
class LegacyKernel implements HttpKernelInterface
{
public function handle(Request $request): Response
{
return new Response('some content');
}
}
Let's wrap the legacy
class LegacyKernel implements HttpKernelInterface
{
public function handle(Request $request): Response
{
ob_start();
require __DIR__.'/legacy/some_legacy_file.php';
$response = ob_get_clean();
return new Response($response);
}
}
Let's wrap the legacy
Symfony 6
Legacy
?
Golden rule
Symfony 6
Legacy
Golden rule
X
class LegacyKernel implements HttpKernelInterface
{
public function __construct(
private ContainerInterface $container
) {}
public function handle(Request $request): Response
{
ob_start();
// find a way to inject the service container
// in the legacy application
require __DIR__.'/legacy/some_legacy_file.php';
$response = ob_get_clean();
return new Response($response);
}
}
What about using the Symfony Service Container in the legacy ?
class LegacyKernel implements HttpKernelInterface
{
public function handle(Request $request): Response
{
$container = $this->getContainer();
ob_start();
require __DIR__.'/legacy/some_legacy_file.php';
// find a way to inject the container in the legacy application
$response = ob_get_clean();
return new Response($response);
}
}
class LegacyKernel implements HttpKernelInterface
{
public function __construct(
private ContainerInterface $container
) {}
public function handle(Request $request): Response
{
ob_start();
$symfonyContainer = $this->container;
global $symfonyContainer;
require __DIR__.'/legacy/some_legacy_file.php';
$response = ob_get_clean();
return new Response($response);
}
}
In the worst case scenario
class LegacyKernel implements HttpKernelInterface
{
public function handle(Request $request): Response
{
$container = $this->getContainer();
ob_start();
global $container;
require __DIR__.'/legacy/some_legacy_file.php';
$response = ob_get_clean();
return new Response($response);
}
}
Inject the Container
<?php
// ./legacy/some_legacy_file.php
include('inclusions/config/config.inc.php');
include('inclusions/fonctions/connexion.inc.php');
include('inclusions/fonctions/fonction_other.inc.php');
include('inclusions/fonctions/formule.inc.php');
if(isset($_SESSION['hall'])) {
//if($_SESSION['hall']->check_hall())
$_SESSION['hall']->register_hall();
$notification = new SmsNotification('An SMS from the legacy but with Symfony code, awesome !');
$symfonyBus = $symfonyContainer->get('messenger')->dispatch(notification);
}
unset($_SESION['hall']);
if(isset($_SESSION['hall_form']))$script_return=$_SESSION['hall_form'].".php";
else $script_return="index2.php";
echo "<script language='javascript'>";
echo "window.location.href='$script_return';";
echo "</script>";
If we jump in any legacy code...
Use the profiler even in legacy
How do we organize our code?
Monorepo != Monolith
Why MonoRepo?
Backward resilient
Better decommissioning
Lesser PullRequest
Better Code Review
Implicit CI
Better productivity
Avoid switching repo
Better collaboration between Teams
We can fix bugs in all projects
Less Managment
Everything is centralized
Dependency managment
Code more reusable
Access Control
Better Continus Integration
Visibility
Cross Team Contribution
Single source of truth
Consistency
Shared Timeline
Atomic commits
Unified CI/CD
Unified build process
Unified deploy process
Refactoring easier
Sharing coding standard
Synchronized release
Sharing dependencies
Why Monorepo?
Version41
Application A
{
"name": "Iphone 27",
"price": 2999.00,
"colour": "red"
}
Application B
Version42
Application A
{
"name": "Iphone 27",
"price": 2999.00,
"colour": "#002222"
}
Application B
$ cd ecommerce
$ symfony new Product --no-git
$ symfony new Order --no-git
$ symfony new Stock --no-git
$ git add -A
$ git commit -m "Setup MonoRepo"
Lets create our mono repo
Mono Repository
Mono Repository
ecommerce
product
sotck
order
X
X
X
X
Mono Repository
ecommerce
product
sotck
order
Tooling
In order to protect and maintain our application
#1 CODEOWNERS
/ecommerce @global-review
/product @product-team
/stock @product-sotck
/stock @product-order @skigun
/deploy @skigun
#1 CODEOWNERS
#1 CODEOWNERS
#1 CODEOWNERS
#2 Deptrac
paths:
- ./ecommerce/src
- ./stock/src
- ./order/src
layers:
- name: Ecommerce
collectors:
- type: className
regex: .*App\\Ecommerce\\.*
- name: Stock
collectors:
- type: className
regex: .*App\\Stock\\.*
- name: Order
collectors:
- type: className
regex: .*App\\Order\\.*
#2 Deptrac
layers:
- name: Ecommerce
collectors:
- type: className
regex: .*App\\Ecommerce\\.*
- name: Stock
collectors:
- type: className
regex: .*App\\Stock\\.*
- name: PaymentBundle
collectors:
- type: className
regex: .*App\\PaymentBundle\\.*
ruleset:
Ecommerce:
- PaymentBundle
Stock:
- PaymentBundle
- Fuel
- Laravel
- CakePHP
- Zend1 or 2
- Home made framwork
- Procedural PHP
- Slim
- Symfony1
- Symfony2
Our success stories with this pattern:
Bonus
What about tracking bugs and performance issues ?
#1 Free moniting tool for dev environment
How to get a free monitoring tools for dev environment ?
# config/packages/dev/web_profiler.yaml
framework:
profiler:
only_exceptions: false
dsn: 'file:/shared_profiler_directory/var/profiler'
#1 Free moniting tool for dev environment
# config/packages/dev/web_profiler.yaml
framework:
profiler:
only_exceptions: false
dsn: 'file:/shared_profiler_directory/var/profiler'
Blackfire !
Feature: Distributed Profiling
#2 Actual real profiler
Profile
Thank you!
https://twitter.com/BERTILLONClment
https://github.com/skigun
Special thanks to Reinis and Salah <3
From monolith to SOA with zero downtime
By skigun
From monolith to SOA with zero downtime
From monolytic to SOA with zero downtime
- 918