Durian: a PHP 5.5 microframework with generator-style middleware

Post on 10-May-2015

735 Views

Category:

Technology

1 Downloads

Preview:

Click to see full reader

DESCRIPTION

Durian utilizes the newest features of PHP 5.4 and 5.5 as well as lightweight library components to create an accessible, compact framework with performant routing and flexible generator-style middleware.

Transcript

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 YOUbigblah@gmail.com

https://github.com/gigablah http://durianphp.com

top related