Upgrade to Pro — share decks privately, control downloads, hide ads and more …

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. About me… Hugo HAMON Head of training at SensioLabs Book

    author Speaker at many conferences Symfony contributor @hhamon
  2. What’s legacy code? Legacy code often relates to source code,

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

    Affects business o  Costly o  Risky…
  4. 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
  5. Have a plan! o  Decide which parts to migrate o 

    Choose your libraries o  Write tests
  6. 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
  7. 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 ...
  8. The good things K Lots of comments Cohesive organization Controllers

    & Views OOP libraries (PDO) Apache URL rewriting
  9. Why the Symfony2 Components? o Well tested o Well documented o Easy to

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

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

    code with events 4. Routing incoming requests 5. Separating concerns
  12. 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
  13. ├── 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/
  14. 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());
  15. ├── 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
  16. 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.*" } }
  17. 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 ); }
  18. # 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()); } }
  19. 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; } }
  20. 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('...'); } }
  21. 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();
  22. Leveraging the built-in HTTP Kernel o  Request / Response handling

    o  Errors management o  Events management o  Extensibility o  Decoupling
  23. 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);
  24. 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); } }
  25. 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); } }
  26. 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), ); } }
  27. 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); } }
  28. 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); // [...] } }
  29. 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); } }
  30. The Routing Component   Maps an http request to a

    set of configuration variables.
  31. 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
  32. 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)); } }
  33. 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)); } }
  34. Templating   •  Pure PHP templates •  Escaping helper • 

    Template inheritance •  Slots •  Helpers •  Extensible
  35. 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, ));
  36. <?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 ?>
  37. 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'));
  38. 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();
  39. 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
  40. 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();
  41. 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);
  42. 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.
  43. # 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  
  44. The DI Component   o  Global configuration parameters o  Lazy

    loaded services o  PHP , YAML or XML configuration o  Inversion of control
  45. 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>
  46. 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);
  47. 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; } }
  48. 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  
  49. 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); // [...] } }
  50. 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'); }
  51. 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(); }
  52. 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(), ))); }
  53. 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); } }
  54. 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
  55. 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>
  56. 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>
  57. 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)); } }
  58. 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
  59. 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()) )); } }
  60. 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'), )); } }
  61. 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(), )); }
  62. 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", ))); // [...] } }
  63. 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'), ))); } }
  64. Authentication & Authorization   o Handling signin and signout o Restricting access

    to non authenticated users o Restricting access to the admin area
  65. 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); } } }
  66. 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(); } } } }
  67. 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);
  68. 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) ; } }
  69. 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'] )); } }
  70. 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
  71. 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