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
May 10, 2015
DURIANA PHP 5.5 microframework based on generator-style middleware
http://durianphp.com
Durian Building Singapore / Dave Cross / CC BY-NC-SA 2.0
BEFORE WE BEGIN…What the heck are generators ?
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
THE YIELD KEYWORDclass 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();}
PHP MICROFRAMEWORKS
How do they handle middleware and routing ?
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); });
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
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); });
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
“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) { // ... }); }); }); }); }); });
How about other languages ?
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);
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() }
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
COMPONENTS• Application container : Pimple by @fabpot
• Request/Response: Symfony2 HttpFoundation
• Routing: FastRoute by @nikic
• Symfony2 HttpKernelInterface (for stackphp compatibility)
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
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']; });
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
A D
B C
A B C D
function generator
Route dispatcher
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());
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!'); });
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()]; });
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]
• 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');
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);
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); } });
AWESOME EXAMPLELet’s add two integers together !
$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();
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)
THANK [email protected]
https://github.com/gigablah http://durianphp.com