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

Advanced Silex (sflive2012)

Advanced Silex (sflive2012)

A4b95be2145cc46f891707b6db9dd82d?s=128

Igor Wiedler

June 08, 2012
Tweet

Transcript

  1. Advanced

  2. • Symfony2 • Silex • Composer igorw @igorwesome

  3. require 'vendor/autoload.php'; $app = new Silex\Application(); $app->get('/', function () {

    return 'Worlds largest microf'; }); $app->run();
  4. Trashbin

  5. None
  6. None
  7. None
  8. # composer.json { "minimum-stability": "dev", "require": { "silex/silex": "1.0.*@dev" }

    }
  9. $ php composer.phar install

  10. require_once __DIR__.'/../vendor/autoload.php'; $app = new Silex\Application(); // ... $app->run();

  11. { "minimum-stability": "dev", "require": { "silex/silex": "1.0.*@dev", "twig/twig": "1.6.0", "predis/service-provider":

    "dev-master" } }
  12. $app->register(new Silex\Provider\TwigServiceProvider(), array( 'twig.path' => __DIR__.'/../views', )); $app->register(new Silex\Provider\UrlGeneratorServiceProvider()); $app->register(new

    Predis\Silex\PredisServiceProvider());
  13. •GET / •POST / •GET /{id}

  14. $app->get('/', function () use ($app) { $id = $app['request']->get('parent'); $parentPaste

    = null; if ($id) { $parentPaste = $app['predis']->hgetall($id); } return $app['twig']->render('index.html', array( 'paste' => $parentPaste, )); }) ->bind('homepage');
  15. $app->post('/', function (Request $request) use ($app) { $content = $request->get('content',

    ''); $id = substr(hash('sha512', mt_rand()), 0, 8); $paste = array( 'content' => $content, 'language' => $request->get('language', ''), 'created_at' => time(), ); if ('' === trim($paste['content'])) { $page = $app['twig']->render('index.html', array( 'errors' => array('you must enter some content'), 'paste' => $paste, )); return new Response($page, 400); } $app['predis']->hmset($id, $paste); return $app->redirect($app['url_generator']->generate('view', array( 'id' => $id, ))); }) ->bind('create');
  16. $app->get('/{id}', function ($id) use ($app) { $paste = $app['predis']->hgetall($id); if

    (!$paste) { return $app->abort(404, 'paste not found'); } $copy_url = $app['url_generator']->generate('homepage', array( 'parent' => $id, )); return $app['twig']->render('view.html', array( 'copy_url' => $copy_url, 'paste' => $paste, )); }) ->bind('view') ->assert('id', '[0-9a-f]{8}');
  17. <?php require_once __DIR__.'/../vendor/autoload.php'; use Symfony\Component\Finder\Finder; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use

    Symfony\Component\HttpKernel\Exception\HttpException; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; $app = new Silex\Application(); $app->register(new Silex\Provider\TwigServiceProvider(), array( 'twig.path' => __DIR__.'/../views', 'twig.options' => array('cache' => __DIR__.'/../cache/twig'), )); $app->register(new Silex\Provider\UrlGeneratorServiceProvider()); $app->register(new Predis\Silex\PredisServiceProvider()); $app['app.languages'] = $app->share(function () { $languages = array(); $finder = new Finder(); foreach ($finder->name('*.min.js')->in(__DIR__.'/../web/shjs/lang') as $file) { if (preg_match('#sh_(.+).min.js#', basename($file), $matches)) { $languages[] = $matches[1]; } } return $languages; }); $app->before(function () use ($app) { // set up some template globals $app['twig']->addGlobal('base_path', $app['request']->getBasePath()); $app['twig']->addGlobal('index_url', $app['url_generator']->generate('homepage')); $app['twig']->addGlobal('create_url', $app['url_generator']->generate('create')); $app['twig']->addGlobal('languages', $app['app.languages']); }); $app->get('/', function () use ($app) { $parentPasteId = $app['request']->get('parent'); $parentPaste = null; if ($parentPasteId) { $parentPaste = $app['predis']->hgetall($id); } return $app['twig']->render('index.html', array( 'paste' => $parentPaste, )); }) ->bind('homepage'); $app->get('/create', function () use ($app) { return $app->redirect($app['url_generator']->generate('homepage')); }); $app->post('/create', function (Request $request) use ($app) { $content = $request->get('content', ''); $content = $this->normalizeContent($content); $id = $this->generateId($content); $paste = array( 'content' => $content, ); $language = $request->get('language', ''); if (in_array($language, $this->languages)) { $paste['language'] = $language; } $paste['created_at'] = time(); $errors = array(); if ('' === trim($paste['content'])) { $errors[] = 'you must enter some content'; } if ($errors) { $page = $app['twig']->render('index.html', array( 'errors' => $errors, 'paste' => $paste, )); return new Response($page, 400); } $app['predis']->hmset($id, $paste) return $app->redirect($app['url_generator']->generate('view', array('id' => $id))); }) ->bind('create'); $app->get('/about', function () use ($app) { return $app['twig']->render('about.html'); }); $app->get('/{id}', function ($id) use ($app) { $paste = $app['predis']->hgetall($id); if (!$paste) { throw new NotFoundHttpException('paste not found'); } return $app['twig']->render('view.html', array( 'copy_url' => $app['url_generator']->generate('homepage', array(' 'paste' => $paste, )); }) ->bind('view') ->assert('id', '[0-9a-f]{8}'); $app->error(function (Exception $e) use ($app) { $code = ($e instanceof HttpException) ? $e->getStatusCode() : 500; return new Response($app['twig']->render('error.html', array( 'message' => $e->getMessage(), )), $code); }); $app->run();
  18. What a freaking mess!

  19. ᵓᴷᴷ composer.json ᵓᴷᴷ README.md ᵓᴷᴷ phpunit.xml.dist ᵓᴷᴷ src ᴹ ᵋᴷᴷ

    app.php ᵓᴷᴷ tests ᵓᴷᴷ views ᵋᴷᴷ web ᵋᴷᴷ index.php
  20. ᵓᴷᴷ composer.json ᵓᴷᴷ README.md ᵓᴷᴷ phpunit.xml.dist ᵓᴷᴷ src ᴹ ᵋᴷᴷ

    app.php ᵓᴷᴷ tests ᵓᴷᴷ views ᵋᴷᴷ web ᵋᴷᴷ index.php
  21. ᵓᴷᴷ composer.json ᵓᴷᴷ README.md ᵓᴷᴷ phpunit.xml.dist ᵓᴷᴷ src ᴹ ᵋᴷᴷ

    app.php ᵓᴷᴷ tests ᵓᴷᴷ views ᵋᴷᴷ web ᵋᴷᴷ index.php
  22. $app = new Silex\Application(); // ... return $app;

  23. $app = require __DIR__.'/../src/app.php'; $app->run();

  24. $app->post('/', function (Request $request) use ($app) { $content = $request->get('content',

    ''); $id = substr(hash('sha512', mt_rand()), 0, 8); $paste = array( 'content' => $content, 'language' => $request->get('language', ''), 'created_at' => time(), ); if ('' === trim($paste['content'])) { $page = $app['twig']->render('index.html', array( 'errors' => array('you must enter some content'), 'paste' => $paste, )); return new Response($page, 400); } $app['predis']->hmset($id, $paste); return $app->redirect($app['url_generator']->generate('view', array( 'id' => $id, ))); }) ->bind('create');
  25. $app->post('/', function (Request $request) use ($app) { $content = $request->get('content',

    ''); $id = substr(hash('sha512', mt_rand()), 0, 8); $paste = array( 'content' => $content, 'language' => $request->get('language', ''), 'created_at' => time(), ); if ('' === trim($paste['content'])) { $page = $app['twig']->render('index.html', array( 'errors' => array('you must enter some content'), 'paste' => $paste, )); return new Response($page, 400); } $app['predis']->hmset($id, $paste); return $app->redirect($app['url_generator']->generate('view', array( 'id' => $id, ))); }) ->bind('create'); Parsing
  26. $app->post('/', function (Request $request) use ($app) { $content = $request->get('content',

    ''); $id = substr(hash('sha512', mt_rand()), 0, 8); $paste = array( 'content' => $content, 'language' => $request->get('language', ''), 'created_at' => time(), ); if ('' === trim($paste['content'])) { $page = $app['twig']->render('index.html', array( 'errors' => array('you must enter some content'), 'paste' => $paste, )); return new Response($page, 400); } $app['predis']->hmset($id, $paste); return $app->redirect($app['url_generator']->generate('view', array( 'id' => $id, ))); }) ->bind('create'); Validation
  27. $app->post('/', function (Request $request) use ($app) { $content = $request->get('content',

    ''); $id = substr(hash('sha512', mt_rand()), 0, 8); $paste = array( 'content' => $content, 'language' => $request->get('language', ''), 'created_at' => time(), ); if ('' === trim($paste['content'])) { $page = $app['twig']->render('index.html', array( 'errors' => array('you must enter some content'), 'paste' => $paste, )); return new Response($page, 400); } $app['predis']->hmset($id, $paste); return $app->redirect($app['url_generator']->generate('view', array( 'id' => $id, ))); }) ->bind('create'); Storage
  28. SRP

  29. ᵓᴷᴷ src ᵓᴷᴷ app.php ᵓᴷᴷ bootstrap.php ᵋᴷᴷ Igorw ᵋᴷᴷ Trashbin

    ᵋᴷᴷ *.php
  30. { "require": { "silex/silex": ">=1.0.0-dev", "symfony/finder": ">=2.1-dev", "twig/twig": "1.6.0", "predis/service-provider":

    "dev-master" }, "autoload": { "psr-0": { "Igorw\\Trashbin": "src" } } }
  31. • Parsing • Validation • Storage

  32. • Parsing => Trashbin\Parser • Validation => Trashbin\Validator • Storage

    => Trashbin\Storage
  33. namespace Igorw\Trashbin; class Parser { // ... }

  34. public function createPasteFromRequest(Request $request) { $content = $request->get('content', ''); $content

    = $this->normalizeContent($content); $id = $this->generateId(); $paste = array( 'content' => $content, ); $language = $request->get('language', ''); if (in_array($language, $this->languages)) { $paste['language'] = $language; } $paste['created_at'] = time(); return array($id, $paste); }
  35. public function createPasteFromRequest(Request $request) { $content = $request->get('content', ''); $content

    = $this->normalizeContent($content); $id = $this->generateId(); $paste = array( 'content' => $content, ); $language = $request->get('language', ''); if (in_array($language, $this->languages)) { $paste['language'] = $language; } $paste['created_at'] = time(); return array($id, $paste); } Bad
  36. public function createPasteFromRequest(Request $request) { $content = $request->get('content', ''); $content

    = $this->normalizeContent($content); $id = $this->generateId(); $paste = array( 'content' => $content, ); $language = $request->get('language', ''); if (in_array($language, $this->languages)) { $paste['language'] = $language; } $paste['created_at'] = $request->server->get('REQUEST_TIME'); return array($id, $paste); }
  37. • header() • setcookie() • var_dump() • echo, print •

    exit, die • include • time() Bad
  38. private $languages; public function __construct(array $languages) { $this->languages = $languages;

    } public function normalizeContent($content) { return preg_replace(array('#\r?\n#', '#\r#'), "\n", $content); } public function generateId() { return substr(hash('sha512', mt_rand()), 0, 8); }
  39. namespace Igorw\Trashbin; class Validator { public function validate(array $paste) {

    $errors = array(); if ('' === trim($paste['content'])) { $errors[] = 'you must enter some content'; } return $errors; } }
  40. namespace Igorw\Trashbin; use Predis\Client; class Storage { private $redis; public

    function __construct(Client $redis) { $this->redis = $redis; } public function get($id) { return $this->redis->hgetall($id); } public function set($id, array $data) { return $this->redis->hmset($id, $data); } }
  41. $app['app.parser'] = $app->share(function () use ($app) { return new Parser($app['app.languages']);

    }); $app['app.validator'] = $app->share(function () { return new Validator(); }); $app['app.storage'] = $app->share(function () use ($app) { return new Storage($app['predis']); });
  42. $app->post('/', function () use ($app) { list($id, $paste) = $app['app.parser']

    ->createPasteFromRequest($app['request']); $errors = $app['app.validator']->validate($paste); if ($errors) { $page = $app['twig']->render('index.html', array( 'errors' => $errors, 'paste' => $paste, )); return new Response($page, 400); } $app['app.storage']->set($id, $paste); $url = $app['url_generator']->generate('view', array('id' => $id)); return $app->redirect($url); }) ->bind('create');
  43. ᵓᴷᴷ composer.json ᵓᴷᴷ README.md ᵓᴷᴷ phpunit.xml.dist ᵓᴷᴷ src ᵓᴷᴷ tests

    ᵓᴷᴷ views ᵋᴷᴷ web
  44. <phpunit bootstrap="vendor/autoload.php">

  45. </tldr>

  46. Functional tests vs Unit tests

  47. use Igorw\Trashbin\Validator; class ValidatorTest extends PHPUnit_Framework_TestCase { /** * @dataProvider

    provideValidate */ public function testValidate($expected, $input) { $validator = new Validator(); $this->assertEquals($expected, $validator->validate($input)); } public function provideValidate() { return array( array(array('you must enter some content'), array('content' => '')), array(array('you must enter some content'), array('content' => ' ')), array(array('you must enter some content'), array('content' => "\t")), array(array('you must enter some content'), array('content' => "\t \n")), array(array(), array('content' => 'hello')), array(array(), array('content' => 'foobar! ')), array(array(), array('content' => 'äöü~')), ); } }
  48. BrowserKit CssSelector DomCrawler

  49. { "require": { "silex/silex": ">=1.0.0-dev", "symfony/finder": ">=2.1-dev", "twig/twig": "1.6.0", "predis/service-provider":

    "dev-master" }, "require-dev": { "symfony/browser-kit": "2.1.*@dev", "symfony/css-selector": "2.1.*@dev", "symfony/dom-crawler": "2.1.*@dev" }, "autoload": { "psr-0": { "Igorw\\Trashbin": "src" } } }
  50. use Silex\WebTestCase; class FunctionalTest extends WebTestCase { public function createApplication()

    { $app = require __DIR__.'/../src/app.php'; $app['app.storage'] = $this ->getMockBuilder('Igorw\Trashbin\Storage') ->disableOriginalConstructor() ->getMock(); unset($this->app['exception_handler']); return $app; } // ... }
  51. public function testCreatePaste() { }

  52. $paste = array('content' => 'foobar', 'created_at' => 1337882841); $this->app['app.storage'] ->expects($this->once())

    ->method('set') ->with($this->isType('string'), $paste); $this->app['app.storage'] ->expects($this->once()) ->method('get') ->with($this->isType('string')) ->will($this->returnValue($paste));
  53. $client = $this->createClient(); $client->setServerParameters(array('REQUEST_TIME' => 1337882841)); $crawler = $client->request('GET', '/');

    $form = $crawler->filter('form')->form(); $form['content'] = 'foobar'; $crawler = $client->submit($form); $crawler = $client->followRedirect(); $response = $client->getResponse(); $this->assertTrue($response->isOk()); $this->assertContains('foobar', $response->getContent());
  54. public function testCreatePasteWithoutContentShouldFail() { $client = $this->createClient(); $crawler = $client->request('GET',

    '/'); $form = $crawler->filter('form')->form(); $form['content'] = ''; $client->submit($form); $response = $client->getResponse(); $this->assertSame(400, $response->getStatusCode()); }
  55. public function testViewPaste() { $paste = array('content' => 'foobar', 'created_at'

    => 1337882841); $this->app['app.storage'] ->expects($this->once()) ->method('get') ->with('abcdef12') ->will($this->returnValue($paste)); $client = $this->createClient(); $client->request('GET', '/abcdef12'); $response = $client->getResponse(); $this->assertTrue($response->isOk()); $this->assertContains('foobar', $response->getContent()); }
  56. public function testViewPasteWithInvalidId() { $this->app['app.storage'] ->expects($this->once()) ->method('get') ->with('00000000') ->will($this->returnValue(null)); $client

    = $this->createClient(); $client->request('GET', '/00000000'); $response = $client->getResponse(); $this->assertSame(404, $response->getStatusCode()); $this->assertContains('paste not found', $response->getContent()); }
  57. None
  58. Recap • Clean code principles still apply • Decouple •

    Write tests
  59. on github igorw/trashbin

  60. Ω

  61. Questions? joind.in/6586 @igorwesome speakerdeck.com /u/igorw