$30 off During Our Annual Pro Sale. View Details »

Advanced Silex (sflive2012)

Advanced Silex (sflive2012)

Igor Wiedler

June 08, 2012
Tweet

More Decks by Igor Wiedler

Other Decks in Programming

Transcript

  1. Advanced

    View Slide

  2. • Symfony2
    • Silex
    • Composer
    igorw
    @igorwesome

    View Slide

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

    View Slide

  4. Trashbin

    View Slide

  5. View Slide

  6. View Slide

  7. View Slide

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

    View Slide

  9. $ php composer.phar install

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  13. •GET /
    •POST /
    •GET /{id}

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  18. What a freaking mess!

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  22. $app = new Silex\Application();
    // ...
    return $app;

    View Slide

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

    View Slide

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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  28. SRP

    View Slide

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

    View Slide

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

    View Slide

  31. • Parsing
    • Validation
    • Storage

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  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

    View Slide

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

    View Slide

  37. • header()
    • setcookie()
    • var_dump()
    • echo, print
    • exit, die
    • include
    • time()
    Bad

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  43. ᵓᴷᴷ composer.json
    ᵓᴷᴷ README.md
    ᵓᴷᴷ phpunit.xml.dist
    ᵓᴷᴷ src
    ᵓᴷᴷ tests
    ᵓᴷᴷ views
    ᵋᴷᴷ web

    View Slide


  44. View Slide


  45. View Slide

  46. Functional tests
    vs
    Unit tests

    View Slide

  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' => 'äöü~')),
    );
    }
    }

    View Slide

  48. BrowserKit
    CssSelector
    DomCrawler

    View Slide

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

    View Slide

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

    View Slide

  51. public function testCreatePaste()
    {
    }

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  57. View Slide

  58. Recap
    • Clean code principles still apply
    • Decouple
    • Write tests

    View Slide

  59. on github
    igorw/trashbin

    View Slide

  60. Ω

    View Slide

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

    View Slide