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

    View full-size slide

  2. $app['some_service'] = $app->share(
    function ($app) {
    return new SomeService($app['other_service']);
    }
    );

    View full-size slide

  3. 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"

    View full-size slide

  4. 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

    View full-size slide

  5. class BidRequest
    {
    public $auctionId;
    public $userId;
    public $amount;
    public function __construct($auctionId, $userId, Money $amount)
    {
    $this->auctionId = $auctionId;
    $this->userId = $userId;
    $this->amount = $amount;
    }
    }

    View full-size slide

  6. class BidResponse
    {
    public $bid;
    public function __construct(BidValue $bid)
    {
    $this->bid = $bid;
    }
    }

    View full-size slide

  7. $interactor = new BidInteractor();
    $request = new BidRequest(
    $auctionId,
    $userId,
    new Money($amount)
    );
    $response = $interactor($request);

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  10. 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];
    }

    View full-size slide

  11. $app->get('/auction/{id}', ...)
    ->convert('request', function ($_, Request $req) {
    return new AuctionViewRequest(
    $req->attributes->get('id')
    );
    });

    View full-size slide

  12. $app->get('/auction/{id}', 'interactor.auction_view')

    View full-size slide

  13. 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];
    }

    View full-size slide

  14. $app['resolver'] = $app->share(
    $app->extend('resolver', function ($resolver, $app) {
    $resolver = new ControllerResolver(
    $resolver,
    $app
    );
    return $resolver;
    })
    );

    View full-size slide

  15. $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);
    });

    View full-size slide

  16. {{> _header.html }}
    {{ auction.name }}
    Ends: {{ auction.endingAt | format_date }}
    {{# auction.highestBid }}
    Highest bid: {{ getAmount | format_money }}
    {{/ auction.highestBid }}
    {{# auction.highestBidder }}
    Highest bidder: {{ . }}
    {{/ auction.highestBidder }}

    View full-size slide

  17. $app->get('/auction/{id}', 'interactor.auction_view')
    ->value('success_handler', function ($view, $req) {
    return new RedirectResponse(
    "/auction/"
    . $request->attributes->get("id")
    );
    });

    View full-size slide

  18. if ($request->attributes->has('success_handler')) {
    $fn = $request->attributes->get('success_handler');
    $view = $fn($view, $request);
    if ($view instanceof Response) {
    $event->setResponse($view);
    return;
    }
    }

    View full-size slide

  19. $app->post('/login', 'interactor.user_login')
    ->value('error_handlers', [
    "IncorrectPasswordException" => function () {
    return ['errors' => [
    'Invalid Credentials',
    ]];
    },
    ]);

    View full-size slide

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

    View full-size slide

  21. 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

    View full-size slide

  22. /**
    * @When /^I place a bid on the auction$/
    */
    public function iPlaceABidOnTheAuction()
    {
    $this->auctionHelper->placeBid(1.0);
    }

    View full-size slide

  23. public function placeBid($amount)
    {
    $interactor = new BidInteractor(
    $this->getAuctionRepository()
    );
    $request = new BidRequest(
    $this->auction->getId(),
    $this->getCurrentUserId(),
    $amount
    );
    $this->response = $interactor($request);
    }

    View full-size slide

  24. public function placeBid($amount)
    {
    $page = $this->mink->getSession()->getPage();
    $page->fillField('amount', $amount);
    $page->pressButton("Place Bid");
    }

    View full-size slide