Bringing Symfony Components into Your Legacy Code

E2ed7c278c8c49bb3e7fe0b7de039997?s=47 Hugo Hamon
February 09, 2013

Bringing Symfony Components into Your Legacy Code

The same question comes every time when dealing with legacy code: shall we maintain or rewrite the whole application? Tough decision to take! Maintaining the legacy code or rewriting it from scratch may cost lots of money… Another solution would be to migrate the code from step to step by introducing new valuable pieces of software like Drupal does for their upcoming major release. This talk will explain how you can take benefit from famous Symfony2 standalone components to empower and modernize your legacy code. Reusing well known and tested Symfony components will also guarantee a smooth learning curve and the support from the biggest PHP developers community.

E2ed7c278c8c49bb3e7fe0b7de039997?s=128

Hugo Hamon

February 09, 2013
Tweet

Transcript

  1. Bringing the Symfony Components into your legacy code. Hugo HAMON

    – TNGTech- Munich 2013
  2. About me… Hugo HAMON Head of training at SensioLabs Book

    author Speaker at many conferences Symfony contributor @hhamon
  3. SensioLabs We’re hiring Symfony and PHP top stars.

  4. Introduction Living with legacy…

  5. How many of you still have to deal with legacy

    code?
  6. What’s legacy code? Legacy code often relates to source code,

    which is not supported anymore or that has to be maintained.
  7. Full rewrite o  Time consuming o  Maintain 2 softwares o 

    Affects business o  Costly o  Risky…
  8. Smooth migration o Keeping backward compatibility o Keeping the application live o Reducing

    impacts on the business o Mastering the codebase from A to Z o Learning new things everytime
  9. Code lifting

  10. Have a plan! o  Decide which parts to migrate o 

    Choose your libraries o  Write tests
  11. The legacy application

  12. The main features… J o News o Tutorials o Snippets o Forums o Guestbook o Contact

    form o Static pages o Users management o Admin area o Notifications
  13. The main issues… No separation of concerns Mixed html+sql Custom

    error handler Mixed procedural and OOP Everything under the web root directory No tests Duplicated code everywhere! Singletons Security issues No caching layer Lack of consistency ...
  14. Metrics

  15. None
  16. The good things K Lots of comments Cohesive organization Controllers

    & Views OOP libraries (PDO) Apache URL rewriting
  17. The Symfony2 Components Config Console DependencyInjection EventDispatcher Filesystem HttpFoundation HttpKernel

    Locale Form Validator Routing Templating Translation Yaml
  18. Why the Symfony2 Components? o Well tested o Well documented o Easy to

    use o Decoupled o PHP 5.4 compatible o Composer ready
  19. Foundation: 3rd party libraries •  Swiftmailer •  GeSHi •  Markdown_Extra

    •  Markdownify •  Imagine •  Symfony FrameworkBundle
  20. Migration plan 1.  Setup the migration 2. Keeping backward compatibility 3. Plug-in

    code with events 4. Routing incoming requests 5. Separating concerns
  21. Migration plan 6. Managing the configuration 7.  Handling client and server

    errors 8. Migrating forms and validation 9. Authenticating & authorizing users 10.  Building command line utility tools
  22. 1st Step Set up the migration

  23. ├── controllers/ ├── css/ ├── design/ ├── downloads/ ├── errors/

    ├── images/ ├── includes/ ├── index.php ├── javascript/ ├── logout.php ├── maintenance.html ├── syndication/ └── views/ ├── legacy/ │ ├── controllers/ │ ├── includes/ │ └── views/ ├── main/ ├── src/ ├── tests/ ├── vendor/ └── web/ ├── css/ ├── design/ ├── downloads/ ├── images/ ├── index.php ├── javascript/ ├── logout.php ├── maintenance.html └── syndication/
  24. Update all paths // index.php define('LEGACY_DIR', realpath(__DIR__.'/../legacy')); define('LIB_DIR', LEGACY_DIR.'/includes')); define('VIEWS_DIR',

    LEGACY_DIR.'/views')); require(LIB_DIR.'/fonctions/librairie-fonctions.inc.php'); require(LIB_DIR.'/configuration/configuration.inc.php'); // ... // configuration.inc.php set_include_path(LEGACY_DIR.':'.get_include_path());
  25. ├── composer.json ├── main/ │ ├── MainKernel.php │ ├── cache/

    │ ├── config/ │ ├── console │ ├── data/ │ ├── logs/ │ ├── phpunit.xml │ ├── sql/ │ └── views/ ├── src/ │ └── Application/ ├── tests/ │ ├── Application/ │ └── Fixtures/ ├── vendor/ └── web/ The application (config, cache…) The codebase Public files
  26. Install required dependencies { "autoload": { "psr-0": { "Application": {

    "src/" } } }, "require": { "symfony/http-kernel": "2.3.*", "symfony/templating": "2.3.*", "symfony/routing": "2.3.*", "symfony/config": "2.3.*", "symfony/yaml": "2.3.*" } }
  27. 2nd Step Keeping backward compatibility

  28. Keeping backward compatibility

  29. MainKernel LegacyKernel Legacy Request HTTP Request HTTP Response

  30. MainKernel LegacyKernel Symfony Request HTTP Request HTTP Response

  31. Leveraging the power of HTTP HttpKernel + HttpFoundation + EventDispatcher

    = Request/Response framework
  32. The HTTP Kernel interface namespace Symfony\Component\HttpKernel; interface HttpKernelInterface { const

    MASTER_REQUEST = 1; const SUB_REQUEST = 2; public function handle( \Symfony\Component\HttpFoundation\Request $request, $type = self::MASTER_REQUEST, $catch = true ); }
  33. # main/LegacyKernel.php // [...] require_once(LIB_DIR.'/fonctions/librairie-fonctions.inc.php'); require_once(LIB_DIR.'/configuration/configuration.inc.php'); class LegacyKernel implements HttpKernelInterface

    { public function handle(Request $request, ...) { // [...] ob_start(); require_once(VIEWS_DIR.'/page/v-header.php'); require_once($view); require_once(VIEWS_DIR.'/page/v-footer.php'); return new Response(ob_get_clean()); } }
  34. The new application’s kernel // main/MainKernel.php use Symfony\Component\HttpKernel\HttpKernelInterface; use Symfony\Component\HttpFoundation\Request;

    use Symfony\Component\HttpFoundation\Response; class MainKernel implements HttpKernelInterface { private $environment; private $legacyKernel; public function __construct($environment, LegacyKernel $kernel) { $this->environment = $environment; $this->legacyKernel = $kernel; } }
  35. The new application’s kernel # main/MainKernel.php class MainKernel implements HttpKernelInterface

    { public function handle(Request $request, $type = ..., $catch = true) { // Let the legacy kernel handle the legacy request // Legacy urls have a ?page GET query string parameter if ($request->query->has('page')) { return $this->legacyKernel->handle($request, $type, $catch); } // No it's a new migrated feature // [...] return new Response('...'); } }
  36. The front controller # web/index.php require_once __DIR__.'/../vendor/autoload.php'; require_once __DIR__.'/../main/MainKernel.php'; require_once

    __DIR__.'/../main/LegacyKernel.php'; use Symfony\Component\HttpFoundation\Request; $request = Request::createFromGlobals(); $kernel = new MainKernel('prod', new LegacyKernel()); $response = $kernel->handle($request); $response->prepare($request); $response->send();
  37. Leveraging the built-in HTTP Kernel o  Request / Response handling

    o  Errors management o  Events management o  Extensibility o  Decoupling
  38. Leveraging the built-in HTTP Kernel use Symfony\Component\EventDispatcher\EventDispatcher; use Symfony\Component\HttpKernel\HttpKernel; use

    Symfony\Component\HttpKernel\Controller\ControllerResolver; $dispatcher = new EventDispatcher(); $resolver = new ControllerResolver(); $kernel = new HttpKernel($dispatcher, $resolver); $response = $kernel->handle($request);
  39. class MainKernel implements HttpKernelInterface { // [...] private $httpKernel; private

    $legacyKernel; public function __construct($environment, LegacyKernel $kernel) { // [...] $dispatcher = new EventDispatcher(); $resolver = new ControllerResolver(); $this->httpKernel = new HttpKernel($dispatcher, $resolver); } }
  40. The built-in HTTP Kernel   class MainKernel implements HttpKernelInterface {

    // ... public function handle(Request $request, $type = ..., $catch = true) { if ($request->query->has('page')) { return $this->legacyKernel->handle($request, $type, $catch); } return $this->httpKernel->handle($request, $type, $catch); } }
  41. 3rd Step Plug-in code with events

  42. The kernel events   kernel.request kernel.controller kernel.view kernel.response Request Response

    handle() L1   L2   L3   L4   L5   L6  
  43. Leveraging the request event   kernel.request kernel.controller Request Request LegacyListener

    ... ...
  44. Wrapping the legacy code in a listener   class LegacyListener

    implements EventSubscriberInterface { private $legacyKernel; public function __construct(LegacyKernel $legacyKernel) { $this->legacyKernel = $legacyKernel; } public static function getSubscribedEvents() { return array( KernelEvents::REQUEST => array('onKernelRequest', 512), ); } }
  45. Wrapping the legacy code in a listener   public function

    onKernelRequest(GetResponseEvent $event) { // The legacy kernel only deals with master requests. if (HttpKernelInterface::MASTER_REQUEST !== $event->getRequestType()) { return; } // Let the wrapped legacy kernel handles the legacy request. // Setting a response in the event will directly jump to the // response event. $request = $event->getRequest(); if ($request->query->has('page')) { $response = $this->legacyKernel->handle($request); $event->setResponse($response); } }
  46. use Application\Kernel\EventListener\LegacyListener; use Symfony\Component\HttpKernel\EventListener\ResponseListener; class MainKernel implements HttpKernelInterface { //

    [...] public function __construct($environment) { // [...] $legacyKernel = new LegacyKernel(); $legacyListener = new LegacyListener($legacyKernel); $responseListener = new ResponseListener('UTF-8'); $dispatcher = new EventDispatcher(); $dispatcher->addSubscriber($legacyListener); $dispatcher->addSubscriber($responseListener); // [...] } }
  47. Refactoring the main kernel class   class MainKernel implements HttpKernelInterface

    { [...] public function handle(Request $request, ...) { // Just forward the request to the HTTP kernel. // The latter will trigger all events and registered // listeners will respond to them. return $this->httpKernel->handle($request, $type, $catch); } }
  48. 4th Step Routing incoming requests

  49. The Routing Component   Maps an http request to a

    set of configuration variables.
  50. Routes configuration   # main/config/routing.yml homepage: path: / defaults: _controller:

    Application\Controller\MainController::indexAction tutorials: path: /tutoriels.html defaults: _controller: Application\Controller\TutorialController::indexAction forums: path: /forums.html defaults: _controller: Application\Controller\ForumController::indexAction page: 1
  51. Easy routes matching   kernel.request kernel.controller Request Request LegacyListener RouterListener

    ...
  52. use Symfony\Component\Config\FileLocator; use Symfony\Component\Routing\Matcher\UrlMatcher; use Symfony\Component\Routing\RequestContext; use Symfony\Component\Routing\Loader\YamlFileLoader; use Symfony\Component\HttpKernel\EventListener\RouterListener;

    class MainKernel implements HttpKernelInterface { public function __construct($environment) { // [...] $locator = new FileLocator(array(__DIR__.'/config')); $loader = new YamlFileLoader($locator); $context = new RequestContext(); $matcher = new UrlMatcher($loader->load('routing.yml'), $context); // [...] $dispatcher->addSubscriber(new RouterListener($matcher, $context)); } }
  53. 5th Step Towards separation of concerns

  54. Controllers   namespace Application\Controller; use Symfony\Component\HttpFoundation\Request; class NewsController extends Controller

    { public function indexAction(Request $request) { $page = $request->attributes->get('page'); $pager = $this->getMapper('News')->getPaginatedNews(5, $page); return $this->render('news/index', array('pager' => $pager)); } }
  55. Templating   •  Pure PHP templates •  Escaping helper • 

    Template inheritance •  Slots •  Helpers •  Extensible
  56. The templating engine   $templating = new PhpEngine( new TemplateNameParser(),

    new FilesystemLoader(array(__DIR__.'/views/%name%.php')) ); $templating->set(new SlotsHelper()); $templating->set(new TextHelper()); $templating->set(new RouterHelper($router)); $templating->set(new ActionsHelper($this)); $templating->render('snippet/index', array( 'snippets' => $snippets, ));
  57. <?php $view->extend('layout') ?> <?php $view['slots']->set('_title', '...') ?> <?php $view['slots']->set('_description', '...')

    ?> <h2>Les actualités de PHP</h2> <?php foreach ($pager as $news) : ?> <h3> <a href="<?php echo $view['router']->path('read_news', array( 'id' => $news['idNews'], 'slug' => $view['text']->slugify($news['titre']) )) ?>"> <?php echo $view->escape($news['titre']) ?> </a> </h3> <div id="actualite<?php echo $news['idNews'] ?>"> <p class="texteADroite"> Par <?php echo $view->escape($news['login']) ?> le <?php echo $view['date']->formatDate($news['datePosted']) ?>. </p> <?php echo $view['text']->glossary($news['corps']) ?> </div> <?php endforeach ?>
  58. Templates helpers   // Text helpers $view['text']->truncate('a string'); $view['text']->slugify('the title');

    // Number helpers $view['number']->formatNumber(26257); // Date helpers $view['date']->formatDate('2013-02-09'); $view['date']->formatAtom('2013-02-09'); $view['date']->formatRss('2013-02-09'); // Routing helpers $view['router']->path('article', array('id' => 3)); $view['router']->url('activate', array('token' => 'abcdef123'));
  59. Templates helpers   // Actions helpers $view['actions']->render('Comment:recent'); // Session helpers

    $view['session']->get('var'); $view['session']->hasFlash('notice'); $view['session']->getFlash('notice'); // Security helpers $view['security']->isAuthenticated(); $view['security']->isAdmin(); $view['security']->getUsername(); $view['security']->getUuid();
  60. The Model   o  Encapsulating SQL queries and business logic

    o  Dealing with objects instead of arrays o  Decoupling data from their storage engine o  Removing code duplications
  61. Database Mappers   use Application\Mapper\MapperFactory; $factory = new MapperFactory(new \PDO('...'));

    $mapper = $factory->getMapper('Tutorial'); $tutorials = $mapper->findAll(); $tutorial = $mapper->find(42); $tutorials = $mapper->getPaginatedTutorials(10); $tutorial = $mapper->getRandomTutorial();
  62. Entities   $tutorial = $tutorialMapper->createObject(); $tutorial->setTitle('The basics of PHP'); $tutorial->setAuthor('Hugo

    Hamon'); $tutorial->setCreatedAt(\new DateTime()); $tutorial->setActive(true); $mapper->insert($tutorial); $mapper->update($tutorial);
  63. 6th Step Managing the configuration

  64. The application’s configuration   The application’s configuration often changes depending

    on the runtime environment. It should be easy to tweak the whole configuration without touching a single line of code.
  65. # main/config/config.yml parameters: kernel.secret: changeme database_dsn: mysql:host=localhost;dbname=apprendre_v2 database_user: root database_password:

    ~ sender_name: Apprendre-PHP sender_email: webmaster@apprendre-php.com contact_recipient: webmaster@apprendre-php.com locale: fr_FR The application’s configuration  
  66. # main/config/config_dev.yml imports: - { resource: config.yml } parameters: router.request_context.host:

    v2.apprendre-php.local mailer.transport.class: Swift_NullTransport # main/config/config_prod.yml imports: - { resource: config.yml } parameters: kernel.secret: djwehcnjbwkfbwlcewf database_user: my_production_user database_password: my_production_password Environment configuration  
  67. The DI Component   o  Global configuration parameters o  Lazy

    loaded services o  PHP , YAML or XML configuration o  Inversion of control
  68. Configuring services   // main/MainKernel.php $legacyKernel = new LegacyKernel(); $legacyListener

    = new LegacyListener($legacyKernel); <!-- main/config/services/legacy.xml --> <service id="legacy_kernel" class="LegacyKernel"/> <service id="http_kernel.legacy_listener" class="Application\HttpKernel\Listener\LegacyListener"> <argument type="service" id="legacy_kernel"/> </service>
  69. Configuring services   <!-- main/config/services/dispatcher.xml --> <service id="event_dispatcher" class="%event_dispatcher.class%"> <call

    method="addSubscriber"> <argument type="service" id="http_kernel.legacy_listener"/> </call> </service> // main/MainKernel.php $dispatcher = new EventDispatcher(); $dispatcher->addSubscriber($legacyListener);
  70. class MainKernel implements HttpKernelInterface { private $container; public function getContainer()

    { if ($this->container) { return $this->container; } // Generate the container class if it doesn't already exist yet if (!file_exists($path = $this->getContainerFilePath())) { $this->buildContainer(); } // Create a unique instance of the container require_once $path; $class = $this->getContainerClass(); $this->container = new $class(); return $this->container; } }
  71. class MainKernel implements HttpKernelInterface { // [...] protected function getContainerClass()

    { return sprintf('Main%sContainer', ucfirst($this->environment)); } protected function getContainerFilePath() { return $this->getCacheDir().'/'.$this->getContainerClass().'.php'; } } Configure container’s location  
  72. Building the container   use Symfony\Component\DependencyInjection\ContainerBuilder; class MainKernel implements HttpKernelInterface

    { protected function buildContainer() { $dic = new ContainerBuilder(); $dic->setParameter('kernel.environment', $this->environment); $dic->setParameter('kernel.cache_dir', $this->getCacheDir()); $dic->setParameter('kernel.root_dir', $this->getRootDir()); $dic->setParameter('vendor.root_dir', $this->getVendorDir()); $dic->set('kernel', $this); // [...] } }
  73. Building the container   use Symfony\Component\Config\FileLocator; use Symfony\Component\DependencyInjection\Loader\XmlFileLoader; protected function

    buildContainer() { // [...] $locator = new FileLocator(array( $this->getRootDir().'/config/services', $this->getRootDir().'/config', )); $loader = new XmlFileLoader($dic, $locator); $loader->load('database.xml'); // [...] $loader->load('dispatcher.xml'); }
  74. Environment configuration   protected function buildContainer() { // [...] //

    Load the application’s configuration to override // the default services definitions configuration per // environment. $loader = new YamlFileLoader($dic, $locator); $loader->load('config_'.$this->environment.'.yml'); $dic->compile(); }
  75. Dumping & caching the container   protected function buildContainer() {

    // [...] $target = $this->getContainerFilePath(); $folder = pathinfo($target, PATHINFO_DIRNAME); if (!file_exists($folder)) { $filesystem = $dic->get('filesystem'); $filesystem->mkdir($folder); } $dumper = new PhpDumper($dic); file_put_contents($target, $dumper->dump(array( 'class' => $this->getContainerClass(), ))); }
  76. Refactor the kernel class   class MainKernel implements HttpKernelInterface {

    public function handle(Request $request, ...) { // Service container is lazy loaded. $dic = $this->getContainer(); if (self::MASTER_REQUEST === $type) { \Locale::setDefault($dic->getParameter('locale')); } return $dic->get('http_kernel')->handle($request, $type, $catch); } }
  77. 38 defined services   avatar_manager controller_resolver database_connection event_dispatcher filesystem form.csrf_provider

    form.factory form.factory_builder forums.answer_notifier http_kernel http_kernel.listener.authentication http_kernel.listener.controller http_kernel.listener.exception http_kernel.listener.firewall http_kernel.listener.router http_kernel.listener.session image_manager imagine mailer mailer.message_factory mapper_factory markdown.converter markdown.parser router router.url_matcher security.authentication_manager security.context security.password_encoder session snippet_type syntax_highlighter templating translator tutorial_type validator validator.constraint.unique validator.constraint.user_password service_container
  78. 7th Step Handling all errors

  79. Handling errors   kernel.exception kernel.response Exception Response ExceptionListener CustomErrorListener ...

  80. Register the exception listener   <?xml version="1.0" ?> <container> <parameters>

    <parameter key="exception_listener.class"> Symfony\Component\HttpKernel\EventListener\ExceptionListener </parameter> <parameter key="exception_listener.exception_controller"> Application\Controller\ErrorController::exceptionAction </parameter> </parameters> <services> <service id="exception_listener" class="%exception_listener.class%"> <argument>%exception_listener.exception_controller%</argument> </service> </services> </container>
  81. Register the exception listener   <?xml version="1.0" encoding="utf-8"?> <container> <services>

    <service id="event_dispatcher" class="%event_dispatcher.class%"> <!-- ... --> <call method="addSubscriber"> <argument type="service" id="exception_listener"/> </call> </service> </services> </container>
  82. Handling errors   class ErrorController extends Controller { public function

    exceptionAction(FlattenException $e) { if ('dev' === $this->container->getParameter('kernel.environment')) { return $this->render('error/exception', array('exception' => $e)); } $code = $e->getStatusCode(); $view = 'error/error'; if (in_array($code, array(401, 403, 404, 405))) { $view .= $code; } return $this->render($view, array('exception' => $e)); } }
  83. 8th Step Migration forms & validation

  84. Form & validator components   o Ease form configuration o Ease form

    rendering o Data mapping with objects o Automatic data validation o PHP , YAML and XML configuration
  85. Migrating forms   class RegistrationType extends AbstractType { public function

    buildForm(FormBuilderInterface $builder, array $options) { $builder ->add('username') ->add('rawPassword', 'repeated', array('type' => 'password')) ->add('email', 'email') ->add('website', 'url', array('required' => false)) ->add('rules', 'checkbox', array( 'mapped' => false, 'constraints' => array(new Assert\True()) )); } }
  86. Migrating forms   class RegistrationType extends AbstractType { public function

    getName() { return 'registration'; } public function setDefaultOptions(OptionsResolverInterface $resolver) { $resolver->setDefaults(array( 'data_class' => 'Application\Domain\User', 'validation_groups' => array('Registration'), )); } }
  87. public function signupAction(Request $request) { $mapper = $this->getMapper('User'); $user =

    $mapper->createObject(); $form = $this->createForm(new RegistrationType(), $user); if ($request->isMethod('POST')) { $form->submit($request); if ($form->isValid()) { // ... process validated data } } return $this->render('user/signup', array( 'form' => $form->createView(), )); }
  88. Handling validation   use Symfony\Component\Validator\Constraints\NotBlank; class User extends AbstractModel {

    private $username; // [...] static function loadValidatorMetadata(ClassMetadata $metadata) { $metadata->addPropertyConstraint('username', new NotBlank(array( 'groups' => array('Registration', 'Account'), 'message' => "Le nom d'utilisateur est obligatoire", ))); // [...] } }
  89. use Application\Validator\Constraints\Unique; class User extends AbstractModel { private $username; //

    [...] static function loadValidatorMetadata(ClassMetadata $metadata) { // [...] $metadata->addConstraint(new Unique(array( 'mapper' => 'User', 'field' => 'username', 'message' => "Username already exists.", 'primaryKey' => 'idMembre', 'groups' => array('Registration'), ))); } }
  90. 9th Step Authenticating users

  91. Authentication & Authorization   o Handling signin and signout o Restricting access

    to non authenticated users o Restricting access to the admin area
  92. 1st security level: request event   kernel.request kernel.controller Request Request

    ... AuthenticationListener FirewallListener
  93. The authentication listener   class AuthenticationListener implements EventSubscriberInterface { //

    [...] public function onKernelRequest(GetResponseEvent $event) { if (HttpKernelInterface::MASTER_REQUEST !== $event->getRequestType()) { return; } $request = $event->getRequest(); $session = $request->getSession(); $mapper = $this->mapperFactory->getMapper('User'); $token = $this->authenticationManager->getToken(); if ($token && $user = $mapper->getActiveUser($token->getUsername())) { $this->authenticationManager->signin($user); } } }
  94. The firewall listener   class FirewallListener implements EventSubscriberInterface { //

    [...] public function onKernelRequest(GetResponseEvent $event) { if (HttpKernelInterface::MASTER_REQUEST !== $event->getRequestType()) { return; } $request = $event->getRequest(); if (preg_match('#^/admin#', $request->getPathInfo())) { if (!$this->securityContext->isAdmin()) { throw new AccessDeniedHttpException(); } } } }
  95. 10th Step Building utility tools

  96. The Console main script   #!/usr/bin/env php <?php require __DIR__.'/../vendor/autoload.php';

    require __DIR__.'/MainKernel.php'; $input = new Symfony\Component\Console\Input\ArgvInput(); $env = $input->getParameterOption(array('--env', '-e'), 'dev'); $kernel = new MainKernel($env); $application = new Application\Console\Application($kernel->getContainer()); $application->add(new Application\Console\Command\ConvertMarkdownCommand()); $application->add(new Application\Console\Command\RestoreHtmlCommand()); $application->add(new Application\Console\Command\UpdateMessageCountCommand()); $application->run($input);
  97. None
  98. Automating tedious tasks   namespace Application\Console\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputArgument;

    use Symfony\Component\Console\Output\OutputInterface; class UpdateMessageCountCommand extends Command { protected function configure() { $this ->setName('model:user:update-messages-count') ->setDescription('Updates all users messages count') ->addArgument('username', InputArgument::OPTIONAL) ; } }
  99. Automating tedious tasks   class UpdateMessageCountCommand extends Command { protected

    function execute(InputInterface $input, OutputInterface $output) { $container = $this->getContainer(); $factory = $container->get('mapper_factory'); $mapper = $factory->getMapper('User'); $stats = $mapper->updateMessagesCount($input->getArgument('username')); $output->writeln(sprintf( '<comment>[info]</comment> %u/%u items updated (%u skipped).', $stats['updated'], $stats['total'], $stats['skipped'] )); } }
  100. In the end Analysis & future plans

  101. Quality Metrics  

  102. Future plans   o Use Twig as templating system o Use a

    more decent Model layer o Refactor some duplicated code with Traits o Add reverse proxy caching support o Implement decent logging mechanism o Add Assetic support o Switch pagination to PagerFanta
  103. Conclusion   o Successfull smooth migration o Better codebase quality o Easy to

    add new features o Custom « full stack » framework o Learnt new things o Improved my OOP skills
  104. Thank you! Hugo Hamon https://joind.in/talk/view/7918 hugo.hamon@sensiolabs.com @hhamon