Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

SensioLabs We’re hiring Symfony and PHP top stars.

Slide 4

Slide 4 text

Introduction Living with legacy…

Slide 5

Slide 5 text

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

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

Code lifting

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

The legacy application

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

Metrics

Slide 15

Slide 15 text

No content

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

Why the Symfony2 Components? o Well tested o Well documented o Easy to use o Decoupled o PHP 5.4 compatible o Composer ready

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

1st Step Set up the migration

Slide 23

Slide 23 text

├── 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/

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

├── 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

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

2nd Step Keeping backward compatibility

Slide 28

Slide 28 text

Keeping backward compatibility

Slide 29

Slide 29 text

MainKernel LegacyKernel Legacy Request HTTP Request HTTP Response

Slide 30

Slide 30 text

MainKernel LegacyKernel Symfony Request HTTP Request HTTP Response

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

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

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

3rd Step Plug-in code with events

Slide 42

Slide 42 text

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

Slide 43

Slide 43 text

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

Slide 44

Slide 44 text

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

Slide 45

Slide 45 text

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

Slide 46

Slide 46 text

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

Slide 47

Slide 47 text

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

Slide 48

Slide 48 text

4th Step Routing incoming requests

Slide 49

Slide 49 text

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

Slide 50

Slide 50 text

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

Slide 51

Slide 51 text

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

Slide 52

Slide 52 text

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

Slide 53

Slide 53 text

5th Step Towards separation of concerns

Slide 54

Slide 54 text

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

Slide 55

Slide 55 text

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

Slide 56

Slide 56 text

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

Slide 57

Slide 57 text

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']) ?>

Slide 58

Slide 58 text

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

Slide 59

Slide 59 text

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

Slide 60

Slide 60 text

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

Slide 61

Slide 61 text

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

Slide 62

Slide 62 text

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

Slide 63

Slide 63 text

6th Step Managing the configuration

Slide 64

Slide 64 text

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.

Slide 65

Slide 65 text

# 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  

Slide 66

Slide 66 text

# 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  

Slide 67

Slide 67 text

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

Slide 68

Slide 68 text

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

Slide 69

Slide 69 text

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

Slide 70

Slide 70 text

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

Slide 71

Slide 71 text

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  

Slide 72

Slide 72 text

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

Slide 73

Slide 73 text

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

Slide 74

Slide 74 text

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

Slide 75

Slide 75 text

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

Slide 76

Slide 76 text

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

Slide 77

Slide 77 text

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

Slide 78

Slide 78 text

7th Step Handling all errors

Slide 79

Slide 79 text

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

Slide 80

Slide 80 text

Register the exception listener   Symfony\Component\HttpKernel\EventListener\ExceptionListener Application\Controller\ErrorController::exceptionAction %exception_listener.exception_controller%

Slide 81

Slide 81 text

Register the exception listener  

Slide 82

Slide 82 text

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

Slide 83

Slide 83 text

8th Step Migration forms & validation

Slide 84

Slide 84 text

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

Slide 85

Slide 85 text

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

Slide 86

Slide 86 text

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

Slide 87

Slide 87 text

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

Slide 88

Slide 88 text

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

Slide 89

Slide 89 text

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

Slide 90

Slide 90 text

9th Step Authenticating users

Slide 91

Slide 91 text

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

Slide 92

Slide 92 text

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

Slide 93

Slide 93 text

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

Slide 94

Slide 94 text

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

Slide 95

Slide 95 text

10th Step Building utility tools

Slide 96

Slide 96 text

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

Slide 97

Slide 97 text

No content

Slide 98

Slide 98 text

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

Slide 99

Slide 99 text

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

Slide 100

Slide 100 text

In the end Analysis & future plans

Slide 101

Slide 101 text

Quality Metrics  

Slide 102

Slide 102 text

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

Slide 103

Slide 103 text

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

Slide 104

Slide 104 text

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