$30 off During Our Annual Pro Sale. View Details »

Bringing Symfony Components into Your Legacy Code

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.

Hugo Hamon

February 09, 2013
Tweet

More Decks by Hugo Hamon

Other Decks in Technology

Transcript

  1. Bringing the Symfony
    Components into your
    legacy code.
    Hugo HAMON – TNGTech- Munich 2013

    View Slide

  2. About me…
    Hugo HAMON
    Head of training at SensioLabs
    Book author
    Speaker at many conferences
    Symfony contributor
    @hhamon

    View Slide

  3. SensioLabs
    We’re hiring
    Symfony and
    PHP top stars.

    View Slide

  4. Introduction
    Living with legacy…

    View Slide

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

    View Slide

  6. What’s legacy code?
    Legacy code often
    relates to source
    code, which is not
    supported anymore
    or that has to be
    maintained.

    View Slide

  7. Full rewrite
    o  Time consuming
    o  Maintain 2 softwares
    o  Affects business
    o  Costly
    o  Risky…

    View Slide

  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

    View Slide

  9. Code lifting

    View Slide

  10. Have a plan!
    o  Decide which parts to migrate
    o  Choose your libraries
    o  Write tests

    View Slide

  11. The legacy application

    View Slide

  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

    View Slide

  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
    ...

    View Slide

  14. Metrics

    View Slide

  15. View Slide

  16. The good things K
    Lots of comments
    Cohesive organization
    Controllers & Views
    OOP libraries (PDO)
    Apache URL rewriting

    View Slide

  17. The Symfony2 Components
    Config
    Console
    DependencyInjection
    EventDispatcher
    Filesystem
    HttpFoundation
    HttpKernel
    Locale
    Form
    Validator
    Routing
    Templating
    Translation
    Yaml

    View Slide

  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

    View Slide

  19. Foundation: 3rd party libraries
    •  Swiftmailer
    •  GeSHi
    •  Markdown_Extra
    •  Markdownify
    •  Imagine
    •  Symfony FrameworkBundle

    View Slide

  20. Migration plan
    1.  Setup the migration
    2. Keeping backward compatibility
    3. Plug-in code with events
    4. Routing incoming requests
    5. Separating concerns

    View Slide

  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

    View Slide

  22. 1st Step
    Set up the migration

    View Slide

  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/

    View Slide

  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());

    View Slide

  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

    View Slide

  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.*"
    }
    }

    View Slide

  27. 2nd Step
    Keeping backward compatibility

    View Slide

  28. Keeping backward compatibility

    View Slide

  29. MainKernel
    LegacyKernel
    Legacy Request
    HTTP Request
    HTTP Response

    View Slide

  30. MainKernel
    LegacyKernel
    Symfony Request
    HTTP Request
    HTTP Response

    View Slide

  31. Leveraging the power of HTTP
    HttpKernel
    + HttpFoundation
    + EventDispatcher
    = Request/Response framework

    View Slide

  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
    );
    }

    View Slide

  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());
    }
    }

    View Slide

  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;
    }
    }

    View Slide

  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('...');
    }
    }

    View Slide

  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();

    View Slide

  37. Leveraging the built-in HTTP Kernel
    o  Request / Response handling
    o  Errors management
    o  Events management
    o  Extensibility
    o  Decoupling

    View Slide

  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);

    View Slide

  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);
    }
    }

    View Slide

  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);
    }
    }

    View Slide

  41. 3rd Step
    Plug-in code with events

    View Slide

  42. The kernel events  
    kernel.request
    kernel.controller
    kernel.view
    kernel.response
    Request
    Response
    handle()
    L1  
    L2  
    L3  
    L4  
    L5  
    L6  

    View Slide

  43. Leveraging the request event  
    kernel.request
    kernel.controller
    Request
    Request
    LegacyListener
    ...
    ...

    View Slide

  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),
    );
    }
    }

    View Slide

  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);
    }
    }

    View Slide

  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);
    // [...]
    }
    }

    View Slide

  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);
    }
    }

    View Slide

  48. 4th Step
    Routing incoming requests

    View Slide

  49. The Routing Component  
    Maps an http
    request to a set
    of configuration
    variables.

    View Slide

  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

    View Slide

  51. Easy routes matching  
    kernel.request
    kernel.controller
    Request
    Request
    LegacyListener
    RouterListener
    ...

    View Slide

  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));
    }
    }

    View Slide

  53. 5th Step
    Towards separation of concerns

    View Slide

  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));
    }
    }

    View Slide

  55. Templating  
    •  Pure PHP templates
    •  Escaping helper
    •  Template inheritance
    •  Slots
    •  Helpers
    •  Extensible

    View Slide

  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,
    ));

    View Slide

  57. extend('layout') ?>
    set('_title', '...') ?>
    set('_description', '...') ?>
    Les actualités de PHP



    escape($news['titre']) ?>




    Par escape($news['login']) ?>
    le formatDate($news['datePosted']) ?>.

    glossary($news['corps']) ?>


    View Slide

  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'));

    View Slide

  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();

    View Slide

  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

    View Slide

  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();

    View Slide

  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);

    View Slide

  63. 6th Step
    Managing the configuration

    View Slide

  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.

    View Slide

  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: [email protected]
    contact_recipient: [email protected]
    locale: fr_FR
    The application’s configuration  

    View Slide

  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  

    View Slide

  67. The DI Component  
    o  Global configuration parameters
    o  Lazy loaded services
    o  PHP
    , YAML or XML configuration
    o  Inversion of control

    View Slide

  68. Configuring services  
    // main/MainKernel.php
    $legacyKernel = new LegacyKernel();
    $legacyListener = new LegacyListener($legacyKernel);


    class="Application\HttpKernel\Listener\LegacyListener">


    View Slide

  69. Configuring services  






    // main/MainKernel.php
    $dispatcher = new EventDispatcher();
    $dispatcher->addSubscriber($legacyListener);

    View Slide

  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;
    }
    }

    View Slide

  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  

    View Slide

  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);
    // [...]
    }
    }

    View Slide

  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');
    }

    View Slide

  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();
    }

    View Slide

  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(),
    )));
    }

    View Slide

  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);
    }
    }

    View Slide

  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

    View Slide

  78. 7th Step
    Handling all errors

    View Slide

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

    View Slide

  80. Register the exception listener  




    Symfony\Component\HttpKernel\EventListener\ExceptionListener


    Application\Controller\ErrorController::exceptionAction




    %exception_listener.exception_controller%



    View Slide

  81. Register the exception listener  











    View Slide

  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));
    }
    }

    View Slide

  83. 8th Step
    Migration forms & validation

    View Slide

  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

    View Slide

  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())
    ));
    }
    }

    View Slide

  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'),
    ));
    }
    }

    View Slide

  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(),
    ));
    }

    View Slide

  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",
    )));
    // [...]
    }
    }

    View Slide

  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'),
    )));
    }
    }

    View Slide

  90. 9th Step
    Authenticating users

    View Slide

  91. Authentication & Authorization  
    o Handling signin and signout
    o Restricting access to non authenticated users
    o Restricting access to the admin area

    View Slide

  92. 1st security level: request event  
    kernel.request
    kernel.controller
    Request
    Request
    ...
    AuthenticationListener
    FirewallListener

    View Slide

  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);
    }
    }
    }

    View Slide

  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();
    }
    }
    }
    }

    View Slide

  95. 10th Step
    Building utility tools

    View Slide

  96. The Console main script  
    #!/usr/bin/env 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);

    View Slide

  97. View Slide

  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)
    ;
    }
    }

    View Slide

  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(
    '[info] %u/%u items updated (%u skipped).',
    $stats['updated'],
    $stats['total'],
    $stats['skipped']
    ));
    }
    }

    View Slide

  100. In the end
    Analysis & future plans

    View Slide

  101. Quality
    Metrics  

    View Slide

  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

    View Slide

  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

    View Slide

  104. Thank you!
    Hugo Hamon
    https://joind.in/talk/view/7918
    [email protected]
    @hhamon

    View Slide