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

Don't Reinvent the Wheel with Symfony2 Components!

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

Hugo Hamon

November 02, 2012
Tweet

More Decks by Hugo Hamon

Other Decks in Technology

Transcript

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

    On top of these components, Symfony2 is also a full-stack framework.
  2. { "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
  3. HttpKernel HttpFoundation Routing EventDispatcher Dependency Injection CssSelector DomCrawler BrowserKit Config

    Yaml Security OptionsResolver Console Filesystem Finder Locale Process Serializer Templating Form Validator Translation
  4. 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
  5. 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
  6. 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!';
  7. 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();
  8. 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
  9. 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();
  10. Router in action Router R1   R2   R3  

    R3   R   Route /hello/hugo /home /about /hello/{name}
  11. 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+') ));
  12. 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+ }
  13. 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');
  14. 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
  15. 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');
  16. 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();
  17. 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); }
  18. 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);
  19. 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); } }
  20. 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();
  21. 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);
  22. 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')); } // ... }
  23. public function dispatch(Order $order) { // ... if ($order->has('Pizza')) {

    $qty = $order->getQty('Pizza'); $this->cookPizza($qty); } // ... }
  24. 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
  25. $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);
  26. use Symfony\Component\EventDispatcher\Event; class MealEvent extends Event { public $order; public

    function __construct(Order $order) { $this->order = $order; } } Designing an event
  27. 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)
  28. # 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)
  29. 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 ) ) )
  30. $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
  31. $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
  32. 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
  33. 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
  34. // 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');
  35. $filter = function (\SplFileInfo $file) { if (strlen($file) > 10)

    { return false; } }; $finder->files()->filter($filter); Custom filters
  36. 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') ) ; } }
  37. #!/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
  38. 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
  39. 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
  40. 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
  41. 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!'); } // ... }
  42. Some purposes Generating les Deploying les Dumping con guration Clearing

    caches Generating documentation Running a web server Sending batch emails…
  43. 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 ));
  44. 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>
  45. 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' ); } // ... } } }
  46. 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
  47. Real World Example class Database { private $dbh; function __construct(PDO

    $dbh) { $this->dbh = $dbh; } function execute($query) { // ... } }
  48. Real World Example class Database { private $dbh; function __construct(PDO

    $dbh) { $this->dbh = $dbh; } function execute($query) { // ... } }
  49. 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); }
  50. Registering a service $container = new ContainerBuilder(); // ... $container

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

    = new Reference('dbal.pdo_handler'); $container ->register('dbal', 'Database') ->addArgument($reference) ;
  52. 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>
  53. Describing services with XML <container ...> <!-- ... -> <services>

    <!-- ... -> <service id="dbal" class="Database"> <argument type="service" id="db.pdo_handler"/> </service> </services> </container>
  54. 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('[email protected]', new Email());
  55. 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 ))); } }
  56. Validating PHP Objects $post = new Post(); $post->title = 'Lorem

    ipsum...'; $post->body = 'Lorem ipsum...'; $violations = $validator ->validate($post) ;
  57. 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();
  58. 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
  59. 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
  60. 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');
  61. 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'));
  62. Reducing the DOM Crawler $filter = function ($node, $i) {

    $content = (string) $node->textContent; if (!preg_match('/symfony/i', $content)) { return false; } }; $tweets = $crawler->reduce($filter);
  63. 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'));
  64. 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);