Share
Embed
Don't reinvent the wheel with Symfony 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 fo Symfony like the Dependency Injection Container, Event Dispatcher, Console, HttpFoundation, HttpKernel, Routing, Browser Kit, Css Selector& Dom Crawler, and much more...
Hugo Hamon
September 15, 2012
Transcript
94 slides
-
Don’t Reinvent the Wheel w/ Symfony Components h"p://www.flickr.com/photos/vemsteroo/7595601626/ PFCongres
2012 – Utrecht – Hugo Hamon -
{ "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 -
require '/path/to/UniversalClassLoader.php'; use Symfony\Component\ClassLoader\UniversalClassLoader; $loader = new UniversalClassLoader(); $loader->registerNamespaces(array( 'Symfony'
=> __DIR__.'/vendor/symfony/src', 'Zend' => __DIR__.'/vendor/zend/src', 'Demo' => __DIR__.'/src', )); $loader->registerPrefixes(array( 'Acme' => __DIR__.'/src', )); $loader->register(); -
# 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 -
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 ) ) ) -
$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 a YAML File -
use Symfony\Component\HttpFoundation\Request; $request = Request::createFromGlobals(); $request->overrideGlobals(); $page = $request->query->get('page', 1);
$body = $request->request->get('body'); $name = $request->cookies->get('username'); $user = $request->server->get('HTTP_AUTH_USER'); $ip = $request->getClientIp(); $lang = $request->getPreferredLanguage(); $user = $request->getUser(); $pwd = $request->getPassword(); $isPost = $request->isMethod('POST'); $isHttps = $request->isSecure(); $isAjax = $request->isXmlHttpRequest(); -
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(); -
use Symfony\Component\HttpFoundation\Cookie; use Symfony\Component\HttpFoundation\RedirectResponse; $cookie = new Cookie('user', 'hhamon', '+30
days'); $resp = new RedirectResponse('http://www.acme.com'); $resp->headers->setCookie($cookie); $resp->send(); Cookies and Redirects -
$storage = new NativeSessionStorage(); $session = new Session($storage); $session->start(); //
Saving data into the session $session->set('cart', array(1, 2, 3)); $cart = $session->get('cart'); $session->save(); Session Management -
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+') )); PHP Con guration -
# config/routing.yml blog: pattern: /blog defaults: { controller: 'BlogController::index' }
post: pattern: /blog/{id}/{slug} defaults: { controller: 'BlogController::post' } requirements: { id: \d+ } YAML Con guration -
use Symfony\Component\Routing\Matcher\UrlMatcher; use Symfony\Component\Routing\RequestContext; $context = new RequestContext($_SERVER['REQUEST_URI']); $matcher =
new UrlMatcher($routes, $context); $parameters = $matcher->match('/blog/42/symfony-2'); Request Matching -
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 URLs Generator -
$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'); The All-in-One Router -
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); } The HttpKernel Interface -
namespace Acme; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\HttpKernelInterface; use Symfony\Component\Routing\RouterInterface; class Kernel
implements HttpKernelInterface { private $router; public function __construct(RouterInterface $router) { $this->router = $router; } public function handle(Request $request, $type = self::MASTER_REQUEST, $catch = true) { // ... } } Designing a Kernel for a Legacy Application -
class Kernel implements HttpKernelInterface { public function handle(Request $request, ...)
{ try { $uri = $request->server->get('PATH_INFO'); $params = $this->router->match($uri); } catch (ResourceNotFoundException $e) { $params['controller'] = 'Acme\Controller\ErrorController'; $params['action'] = 'error404'; } $method = sprintf('%sAction', $params['action']); $callable = array(new $params['controller'](), $method); return call_user_func_array($callable, array($request)); } } -
namespace Acme\Controller; use Symfony\Component\HttpFoundation\Response; abstract class Controller { public function
render($name, array $context = array()) { $template = sprintf(__DIR__.'/../../../views/%s.php', $name); if (!is_readable($template)) { throw new \InvalidArgumentException('No "'. $template.'" template.'); } extract($context); ob_start(); include $template; $content = ob_get_clean(); return new Response($content, 200); } } The Base Controller Class -
namespace Acme\Controller; use Symfony\Component\HttpFoundation\Request; class BlogController extends Controller { public
function indexAction(Request $request) { // Get a list of recent posts $posts = ...; return $this->render('blog/index', array('posts' => $posts)); } } The BlogController Class -
$request = Request::createFromGlobals(); $request->overrideGlobals(); $router = new Router(...); $kernel =
new Acme\Kernel($router); $response = $kernel->handle($request); $response->send(); index.php -
class Article { public function save(Database $con) { $parser =
new Markdown(); $html = $parser->toHTML($this->getContent()); $this->setHtmlContent($html); $this->save($con); $this->updateLuceneIndex(); } } -
use Symfony\Component\EventDispatcher\EventDispatcher; // Declaring listeners $l1 = array(new ArticleListener(), 'onPreSave');
$l2 = array(new LuceneListener(), 'onPostSave'); // Registering listeners $dispatcher = new EventDispatcher(); $dispatcher->addListener('article.pre_save', $l1); $dispatcher->addListener('article.post_save', $l2); Registering Listeners in the Dispatcher -
Setting the Dispatcher use Symfony\Component\EventDispatcher\EventDispatcher; class Article { private $dispatcher;
public function setDispatcher(EventDispatcher $dp) { $this->dispatcher = $dp; } } -
Dispatching Events class Article { // ... public function save(Database
$con) { $e = new ArticleEvent($this); $this->dispatcher->dispatch('article.pre_save', $e); $this->save($con); $this->dispatcher->dispatch('article.post_save', $e); } } -
The Listener class ArticleListener { public function onPreSave(ArticleEvent $event) {
$article = $event->article; $markdown = $article->getContent(); $parser = new Markdown(); $html = $parser->toHTML($markdown); $article->setHtmlContent($html); } } -
Triggering the Registered Listeners $markdown = 'Some **markdown**'; $article =
new Article(); $article->setDispatcher($dispatcher); $article->setMarkdownContent($markdown); $article->save(); -
$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 -
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"; } -
// 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'); -
$filter = function (\SplFileInfo $file) { if (strlen($file) > 10)
{ return false; } }; $finder->files()->filter($filter); Custom Filter -
#!/usr/bin/env php $loader = require __DIR__.'/vendor/autoload.php'; $loader->add('Hangman', __DIR__.'/src'); use Symfony\Component\Console\Application;
use Hangman\Command\HangmanPlayCommand; $application = new Application(); $application->add(new HangmanPlayCommand()); $application->run(); The Console Script -
namespace HangmanPlay\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) { // ... } } -
namespace HangmanPlay\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) { // ... } } -
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, 'The word length', 8) ->setHelp(<<<EOF This is the manual for the <info>%command.name%</info> command. You can run the command like this: $ php %command.full_name% hhamon --length=6 EOF); } -
protected function execute(InputInterface $input, OutputInterface $output) { $words = new
WordList(); $words->load(__DIR__.'/../../../data/words.txt'); $length = $input->getOption('length'); $name = $input->getArgument('name'); $game = new Game($words->getRandomWord($length)); // ... } -
protected function execute(InputInterface $input, OutputInterface $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!'); } } -
$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 )); -
<?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> -
class Database { private $dbh; function __construct(PDO $dbh) { $this->dbh
= $dbh; } function execute($query) { // ... } } -
<container ...> <services> <service id="db.pdo_handler" class="PDO"> <argument>mysql:host=localhost;dbname=demo</ argument> <argument>root</argument> <argument>secret</argument>
</service> <service id="dbal" class="DatabaseConnection"> <argument type="service" id="db.pdo_handler"/> </service> </services> </container> -
$locator = new FileLocator(__DIR__); $container = new ContainerBuilder(); $loader =
new XmlFileLoader( $container, $locator ); $loader->load('services.xml'); // Get the DBAL service $db = $container->get('dbal'); $result = $db->execute('SELECT ...'); -
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()); -
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 ))); } } -
$post = new Post(); $post->title = 'Lorem ipsum...'; $post->body =
'Lorem ipsum...'; $validator->validate($post); -
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(); -
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 -
Serialization is the process of converting an object state into
a format that can be stored and resurrected later Wikipedia -
use Symfony\Component\Serializer\Serializer; use Symfony\Component\Serializer\Normalizer\GetSetMethodNormalizer; use Symfony\Component\Serializer\Encoder\JsonEncoder; use Symfony\Component\Serializer\Encoder\XmlEncoder; // Register
the normalizers $normalizers[] = new GetSetMethodNormalizer(); // Register the encoders $encoders[] = new JsonEncoder(); $encoders[] = new XmlEncoder(); // Create and initialize the serializer $serializer = new Serializer($normalizers, $encoders); -
use Symfony\Component\Locale\Locale; // Get the country names for a locale
or get all country codes $countries = Locale::getDisplayCountries('pl'); $countryCodes = Locale::getCountries(); // Get the language names for a locale or get all language codes $languages = Locale::getDisplayLanguages('fr'); $languageCodes = Locale::getLanguages(); // Get the locale names for a given code or get all locale codes $locales = Locale::getDisplayLocales('en'); $localeCodes = Locale::getLocales(); // Get ICU versions $icuVersion = Locale::getIcuVersion(); $icuDataVersion = Locale::getIcuDataVersion(); -
use Symfony\Component\CssSelector\CssSelector; print CssSelector::toXPath('div.item > h4 > a'); Converting a
CSS Selector to an XPath descendant-or-self::div[contains(concat(' ',normalize-space(@class), ' '), ' item ')]/ h4/a -
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'); -
// 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')); Using the Crawler -
$filter = function ($node, $i) { $content = (string) $node->textContent;
if (!preg_match('/symfony/i', $content)) { return false; } }; $tweets = $crawler->reduce($filter); Reducing a Crawler Selection -
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'));