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

Made with Slides.com