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.

Igor Wiedler

May 22, 2013
Tweet

More Decks by Igor Wiedler

Other Decks in Programming

Transcript

  1. 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 "[email protected]" 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"
  2. 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
  3. class BidRequest { public $auctionId; public $userId; public $amount; public

    function __construct($auctionId, $userId, Money $amount) { $this->auctionId = $auctionId; $this->userId = $userId; $this->amount = $amount; } }
  4. $interactor = new BidInteractor(); $request = new BidRequest( $auctionId, $userId,

    new Money($amount) ); $response = $interactor($request);
  5. 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); } }
  6. 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); }
  7. 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]; }
  8. 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]; }
  9. $app['resolver'] = $app->share( $app->extend('resolver', function ($resolver, $app) { $resolver =

    new ControllerResolver( $resolver, $app ); return $resolver; }) );
  10. $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); });
  11. {{> _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 }}
  12. $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'] ); } } });
  13. 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
  14. /** * @When /^I place a bid on the auction$/

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

    $request = new BidRequest( $this->auction->getId(), $this->getCurrentUserId(), $amount ); $this->response = $interactor($request); }