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

Silex: An implementation detail

Silex: An implementation detail

Joint talk with Dave Marshall.

A4b95be2145cc46f891707b6db9dd82d?s=128

Igor Wiedler

May 22, 2013
Tweet

More Decks by Igor Wiedler

Other Decks in Programming

Transcript

  1. None
  2. None
  3. None
  4. None
  5. None
  6. None
  7. None
  8. None
  9. require_once __DIR__.'/../vendor/autoload.php'; $app = new Silex\Application(); $app->get('/hello/{name}', function ($name) use

    ($app) { return 'Hello '.$app->escape($name); }); $app->run();
  10. $app['some_service'] = $app->share( function ($app) { return new SomeService($app['other_service']); }

    );
  11. None
  12. None
  13. None
  14. Scenario: Place bid on a running auction Given there is

    an auction for some "Glasses" And I am a registered user And I am on "/" When I follow "Login" And I fill in "email" with "dave@example.com" And I fill in "password" with "password" And I press "Login" And I follow "Glasses" And I fill in "amount" with "10.00" And I select "USD" from "currency" And I press "Place Bid" Then I should see "Bid Accepted"
  15. None
  16. Scenario: Place bid on a running auction Given there is

    a running auction And I am viewing the auction When I place a bid on the auction Then I should see my bid is accepted
  17. None
  18. None
  19. None
  20. None
  21. class BidRequest { public $auctionId; public $userId; public $amount; public

    function __construct($auctionId, $userId, Money $amount) { $this->auctionId = $auctionId; $this->userId = $userId; $this->amount = $amount; } }
  22. class BidResponse { public $bid; public function __construct(BidValue $bid) {

    $this->bid = $bid; } }
  23. $interactor = new BidInteractor(); $request = new BidRequest( $auctionId, $userId,

    new Money($amount) ); $response = $interactor($request);
  24. None
  25. None
  26. namespace Douche\Interactor; class Bid { public function __invoke(BidRequest $request) {

    $auction = $this->auctionRepo->find($request->auctionId); $user = $this->userRepo->find($request->userId); $converted = $this->converter->convert( $request->amount, $auction->getCurrency() ); $bid = new BidValue($converted, $request->amount); $auction->bid($user, $bid); return new BidResponse($bid); } }
  27. None
  28. class Auction { public function getId(); public function getName(); public

    function getCurrency(); public function getEndingAt(); public function getHighestBid(); public function getHighestBidder(); public function isRunning(DateTime $now = null); public function bid(User $bidder, Bid $bid, DateTime $now = null); }
  29. public function bid(User $bidder, Bid $bid, DateTime $now = null)

    { if (!$this->isRunning($now)) { throw new AuctionClosedException(); } $highestBid = $this->getHighestBid(); if ($highestBid && $bid->getAmount() <= $highestBid->getAmount()) { throw new BidTooLowException(); } $this->bids[] = [$bidder, $bid]; }
  30. None
  31. None
  32. None
  33. None
  34. None
  35. $app->get('/auction/{id}', ...) ->convert('request', function ($_, Request $req) { return new

    AuctionViewRequest( $req->attributes->get('id') ); });
  36. $app->get('/auction/{id}', 'interactor.auction_view')

  37. public function getController(Request $req) { $controller = $req->attributes->get('_controller'); if (!is_string($controller)

    || !isset($this->container[$controller])) { return $this->resolver->getController($req); } if (!is_callable($this->container[$controller])) { throw new \InvalidArgumentException("..."); } return $this->container[$controller]; }
  38. $app['resolver'] = $app->share( $app->extend('resolver', function ($resolver, $app) { $resolver =

    new ControllerResolver( $resolver, $app ); return $resolver; }) );
  39. $dispatcher->addListener(KernelEvents::VIEW, function ($event) use ($app) { $view = $event->getControllerResult(); $request

    = $event->getRequest(); $controller = $request->attributes->get('controller'); $template = "$controller.html"; $body = $app['mustache']->render($template, $view); $response = new Response($body); $event->setResponse($response); });
  40. None
  41. {{> _header.html }} <h1>{{ auction.name }}</h1> <p>Ends: {{ auction.endingAt |

    format_date }}</p> {{# auction.highestBid }} <p>Highest bid: {{ getAmount | format_money }}</p> {{/ auction.highestBid }} {{# auction.highestBidder }} <p>Highest bidder: {{ . }}</p> {{/ auction.highestBidder }}
  42. $app->get('/auction/{id}', 'interactor.auction_view') ->value('success_handler', function ($view, $req) { return new RedirectResponse(

    "/auction/" . $request->attributes->get("id") ); });
  43. if ($request->attributes->has('success_handler')) { $fn = $request->attributes->get('success_handler'); $view = $fn($view, $request);

    if ($view instanceof Response) { $event->setResponse($view); return; } }
  44. $app->post('/login', 'interactor.user_login') ->value('error_handlers', [ "IncorrectPasswordException" => function () { return

    ['errors' => [ 'Invalid Credentials', ]]; }, ]);
  45. $app->error(function (DoucheException $e, $code) use ($app) { $handlers = $app['request']

    ->attributes ->get('error_handlers', []); foreach ($handlers as $type => $handler) { if ($e instanceof $type) { return $handler( $e, $code, $app['request'] ); } } });
  46. namespace DoucheWeb; use Douche\Interactor\AuctionListResponse; use Douche\Interactor\AuctionViewRequest; use Douche\Interactor\UserLoginRequest; use Douche\Interactor\UserLoginResponse;

    use Douche\Interactor\AuctionViewResponse; use Douche\Interactor\BidRequest; use Douche\Exception\Exception as DoucheException; use Mustache\Silex\Provider\MustacheServiceProvider; use Silex\Application; use Silex\Provider\DoctrineServiceProvider; use Silex\Provider\MonologServiceProvider; use Silex\Provider\ServiceControllerServiceProvider; use Silex\Provider\SessionServiceProvider; use Silex\ExceptionListenerWrapper; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpKernel\KernelEvents; use Money\Money; use Money\Currency; $app = new Application(); $app->register(new MonologServiceProvider()); $app->register(new DoctrineServiceProvider()); $app->register(new MustacheServiceProvider(), [ 'mustache.options' => [ 'helpers' => [ 'format_money' => function ($money) { return $money->getCurrency().' '.($money->getAmount() / 100); }, 'format_date' => function (\DateTime $date) { return $date->format("Y-m-d H:i:s"); }, ], ], ]); $app->register(new ServiceControllerServiceProvider()); $app->register(new SessionServiceProvider()); $app->register(new ServiceProvider()); $app->get('/', 'interactor.auction_list') ->value('controller', 'auction_list'); $app->get('/auction/{id}', 'interactor.auction_view') ->value('controller', 'auction_view') ->convert('request', function ($_, Request $request) { return new AuctionViewRequest($request->attributes->get('id')); }); $app->post('/auction/{id}/bids', 'interactor.bid') ->before(function (Request $request, Application $app) { if (!$request->getSession()->has('current_user')) { return $app->abort(401, 'Authenitcation Required'); } }) ->value('controller', 'bid') ->value('success_handler', function ($view, $request) { return new RedirectResponse("/auction/" . $request->attributes->get('id')); }) ->value('error_handlers', [ "Douche\Exception\BidTooLowException" => function ($e, $code, $request) { $request->getSession()->getFlashBag()->set('errors', [ 'The provided bid was too low.', ]); return new RedirectResponse("/auction/" . $request->attributes->get('id')); }, ]) ->convert('request', function ($_, Request $request) { return new BidRequest( $request->attributes->get('id'), $request->getSession()->get('current_user')->id, new Money((int) $request->request->get('amount') * 100, new Currency($request->request->get('currency'))) ); }); $app->post('/login', 'interactor.user_login') ->value('controller', 'login') ->value('success_handler', function ($view, $request) { $request->getSession()->set('current_user', $view->user); return new RedirectResponse("/"); }) ->value('error_handlers', [ "Douche\Exception\UserNotFoundException" => function ($e) { return [ 'errors' => ['Incorrect email provided.'], 'email' => $e->email, ]; }, "Douche\Exception\IncorrectPasswordException" => function ($e) { return [ 'errors' => ['Invalid credentials provided.'], 'email' => $e->email, ]; }, ]) ->convert('request', function ($_, Request $request) { return new UserLoginRequest($request->request->all()); }); $app->get('/login', function(Request $request, Application $app) { $view = [ 'errors' => [], ]; return $app['mustache']->render('login.html.mustache', $view); }); $app->get('/logout', function(Request $request, Application $app) { $request->getSession()->start(); $request->getSession()->invalidate(); return $app->redirect("/"); }); $app['resolver'] = $app->share($app->extend('resolver', function ($resolver, $app) { $resolver = new ControllerResolver($resolver, $app); return $resolver; })); // TODO change to ->error once fabpot/silex#705 is merged $app['dispatcher'] = $app->share($app->extend('dispatcher', function ($dispatcher, $app) { $dispatcher->addListener(KernelEvents::EXCEPTION, new ExceptionListenerWrapper($app, function (DoucheException $e, $code) use ($app) { $app['request']->attributes->set('failed', true); $errorHandlers = $app['request']->attributes->get('error_handlers', []); foreach ($errorHandlers as $type => $handler) { if ($e instanceof $type) { return $handler($e, $code, $app['request']); } } }), -8); return $dispatcher; })); // TODO change to ->on once fabpot/silex#705 is merged $app['dispatcher'] = $app->share($app->extend('dispatcher', function ($dispatcher, $app) { $dispatcher->addListener(KernelEvents::VIEW, function ($event) use ($app) { $view = $event->getControllerResult(); if (is_null($view) || is_string($view)) { return; } $request = $event->getRequest(); if (!$request->attributes->get('failed') && $request->attributes->has('success_handler')) { $handler = $request->attributes->get('success_handler'); $view = $handler($view, $request); if ($view instanceof Response) { $event->setResponse($view); return; } } $controller = $request->attributes->get('controller'); $template = "$controller.html"; $view = (object) $view; $view->current_user = $request->getSession()->get('current_user'); $view->form_errors = $request->getSession()->getFlashBag()->get('errors'); $body = $app['mustache']->render($template, $view); $response = new Response($body); $event->setResponse($response); }); return $dispatcher; })); // TODO change to ->after once fabpot/silex#705 is merged $app['dispatcher'] = $app->share($app->extend('dispatcher', function ($dispatcher, $app) { $dispatcher->addListener(KernelEvents::RESPONSE, function () use ($app) { $app['douche.auction_repo']->save(); }); return $dispatcher; })); return $app; service providers routes listeners
  47. None
  48. None
  49. /** * @When /^I place a bid on the auction$/

    */ public function iPlaceABidOnTheAuction() { $this->auctionHelper->placeBid(1.0); }
  50. public function placeBid($amount) { $interactor = new BidInteractor( $this->getAuctionRepository() );

    $request = new BidRequest( $this->auction->getId(), $this->getCurrentUserId(), $amount ); $this->response = $interactor($request); }
  51. public function placeBid($amount) { $page = $this->mink->getSession()->getPage(); $page->fillField('amount', $amount); $page->pressButton("Place

    Bid"); }
  52. None
  53. None
  54. None
  55. None
  56. None