Durian Building Singapore / Dave Cross / CC BY-NC-SA 2.0

DURIAN
A PHP 5.5 microframework based on generator-style middleware	

http://durianphp.com
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 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();	
}
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 DurianApplication();	
!

$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
function

generator

Route dispatcher
A

D

B

A

C

B

C

D
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 DurianMiddlewareRouterMiddleware()	
]);	

!
$app->after(new DurianMiddlewareResponseMiddleware());	

!
$app->before(new DurianMiddlewareWhoopsMiddleware());
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
POST
GET
PUT
DELETE

/albums
/albums
/albums/{aid}
/albums/{aid}
/albums/{aid}

=>
=>
=>
=>
=>

[A,B]"
[A,C]"
[A,D,E,F]"
[A,D,E,G,H]"
[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 EXAMPLE
Let’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('SimplePHPEasyPlusNumberSimpleNumber');	
$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 YOU
bigblah@gmail.com	

https://github.com/gigablah	

http://durianphp.com

Durian: a PHP 5.5 microframework with generator-style middleware

  • 1.
    Durian Building Singapore/ Dave Cross / CC BY-NC-SA 2.0 DURIAN A PHP 5.5 microframework based on generator-style middleware http://durianphp.com
  • 2.
    BEFORE WE BEGIN… Whatthe heck are generators ?
  • 3.
    GENERATORS • Introduced in PHP5.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
  • 4.
    THE YIELD KEYWORD classMyIterator 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(); }
  • 5.
  • 6.
    How do theyhandle middleware and routing ?
  • 7.
    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); });
  • 8.
    THE DOWNSIDE • A decoratorhas to be split into two separate functions to wrap the main application • Data has to be passed between functions • Can be confusing to maintain
  • 9.
    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); });
  • 10.
    THE DOWNSIDE • Subsequent routeand 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
  • 11.
    “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) { // ... }); }); }); }); }); });
  • 12.
    How about otherlanguages ?
  • 13.
    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);
  • 14.
    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() }
  • 15.
    INTRODUCING DURIAN • Take advantageof 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
  • 16.
    COMPONENTS • Application container: Pimpleby @fabpot • Request/Response: Symfony2 HttpFoundation • Routing: FastRoute by @nikic • Symfony2 HttpKernelInterface (for stackphp compatibility)
  • 17.
    A DURIAN APPLICATION $app= new DurianApplication(); ! $app->route('/hello/{name}', function () { return 'Hello '.$this->param('name'); }); ! $app->run();
 • Nothing special there, basically the same syntax as every microframework ever
  • 18.
    HANDLERS • Simple wrapper aroundclosures 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']; });
  • 19.
    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
  • 20.
  • 21.
    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 DurianMiddlewareRouterMiddleware() ]); ! $app->after(new DurianMiddlewareResponseMiddleware()); ! $app->before(new DurianMiddlewareWhoopsMiddleware());
  • 22.
    ROUTE HANDLER • Apply thehandler 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!'); });
  • 23.
    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()]; });
  • 24.
    ROUTE DISPATCHING • This routedefinition: 
 $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 POST GET PUT DELETE /albums /albums /albums/{aid} /albums/{aid} /albums/{aid} => => => => => [A,B]" [A,C]" [A,D,E,F]" [A,D,E,G,H]" [A,D,E,I]
  • 25.
    • Route chaining isn’tmandatory ! • 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');
  • 26.
    CONTEXT • Every handler isbound 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);
  • 27.
    EXCEPTION HANDLING • Exceptions arecaught 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); } });
  • 28.
    AWESOME EXAMPLE Let’s addtwo integers together !
  • 30.
    $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('SimplePHPEasyPlusNumberSimpleNumber'); $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();
  • 31.
    COMING SOON • Proper testsand coverage (!!!) • Handlers for format negotiation, session, locale, etc • Dependency injection through reflection (via trait) • Framework/engine-agnostic view composition and template rendering (separate project)
  • 32.