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

Durian: a PHP 5.5 microframework based on gener...

Chris Heng
February 26, 2014

Durian: a PHP 5.5 microframework based on generator-style middleware

Durian is a PHP microframework that utilizes the newest features of PHP 5.5 together with lightweight library components to create an accessible, compact framework with performant routing and flexible generator-style middleware.

Chris Heng

February 26, 2014
Tweet

More Decks by Chris Heng

Other Decks in Programming

Transcript

  1. DURIAN A PHP 5.5 microframework based on generator-style middleware http://durianphp.com

    Durian Building Singapore / Dave Cross / CC BY-NC-SA 2.0
  2. GENERATORS • Introduced in PHP 5.5 (although HHVM had them

    earlier) • Generators are basically iterators with a simpler syntax • The mere presence of the yield keyword turns a closure into a generator constructor • Generators are forward-only (cannot be rewound) • You can send() values into generators • You can throw() exceptions into generators
  3. THE YIELD KEYWORD class MyIterator implements \Iterator { private $values;

    ! public function __construct(array $values) { $this->values = $values; } public function current() { return current($this->values); } public function key() { return key($this->values); } public function next() { return next($this->values); } public function rewind() {} public function valid() { return null !== key($this->values); } } $iterator = new MyIterator([1,2,3,4,5]); while ($iterator->valid()) { echo $iterator->current(); $iterator->next(); } $callback = function (array $values) { foreach ($values as $value) { yield $value; } }; $generator = $callback([1,2,3,4,5]); while ($generator->valid()) { echo $generator->current(); $generator->next(); }
  4. EVENT LISTENERS $app->before(function (Request $request) use ($app) { $app['response_time'] =

    microtime(true); }); $app->get('/blog', function () use ($app) { return $app['blog_service']->getPosts()->toJson(); }); $app->after(function (Request $request, Response $response) use ($app) { $time = microtime(true) - $app['response_time']; $response->headers->set('X-Response-Time', $time); });
  5. THE DOWNSIDE • A decorator has to be split into

    two separate functions to wrap the main application • Data has to be passed between functions • Can be confusing to maintain
  6. HIERARCHICAL ROUTING $app->path('blog', function ($request) use ($app) { $time =

    microtime(true); $blog = BlogService::create()->initialise(); $app->path('posts', function () use ($app, $blog) { $posts = $blog->getAllPosts(); $app->get(function () use ($app, $posts) { return $app->template('posts/index', $posts->toJson()); }); }); $time = microtime(true) - $time; $this->response()->header('X-Response-Time', $time); });
  7. THE DOWNSIDE • Subsequent route and method declarations are now

    embedded inside a closure • Closure needs to be executed to proceed • Potentially incurring expensive initialisation or computations only to be discarded • Middleware code is still split across two locations
  8. “CALLBACK HELL” $app->path('a', function () use ($app) { $app->param('b', function

    ($b) use ($app) { $app->path('c', function () use ($b, $app) { $app->param('d', function ($d) use ($app) { $app->get(function () use ($d, $app) { $app->json(function () use ($app) { // ... }); }); }); }); }); });
  9. KOA (NODEJS) var koa = require('koa'); var app = koa();

    ! app.use(function *(next){ var start = new Date; yield next; var ms = new Date - start; console.log('%s %s - %s', this.method, this.url, ms); }); ! app.use(function *(){ this.body = 'Hello World'; }); ! app.listen(3000);
  10. MARTINI (GOLANG) package main import "github.com/codegangsta/martini" ! func main() {

    m := martini.Classic() ! m.Use(func(c martini.Context, log *log.Logger) { log.Println("before a request") c.Next() log.Println("after a request") }) ! m.Get("/", func() string { return "Hello world!" }) ! m.Run() }
  11. INTRODUCING DURIAN • Take advantage of PHP 5.4, 5.5 features

    • Unify interface across controllers and middleware • Avoid excessive nesting / callback hell • Use existing library components • None of this has anything to do with durians
  12. COMPONENTS • Application container: Pimple by @fabpot • Request/Response: Symfony2

    HttpFoundation • Routing: FastRoute by @nikic • Symfony2 HttpKernelInterface (for stackphp compatibility)
  13. A DURIAN APPLICATION $app = new Durian\Application(); ! $app->route('/hello/{name}', function

    () { return 'Hello '.$this->param('name'); }); ! $app->run();
 • Nothing special there, basically the same syntax as every microframework ever
  14. HANDLERS • Simple wrapper around closures and generators • Handlers

    consist of the primary callback and an optional guard callback
 $responseHandler = $app->handler(function () { $time = microtime(true); yield; $time = microtime(true) - $time; $this->response()->headers->set('X-Response-Time', $time); }, function () use ($app) { return $app['debug']; });
  15. THE HANDLER STACK • Application::handle() iterates through a generator that

    produces Handlers to be invoked • Generators produced from handlers are placed into another stack to be revisited in reverse order • A Handler may produce a generator that produces more Handlers, which are fed back to the main generator • The route dispatcher is one such handler
  16. MODIFYING THE STACK $app['middleware.response_time'] = $app->handler(function () { $time =

    microtime(true); yield; $time = microtime(true) - $time; $this->response()->headers->set('X-Response-Time', $time); }, function () use ($app) { return $this->master() && $app['debug']; }); ! $app->handlers([ 'middleware.response_time', new Durian\Middleware\RouterMiddleware() ]); ! $app->after(new Durian\Middleware\ResponseMiddleware()); ! $app->before(new Durian\Middleware\WhoopsMiddleware());
  17. ROUTE HANDLER • Apply the handler concept to route matching

    
 $app->handler(function () { $this->response('Hello World!'); }, function () { $matcher = new RequestMatcher('^/$'); return $matcher->matches($this->request()); }); • Compare to 
 $app->route('/', function () { $this->response('Hello World!'); });
  18. ROUTE CHAINING $app['awesome_library'] = $app->share(function ($app) { return new MyAwesomeLibrary();

    }); ! $app->route('/hello', function () use ($app) { $app['awesome_library']->performExpensiveOperation(); yield 'Hello '; $app['awesome_library']->performCleanUp(); })->route('/{name}', function () { return $this->last().$this->param('name'); })->get(function () { return ['method' => 'GET', 'message' => $this->last()]; })->post(function () { return ['method' => 'POST', 'message' => $this->last()]; });
  19. ROUTE DISPATCHING • This route definition: 
 $albums = $app->route('/albums',

    A)->get(B)->post(C); $albums->route('/{aid:[0-9]+}', D, E)->get(F)->put(G, H)->delete(I); • Gets turned into: 
 GET /albums => [A,B]" POST /albums => [A,C]" GET /albums/{aid} => [A,D,E,F]" PUT /albums/{aid} => [A,D,E,G,H]" DELETE /albums/{aid} => [A,D,E,I]
  20. • Route chaining isn’t mandatory ! • You can still

    use the regular syntax
 // Routes will support GET by default $app->route('/users'); ! // Methods can be declared without handlers $app->route('/users/{name}')->post(); ! // Declare multiple methods separated by pipe characters $app->route('/users/{name}/friends')->method('GET|POST');
  21. CONTEXT • Every handler is bound to the Context object

    using Closure::bind • A new context is created for every request or sub request Get the Request object $request = $this->request(); Get the Response $response = $this->response(); Set the Response $this->response("I'm a teapot", 418); Get the last handler output $last = $this->last(); Get a route parameter $id = $this->param('id'); Throw an error $this->error('Forbidden', 403);
  22. EXCEPTION HANDLING • Exceptions are caught and bubbled back up

    through all registered generators • Intercept them by wrapping the yield statement with a try/catch block
 $exceptionHandlerMiddleware = $app->handler(function () { try { yield; } catch (\Exception $exception) { $this->response($exception->getMessage(), 500); } });
  23. $app->route('/add', function () use ($app) {
 
 $app['number_collection'] = $app->share(function

    ($app) { return new NumberCollection(); }); $app['number_parser'] = $app->share(function ($app) { return new SimpleNumberStringParser(); });" yield; $addition = new AdditionOperator('SimplePHPEasyPlus\Number\SimpleNumber'); $operation = new ArithmeticOperation($addition); $engine = new Engine($operation); $calcul = new Calcul($engine, $app['number_collection']); $runner = new CalculRunner(); $runner->run($calcul); $result = $calcul->getResult(); $numericResult = $result->getValue(); $this->response('The answer is: ' . $numericResult);
 })->route('/{first:[0-9]+}', function () use ($app) {
 $firstParsedNumber = $app['number_parser']->parse($this->param('first')); $firstNumber = new SimpleNumber($firstParsedNumber); $firstNumberProxy = new CollectionItemNumberProxy($firstNumber); $app['number_collection']->add($firstNumberProxy);
 })->route('/{second:[0-9]+}', function () use ($app) {
 $secondParsedNumber = $app['number_parser']->parse($this->param('second')); $secondNumber = new SimpleNumber($secondParsedNumber); $secondNumberProxy = new CollectionItemNumberProxy($secondNumber); $app['number_collection']->add($secondNumberProxy);
 })->get();
  24. COMING SOON • Proper tests and coverage (!!!) • Handlers

    for format negotiation, session, locale, etc • Dependency injection through reflection (via trait) • Framework/engine-agnostic view composition and template rendering (separate project)