Don't Reinvent the Wheel with Symfony2 Components!

E2ed7c278c8c49bb3e7fe0b7de039997?s=47 Hugo Hamon
November 02, 2012

Don't Reinvent the Wheel with Symfony2 Components!

Paradoxically, Symfony2 is not only a full-stack framework. Its underlaying architecture relies on standalone, decoupled and cohesive components. This session will focus on the use of the most interesting components of Symfony like the Dependency Injection Container, Event Dispatcher, Console, HttpFoundation, HttpKernel, Routing, Browser Kit, Css Selector and Dom Crawler, and much more...

E2ed7c278c8c49bb3e7fe0b7de039997?s=128

Hugo Hamon

November 02, 2012
Tweet

Transcript

  1. Don’t Reinvent the Wheel with Symfony Components h"p://www.flickr.com/photos/25947610@N04/2546779304  

  2. None
  3. 0. What is Symfony2?

  4. Symfony2 is a set of standalone, decoupled and cohesive components.

    On top of these components, Symfony2 is also a full-stack framework.
  5. http://www.flickr.com/photos/alshepmcr/4298593016/

  6. None
  7. { "require": { "symfony/http-kernel": "2.1.*", "symfony/config": "2.1.*", "symfony/routing": "2.1.*", "symfony/yaml":

    "2.1.*", "symfony/filesystem": "2.1.*", "symfony/finder": "2.1.*", "symfony/process": "2.1.*", ... } } Composer Installation
  8. HttpKernel HttpFoundation Routing EventDispatcher Dependency Injection CssSelector DomCrawler BrowserKit Config

    Yaml Security OptionsResolver Console Filesystem Finder Locale Process Serializer Templating Form Validator Translation
  9. They already use them!

  10. 1. HttpFoundation

  11. GET /hello/hugo?p=2 HTTP/1.1 Host: say.hello.com Accept: text/html Accept-Language: fr,fr-fr;q=0.8,en-us;q=0.5,en;q=0.3 Cookie:

    username=hhamon User-Agent: Mozilla/5.0 Firefox/16.0 $path = $_SERVER['REQUEST_URI']; $page = isset($_GET['p']) ? $_GET['p'] : 1; $cookie = $_COOKIE['username']; $lang = $_SERVER['HTTP_ACCEPT_LANGUAGE']; $agent = $_SERVER['HTTP_USER_AGENT']; Request
  12. GET /hello/hugo?p=2 HTTP/1.1 Host: say.hello.com Accept: text/html Accept-Language: fr,fr-fr;q=0.8,en-us;q=0.5,en;q=0.3 Cookie:

    username=hhamon User-Agent: Mozilla/5.0 Firefox/16.0 use Symfony\Component\HttpFoundation\Request; $request = Request::createFromGlobals(); $path = $request->getPathInfo(); $page = $request->query->get('p', 1); $cookie = $request->cookies->get('username'); $lang = $request->getPreferredLanguage(); $agent = $request->server->get('USER_AGENT'); Request
  13. HTTP/1.1 200 OK Date: Fri, 02 Nov 2012 02:14:17 GMT

    Cache-Control: private, max-age=90 Set-Cookie: username=anonymous; Content-Type: text/html; charset=UTF-8 Hello Hugo! Response header('HTTP/1.1 200 OK'); header('Cache-Control: private, max-age=90'); setcookie('username', 'anonymous'); echo 'Hello Hugo!';
  14. HTTP/1.1 200 OK Date: Fri, 02 Nov 2012 02:14:17 GMT

    Cache-Control: private, max-age=90 Set-Cookie: username=anonymous; Content-Type: text/html; charset=UTF-8 Hello Hugo! Response use Symfony\Component\HttpFoundation\Cookie; use Symfony\Component\HttpFoundation\Response; $username = new Cookie('username', $name); $response = new Response('Hello Hugo!'); $response->setMaxAge(90); $response->headers->setCookie($username); $response->send();
  15. use Symfony\Component\HttpFoundation\Response; $response = new Response(); $response->setCharset('UTF-8'); $response->setEtag('abcdef'); $response->headers->set('Content-Type', 'application/json');

    if ($response->isNotModified($request)) { $response->send(); } $response->setContent('{ "name": "John Doe"}'); $response->setStatusCode(200); $response->setPublic(); $response->setSharedMaxAge(3600); $response->prepare($request); $response->send(); Caching
  16. Session Management use Symfony\Component\HttpFoundation\Session; $storage = new Storage\Handler\NativeSessionStorage(); $session =

    new Session\Session($storage); $session->start(); // Saving data into the session $session->set('cart', array(1, 2, 3)); $cart = $session->get('cart'); $session->save();
  17. 2. Routing

  18. Bidirectional Router

  19. Router in action Router R1   R2   R3  

    R3   R   Route /hello/hugo /home /about /hello/{name}
  20. Configuring Routes with PHP use Symfony\Component\Routing\RouteCollection; use Symfony\Component\Routing\Route; $routes =

    new RouteCollection(); $routes->add('post', new Route( '/blog/{id}/{slug}', array('controller' => 'BlogController::post'), array('id' => '\d+') ));
  21. Configuring Routes inYAML # config/routing.yml blog: pattern: /blog defaults: {

    controller: 'BlogController::index' } post: pattern: /blog/{id}/{slug} defaults: { controller: 'BlogController::post' } requirements: { id: \d+ }
  22. Matching a Route use Symfony\Component\Routing\Matcher\UrlMatcher; use Symfony\Component\Routing\RequestContext; $context = new

    RequestContext($_SERVER['REQUEST_URI']); $matcher = new UrlMatcher($routes, $context); $params = $matcher->match('/blog/42/symfony-2');
  23. Generating URLs use Symfony\Component\Routing\RequestContext; use Symfony\Component\Routing\Generator\UrlGenerator; // ... $context =

    new RequestContext($_SERVER['REQUEST_URI']); $generator = new UrlGenerator($routes, $context); echo $generator->generate('post', array( 'id' => '42', 'slug' => 'symfony-components', 'page' => '2', )); // Generates /blog/42/symfony-components?page=2
  24. The all-in-one Router $locator = new FileLocator(array(__DIR__.'/config')); $router = new

    Router( new YamlFileLoader($locator), 'routing.yml', array('cache_dir' => __DIR__.'/cache'), new RequestContext($_SERVER['REQUEST_URI']), ); $params = $router->match('/blog/42/symfony'); $url = $router->generate('blog');
  25. 3. Http Kernel

  26. Kernel in action Kernel Response Request Where the magic happens!

  27. Kernel in action // index.php use Symfony\Component\HttpFoundation\Request; $request = Request::createFromGlobals();

    $request->overrideGlobals(); $kernel = new DrupalKernel(...); $response = $kernel->handle($request); $response->prepare($request); $response->send();
  28. The HttpKernelInterface namespace Symfony\Component\HttpKernel; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; interface HttpKernelInterface

    { const MASTER_REQUEST = 1; const SUB_REQUEST = 2; public function handle( Request $request, $type = self::MASTER_REQUEST, $catch = true); }
  29. Wrapping a legacy application // index.php require_once 'config.php'; $module =

    !empty($_GET['module']) ? $_GET['module'] : ''; $action = !empty($_GET['action']) ? $_GET['action'] : ''; if (!controller_exists($module, $action)) { header('HTTP/1.0 404 Page Not Found'); echo render_view('error', '404'); exit; } echo include_controller($module, $action);
  30. use Symfony\Component\HttpKernel\HttpKernelInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; class LegacyKernel implements HttpKernelInterface

    { public function handle(Request $request, ...) { $module = $request->query->get('module'); $action = $request->query->get('action'); require_once 'config.php'; if (!controller_exists($module, $action)) { $html = render_view('error', '404'); return new Response($html, 404); } $html = include_controller($module, $action); return new Response($html, 200); } }
  31. Wrapping a legacy application // index.php use Symfony\Component\HttpFoundation\Request; $request =

    Request::createFromGlobals(); $request->overrideGlobals(); $kernel = new LegacyKernel(); $response = $kernel->handle($request); $response->prepare($request); $response->send();
  32. 4. Event Dispatcher

  33. None
  34. Real World Example $order = new Order(); $order->add('Cheeseburger', 3); $order->add('Caesar

    Salad', 1); $order->add('Coffee', 4); $cashier = new Cashier(); $cashier->dispatch($order);
  35. public function dispatch(Order $order) { if ($order->has('Cheeseburger')) { $this->cookCheesburger($order->getQty('Cheeseburger')); }

    if ($order->has('Caesar Salad')) { $this->cookCaesarSalad($order->getQty('Caesar Salad')); } if ($order->has('Coffee')) { $this->cookCoffee($order->getQty('Coffee')); } // ... }
  36. None
  37. public function dispatch(Order $order) { // ... if ($order->has('Pizza')) {

    $qty = $order->getQty('Pizza'); $this->cookPizza($qty); } // ... }
  38. use Symfony\Component\EventDispatcher\EventDispatcher; $cashier = new EventDispatcher(); $listeners[] = array(new CheesburgerCooker(),

    'onMealCook'); $listeners[] = array(new CaesarSaladCooker(), 'onMealCook'); $listeners[] = array(new CoffeeCooker(), 'onMealCook'); $listeners[] = array(new PizzaCooker(), 'onMealCook'); foreach ($listeners as $listener) { $cashier->addListener('meal.cook', $listener); } Using the Event Dispatcher
  39. $cashier = new EventDispatcher(); // ... $order = new Order();

    $order->add('Cheeseburger', 3); $order->add('Caesar Salad', 1); $order->add('Coffee', 4); $order->add('Pizza', 1); $event = new MealEvent($order); $cashier->dispatch('meal.cook', $event);
  40. use Symfony\Component\EventDispatcher\Event; class MealEvent extends Event { public $order; public

    function __construct(Order $order) { $this->order = $order; } } Designing an event
  41. class PizzaCooker { public function onMealCook(MealEvent $event) { if (!$event->order->has('Pizza'))

    { return; } $quantity = $event->order->getQuantity('Pizza'); // ... cook the pizza!!! } } Designing a listener (cooker)
  42. 5. YAML

  43. http://www.flickr.com/photos/drymek/4310204250/

  44. # parameters.yml parameters: database_host: localhost database_port: 3306 database_name: demo database_user:

    root database_pwd: ~ credentials: { user: john, password: secret } recaptcha: public_key: 6LdZPNYSAAAAAICZ43AfeMfEsJVQXr_CXh4f3mWy private_key: 6LdZPNYSAAAAAB8ALTjfVr72iGYODjKAzic1KuoC Designing a listener (cooker)
  45. use Symfony\Component\Yaml\Yaml; $config = Yaml::parse('parameters.yml'); echo '<pre>'; print_r($config); echo '</pre>';

    Loading a YAML file
  46. Array ( [parameters] => Array ( [database_host] => localhost [database_port]

    => 3306 [database_name] => demo [database_user] => root [database_pwd] => [credentials] => Array ( [user] => john [password] => secret ) [recaptcha] => Array ( [public_key] => 6LdZPNYSAAAAAICZ43AfeMfEsJVQXr_CXh4f3mWy [private_key] => 6LdZPNYSAAAAAB8ALTjfVr72iGYODjKAzic1KuoC ) ) )
  47. $config = array('parameters' => array( 'database_host' => 'localhost', 'database_port' =>

    3306, 'database_name' => 'root', 'database_user' => 'root', 'database_pwd' => null, 'credentials' => array( 'user' => 'john', 'password' => 'secret', ), 'recaptcha' => array( 'public_key' => '6LdZPNYSAAAAAICZ43AfeMfEsJVQXr_CXh4f3m', 'private_key' => '6LdZPNYSAAAAAB8ALTjfVr72iGYODjKAzic1Ku', ), )); Dumping an array to YAML
  48. use Symfony\Component\Yaml\Yaml; $yaml = Yaml::dump($config); file_put_contents( __DIR__.'/config.yml', $yaml ); Dumping

    an array to YAML
  49. 6. Filesystem

  50. http://www.flickr.com/photos/sebastian_bergmann/173880207/ PHP Way mkdir() copy() delete() unlink() chmod() chown() ...

  51. http://www.flickr.com/photos/amazeelabs/7843168540/ Symfony Way Filesystem

  52. $cache1 = __DIR__.'/tmp/cache1'; $cache2 = __DIR__.'/tmp/cache2'; $fs = new Symfony\Component\Filesystem\Filesystem();

    $fs->mkdir(array($cache1, $cache2)); $fs->touch($cache1.'/routing.php'); $fs->copy( $cache1.'/routing.php', $cache1.'/routing.old' ); $fs->remove($cache1); The Filesystem object
  53. abstract class AbstractCommand extends Command { // ... protected function

    createDirectory($directory) { $filesystem = $this->getFilesystem(); try { $filesystem->mkdir($directory); } catch (IOException $e) { throw new \RuntimeException('...', 0, $e); } } } Example taken from Propel2
  54. 7. Finder

  55. None
  56. use Symfony\Component\Finder\Finder; $finder = new Finder(); $finder ->files() ->in(__DIR__.'/src/Symfony/Bundle/FrameworkBundle') ->exclude('Tests')

    ->ignoreVCS(true) ->name('/Controller/') ; foreach ($finder->sortByName() as $file) { echo $file->getFilename()."\n"; } Using the Finder Component
  57. // Filter by name $finder->directories()->name('Tests'); $finder->files()->name('*.php'); $finder->files()->name('/\.php$/'); $finder->files()->notName('*.rb'); // Filter

    by size $finder->files()->size('< 1.5K'); $finder->files()->size('>= 1K')->size('<= 2K'); // Filter by date $finder->date('since yesterday'); // Limiting the depth $finder->depth('== 0'); $finder->depth('< 3');
  58. $filter = function (\SplFileInfo $file) { if (strlen($file) > 10)

    { return false; } }; $finder->files()->filter($filter); Custom filters
  59. class MagentoFinder extends Symfony\Component\Finder\Finder { public function __construct() { parent::__construct();

    $this ->name('*.php') ->name('*.phtml') ->name('*.xml') ->exclude(array( 'lib', 'shell', 'app/Mage.php', 'app/code/core', 'app/code/community', 'app/design/frontend/default', 'app/design/frontend/enterprise/default', 'app/design/frontend/base', 'app/design/adminhtml/default') ) ; } }
  60. 8. Console

  61. http://www.flickr.com/photos/ideonexus/7012997141/lightbox/

  62. None
  63. None
  64. #!/usr/bin/env php $loader = require __DIR__.'/vendor/autoload.php'; $loader->add('Hangman', __DIR__.'/src'); use Symfony\Component\Console\Application;

    use Acme\Command\HangmanPlayCommand; $application = new Application(); $application->add(new HangmanPlayCommand()); $application->run(); The Console Script
  65. namespace Acme\Command; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; class HangmanPlayCommand

    extends Command { protected function configure() { ... } protected function execute( InputInterface $input, OutputInterface $output ) { ... } } Creating a new Command Class
  66. protected function configure() { $this ->setName('hangman:play') ->setDescription('Play the famous Hangman

    game') ->addArgument('name', InputArgument::REQUIRED, 'Your name') ->addOption('length', 'l', InputOption::VALUE_REQUIRED) ->setHelp('The complete manual ...'); } Documenting the Command
  67. protected function execute($input, $output) { $words = new WordList(); $words->load(__DIR__.'/../../../data/words.txt');

    $length = $input->getOption('length', 8); $name = $input->getArgument('name'); $game = new Game($words->getRandomWord($length)); // ... } Adding the business logic
  68. protected function execute($input, $output) { // ... $dialog = $this->getHelperSet()->get('dialog');

    $output->writeln('Welcome '.$name.' to the Hangman Game!'); while (!$game->isOver()) { $letter = $dialog->ask($output, 'Type a letter: '); $game->tryLetter($letter); $output->writeLn(implode(' ', $game->getSolution())); } if ($game->isWon()) { $output->writeln('Congratulations!'); } else { $output->writeln('Game Over!'); } // ... }
  69. None
  70. Some purposes Generating les Deploying les Dumping con guration Clearing

    caches Generating documentation Running a web server Sending batch emails…
  71. 9. Translation

  72. None
  73. Basic Usage $selector = new MessageSelector(); $trans = new Translator('fr',

    $selector); $trans->setFallbackLocale('en'); $loader = new XliffFileLoader(); $trans->addLoader('xliff', $loader); $trans->addResource('yaml', '/fr.yml', 'fr'); $trans->trans('Hello %name%', array( '%name%' => $name ));
  74. XLIFF Support <?xml version="1.0"?> <xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2"> <file source-language="en" datatype="plaintext"

    original="file.ext"> <body> <trans-unit id="1"> <source>Hello %name%</source> <target>Bonjour %name%</target> </trans-unit> </body> </file> </xliff>
  75. Usage in Behat class TranslatedLoader implements LoaderInterface { private $translator;

    // ... public function load(ContextInterface $context) { foreach ($context->getTranslationResources() as $path) { $extension = pathinfo($path, PATHINFO_EXTENSION); if ('yml' === $extension) { $this->translator->addResource( 'yaml', $path, basename($path, '.yml'), 'behat.definitions' ); } // ... } } }
  76. 10. Dependency Injection

  77. What’s dependency injection? « Dependency Injection is where components are

    given their dependencies through their constructors, methods, or directly into elds. » http://picocontainer.org/injection.html
  78. Real World Example class Database { private $dbh; function __construct(PDO

    $dbh) { $this->dbh = $dbh; } function execute($query) { // ... } }
  79. $container->setParameter('foo', 'bar'); $container->getParameter('dbal.dsn'); $dbal = $container->get('dbal'); The Service Container

  80. Real World Example class Database { private $dbh; function __construct(PDO

    $dbh) { $this->dbh = $dbh; } function execute($query) { // ... } }
  81. Initializing the Container $container = new ContainerBuilder(); $params = array(

    'dbal.dsn' => 'mysql:host=localhost;dbname=demo', 'dbal.user' => 'root', 'dbal.password' => 'secret', ); foreach ($params as $name => $value) { $container->setParameter($name, $value); }
  82. Registering a service $container = new ContainerBuilder(); // ... $container

    ->register('dbal.pdo_handler', 'PDO') ->addArgument('%dbal.dsn%') ->addArgument('%dbal.user%') ->addArgument('%dbal.password%') ;
  83. Injecting service references $container = new ContainerBuilder(); // ... $reference

    = new Reference('dbal.pdo_handler'); $container ->register('dbal', 'Database') ->addArgument($reference) ;
  84. Describing services with XML <container ...> <parameters> <parameter key="dbal.dsn">mysql:...</parameter> <parameter

    key="dbal.user">root</parameter> <parameter key="dbal.password">secret</parameter> </parameters> <services> <service id="dbal.pdo_handler" class="PDO"> <argument>%dbal.dsn%</argument> <argument>%dbal.user%</argument> <argument>%dbal.password%</argument> </service> </services> </container>
  85. Describing services with XML <container ...> <!-- ... -> <services>

    <!-- ... -> <service id="dbal" class="Database"> <argument type="service" id="db.pdo_handler"/> </service> </services> </container>
  86. 12. Validator

  87. The Validator Component use Symfony\Component\Validator\Validator; use Symfony\Component\Validator\Mapping\ClassMetadataFactory; use Symfony\Component\Validator\Mapping\Loader\StaticMethodLoader; use

    Symfony\Component\Validator\ConstraintValidatorFactory; use Symfony\Component\Validator\Constraints\Email; $validator = new Validator( new ClassMetadataFactory(new StaticMethodLoader()), new ConstraintValidatorFactory() ); $validator->validateValue('foo@bar.com', new Email());
  88. Validating PHP Objects use Symfony\Component\Validator\Mapping\ClassMetadata; use Symfony\Component\Validator\Constraints; class Post {

    public $title; public $body; static public function loadValidatorMetadata(ClassMetadata $metadata) { $metadata->addPropertyConstraint('title', new Constraints\NotNull()); $metadata->addPropertyConstraint('title', new Constraints\NotBlank()); $metadata->addPropertyConstraint('body', new Constraints\Length(array( 'max' => 10 ))); } }
  89. Validating PHP Objects $post = new Post(); $post->title = 'Lorem

    ipsum...'; $post->body = 'Lorem ipsum...'; $violations = $validator ->validate($post) ;
  90. 12. Process

  91. Executing command lines use Symfony\Component\Process\Process; $proc = new Process('ls -lah');

    $proc->setWorkingDirectory(__DIR__.'/..'); $proc->setTimeout(3600); $proc->run(); if (!$proc->isSuccessful()) { throw new RuntimeException( $proc->getErrorOutput() ); } print $proc->getOutput();
  92. Executing command lines total 0 drwxr-xr-x 11 Hugo staff 374B

    Sep 8 13:38 . drwxr-xr-x 8 Hugo staff 272B Sep 8 13:39 .. drwxr-xr-x 4 Hugo staff 136B Aug 30 11:31 01-ClassLoader drwxr-xr-x 2 Hugo staff 68B Aug 30 11:30 02-HttpFoundation drwxr-xr-x 4 Hugo staff 136B Sep 4 22:06 03-Routing drwxr-xr-x 6 Hugo staff 204B Sep 2 11:49 04-HttpKernel drwxr-xr-x 4 Hugo staff 136B Sep 2 14:07 05-Filesystem drwxr-xr-x 3 Hugo staff 102B Sep 4 22:40 06-Finder drwxr-xr-x 5 Hugo staff 170B Sep 8 10:24 07-Console drwxr-xr-x 5 Hugo staff 170B Sep 8 12:55 08-Yaml drwxr-xr-x 3 Hugo staff 102B Sep 8 13:41 09-Process
  93. 13. CSS Selector

  94. CSS Selectors to XPath use Symfony\Component\CssSelector\CssSelector; print CssSelector::toXPath('div.item > h4

    > a'); descendant-or-self::div[contains(concat(' ',normalize-space(@class), ' '), ' item ')]/ h4/a
  95. 14. DOM Crawler

  96. Using the DOM Crawler use Symfony\Component\DomCrawler\Crawler; $html = <<<'HTML' <!DOCTYPE

    html> <html> <body> <p class="message">Hello World!</p> <p>Hello Crawler!</p> </body> </html> HTML; $crawler = new Crawler($html); $crawler = $crawler->filterXPath('descendant-or-self::body/p'); $crawler = $crawler->filter('body > p');
  97. The DOM Crawler API // Filtering with CSS or XPath

    selectors $tweets = $crawler->filter('#sidebar .tweet'); $tweets = $crawler->filterXPath('//p[class="tweet"]'); // Traversing $first = $tweets->first(); $third = $tweets->eq(2); $last = $tweets->last(); // Extracting $text = $first->text(); $class = $first->attr('class'); $infos = $first->extract(array('_text', 'class'));
  98. Reducing the DOM Crawler $filter = function ($node, $i) {

    $content = (string) $node->textContent; if (!preg_match('/symfony/i', $content)) { return false; } }; $tweets = $crawler->reduce($filter);
  99. Finding links Finding forms $link = $crawler->selectLink('Click me')->link(); $crawler =

    $client->click($link); $form = $crawler->selectButton('send')->form(); $client->submit($form, array('name' => 'Foo'));
  100. DOM Crawler in Goutte use Goutte\Client; $client = new Client();

    $crawler = $client->request('GET', 'http://www.symfony.com'); $download = $crawler ->filter('#header .download_button') ->link() ; $crawler = $client->click($download);
  101. https://joind.in/talk/view/7413