How We Moved Away from Spryker to Symfony

Asmir Mustafic

Berlin Symfony Meetup
July 2023

(and deleted 2M lines of code)

  • 1+ bln known parts
  • 10m-40m pre-know sell-able parts
  • ~10-15 people in the main team

What is Spryker?

Project A

Why Spryker?

  • It is PHP
  • You will find an agency in Berlin
  • Some built-in stuff
  • Built on industry knowledge acquired in years
  • Standardized style

Show time (1/2)

Why not Spryker?

  • Agencies are not cheap
  • Devs are rare
  • Bloated
  • New framework to learn
  • Not invented here syndrome
  • Questionable dependency injection
  • Questionable performance
  • Questionable architecture
  • Application guidelines are over-engineered
  • Code code code code code code code code
  •  
  •  
  •  
  •  

Why not Spryker for us?

  • Did not scale (scalability issues with ~20k parts)
  • In 2017-2018 was not that mature
  • We had an uncommon UX for the feature available at that time
  • Did not talk well with other systems (poor apis)
  • Not agile? (app guidelines too strict)
  • Very few developers in US
  • No need for dynamic translations (big performance hit)
  • No need for CMS (too simple)
  •  
  •  
  •  

How we wanted to do it

  • No feature freeze
  • No maintenance screen
  • No dedicated teams
  • No green field
  • No blockers for other features

Technical Goals

  • Symfony standard project
  • Any Symfony dev should be able to contribute easily
  • Stop not-invented-here syndrome
  • Inherit most of the advantages of using symfony
    • bundles, components
    • stack overflow
    • community of developers to hire
    • fun

What was migrated already

How to move away from it?

Show time (2/2)

Steps

  1. Wrap stuff in Symfony
  2. Build new services in Symfony
  3. Share services between Yves and Symfony Yves
  4. Repeat until everything is in Symfony
<?php
class RequestHandler
{
    public function __construct(ContainerInterface $container, string $cacheDir)
    {
        $this->container = $container;
        self::$staticContainer = $container;
    }
    
    public static function getContainer(): ContainerInterface
    {
        return self::$staticContainer;
    }
    
    public function getBootstrap(): YvesBootstrap
    {
        if ($this->bootstrap) return $this->bootstrap;
        return $this->bootstrap = new YvesBootstrap();
    }
    
    public function getApplication(): Application
    {
        if ($this->app) return $this->app;

        $this->app = $this->getBootstrap()->boot();
        $this->app->boot();

        return $this->app;
    }
    
    public function handle(Request $request): Response
    {
        return $this->getApplication()->handle($request->duplicate());
    }
}
<?php
class SearchApiFactory extends AbstractFactory // Spryker app service
{
    /**
     * @return \Pyz\Client\SearchApi\Parts\PartsAutocompletionSearch
     */
    public function createPartsAutocompleteSearch(): PartsAutocompletionSearch
    {
        return new PartsAutocompletionSearch(
            SprykerFacade::getContainer()->get('guzzle_client'),
            $this->getConfig()
        );
    }
}
<?php
class CategoryFinder // Symfony app service
{
    public function __construct(SprykerFacade $spryker)
    {
        $sprkerFormFactory = $spryker->getApplication()->get('form.factory');
    }
}
<?php

class ServiceControllerResolver extends AbstractControllerResolver
{
    protected function getResolvedClassInstance()
    {
        $container = SprykerFacade::getContainer();
        $resolvedClassName = $this->resolvedClassName();
        
        if ($container->has($resolvedClassName)) {
            return $container->get($resolvedClassName);
        }

        return parent::getResolvedClassInstance();
    }
}

Symfony controller autowiring

services:
    _defaults:
        autowire: true
        autoconfigure: true
        public: false
        
    Pyz\Yves\Customer\Controller\AddressBookController:
        public: true
<?php

class AddressBookController extends AbstractCustomerController
{
    public function __construct(
        AddressRepository $addressRepository,
        AccountRepository $accountRepository
    ) {
        $this->addressRepository = $addressRepository;
        $this->accountRepository = $accountRepository;
    }
}

Symfony controller autowiring

Steps

  1. Create new API application
    1. create doctrine entities based on db schema
  2. Replace any call from Yves to Zed with calls to new API
  3. Repeat until no API calls are done to Zed
    1. Api can still tak to Zed for complex stuff (state machine)
    2. Frontend can call API directly to avoid Yves proxy for no reason

Steps

  1. Move all the emails in the new system
    • Have Spryker sending emails by calling the new system

Steps

  1. Create backend UI system (Sonata admin)
  2. Re implement everything it is use in Zed backend UI
    1. Talk to zed for complex stuff (state machine)

Steps

  1. Move state machine into new code base

State Machine (before)

State Machine (migrated)

Symfony workflow component

Done

(delete leftovers)

Was it worth ?

YES

start development

mvp release

introduction to symfony

introduction to wiliam

introduction to
xyla

removed zed

end of spryker

me

Current state of the project

  • 3 developers less
  • 1 more project
  • no vendor lock-in
    (we are not looking for spryker experience anymore)
  • simpler to reason about
  •  
  •  
  •  

Thank you!

How We Moved Away from Spryker to Symfony

By Asmir Mustafic

How We Moved Away from Spryker to Symfony

  • 445