Skip to content

Commit

Permalink
Merge pull request #6 from krakphp/5-mountable-middleware
Browse files Browse the repository at this point in the history
Mountable Middleware #5
  • Loading branch information
ragboyjr committed Dec 5, 2016
2 parents b7d6928 + 69e5c61 commit ef1088e
Show file tree
Hide file tree
Showing 8 changed files with 188 additions and 11 deletions.
53 changes: 53 additions & 0 deletions doc/app.rst
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,56 @@ usage:
return "Hello World!";
});
$app->serve();
The App at its core manages a stack of http middleware. When the ``serve()`` function is called, it freezes the middleware into the http stack and then executes it.

The App also servers as a callable middleware that you can use in any place you would use a normal middleware. This is very useful for things like Mounting.

Mounting
--------

Http Apps allow the mounting of middleware onto the app itself. You mount the middleware on a prefix. This is similar to adding middleware onto route groups, but it is different because these middleware are executed before the routing phase. This fact is crucial because it allows you to perform authentication before routing and mount full featured apps without having to go through the routing process of the base app.

example:

.. code-block:: php
<?php
use Krak\Mw\Http;
$app = new Http\App();
$app->with(Http\Package\std());
$app->get('/', function() {
return 'Home';
});
$api = new Http\App();
$api->with(Http\Package\std());
$api->with(Http\Package\rest());
$api->get('/users', function() {
return [
['id' => 1],
['id' => 2]
];
});
$api->mount('/users', function($req, $next) use ($api) {
// basic auth with user `foo` and password `bar`
$rf = http\jsonResponseFactory($api['response_factory']);
if (!$req->hasHeader('Authorization')) {
return $rf(401, ['WWW-Authenticate' => 'Basic realm="/"'], ['code' => 'unauthorized']);
}
if ($req->getHeader('Authorization')[0] != 'Basic Zm9vOmJhcg==') {
return $rf(403, [], ['code' => 'forbidden']);
}
return $next($req);
});
$app->mount('/api', $api);
$app->serve();
In this example, all calls to the ``/api*`` will be handled via the ``$api`` application instead of the base ``$app``. Every call to ``/api/users*`` will now have to go through Basic authentication before the routing starts.
2 changes: 1 addition & 1 deletion example/advanced.php
Original file line number Diff line number Diff line change
Expand Up @@ -68,5 +68,5 @@ public function getAction($req, $params) {
throw new \Exception('Somthing Bad Happened!');
});

$app->push(Http\mount('/admin', $app1));
$app->mount('/admin', $app1);
$app->serve();
40 changes: 40 additions & 0 deletions example/auth.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?php

// php -S localhost:8000 example/auth.php
use Krak\Mw\Http;

require_once __DIR__ . '/../vendor/autoload.php';

$app = new Http\App();
$app->with(Http\Package\std());

$app->get('/', function() {
return 'Home';
});

$api = new Http\App();
$api->with(Http\Package\std());
$api->with(Http\Package\rest());
$api->get('/users', function() {
return [
['id' => 1],
['id' => 2]
];
});
$api->mount('/users', function($req, $next) use ($api) {
// basic auth with user `foo` and password `bar`
$rf = http\jsonResponseFactory($api['response_factory']);

if (!$req->hasHeader('Authorization')) {
return $rf(401, ['WWW-Authenticate' => 'Basic realm="/"'], ['code' => 'unauthorized']);
}
if ($req->getHeader('Authorization')[0] != 'Basic Zm9vOmJhcg==') {
return $rf(403, [], ['code' => 'forbidden']);
}

return $next($req);
});

$app->mount('/api', $api);

$app->serve();
23 changes: 23 additions & 0 deletions src/App.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,29 @@ public function withRoutePrefix($prefix) {
return $app;
}

/** mounts a middleware onto the stack at uri prefix. The mounted middleware is
pushed on the main http stack and will be executed before the standard routing
middleware. This is useful for mounting other applications or authentication
middleware */
public function mount($prefix, $mw) {
$mw = function($req, $next) use ($prefix, $mw) {
if (!$mw instanceof self) {
return $mw($req, $next);
}

$prefix = Util\joinUri($this['routes']->getPrefix(), $prefix);
$mw = $mw->withRoutePrefix($prefix);
return $mw($req, $next);
};

$mw = mw\filter($mw, function($req) use ($prefix, $mw) {
$prefix = Util\joinUri($this['routes']->getPrefix(), $prefix);
return strpos($req->getUri()->getPath(), $prefix) === 0;
});

return $this->push($mw);
}

/** forward to the Event Emitter */
public function on($event, callable $listener) {
return $this['event_emitter']->on($event, $listener);
Expand Down
8 changes: 8 additions & 0 deletions src/Util/util.php
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,11 @@ function isTuple($tuple, ...$types) {

return true;
}

function joinUri($a, $b) {
if ($b == '/') {
return $a;
}

return rtrim($a, '/') . '/' . ltrim($b, '/');
}
16 changes: 6 additions & 10 deletions src/router.php
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ public function group($path, $cb) {
}

public function getRoutes($prefix = '/') {
$prefix = _joinUri($prefix, $this->prefix);
$prefix = Util\joinUri($prefix, $this->prefix);

$routes = iter\flatten(iter\map(function($r) use ($prefix) {
if ($r instanceof Route) {
Expand All @@ -143,6 +143,10 @@ public function getRoutes($prefix = '/') {

return iter\toArray($routes);
}

public function getPrefix() {
return $this->prefix;
}
}

class Route {
Expand Down Expand Up @@ -170,7 +174,7 @@ public function getPath() {
public function withPathPrefix($prefix) {
return new self(
$this->methods,
_joinUri($prefix, $this->path),
Util\joinUri($prefix, $this->path),
$this->handler,
$this->attributes
);
Expand Down Expand Up @@ -205,11 +209,3 @@ public function delete($uri, $handler) {
return $this->match('DELETE', $uri, $handler);
}
}

function _joinUri($a, $b) {
if ($b == '/') {
return $a;
}

return rtrim($a, '/') . '/' . ltrim($b, '/');
}
54 changes: 54 additions & 0 deletions test/app.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<?php

use Krak\Mw\Http,
GuzzleHttp\Psr7\ServerRequest;

describe('->mount', function() {
it('mounts a middleware onto the app', function() {
$app = new Http\App();
$app->with(Http\Package\std());
$app->mount('/a', function() {
return 1;
});
$app->get('/a/1', function() { assert(false); });
$req = new ServerRequest('GET', '/a/2');
$res = $app($req, function() {});
assert($app($req, function() {}) == 1);
});
it('allows for recursive mounts', function() {
$app1 = new Http\App();
$app1->with(Http\Package\std());
$app2 = new Http\App();
$app2->with(Http\Package\std());
$app3 = new Http\App();
$app3->with(Http\Package\std());

$app3->mount('/c', function() {
return 4;
});
$app1->mount('/a', $app2);
$app2->mount('/b', $app3);

$app3->get('/c/1', function() { assert(false); });
$req = new ServerRequest('GET', '/a/b/c/d');
assert($app1($req, function() {}) == 4);
});
it('allows for recursive mounted routes', function() {
$app1 = new Http\App();
$app1->with(Http\Package\std());
$app2 = new Http\App();
$app2->with(Http\Package\std());
$app3 = new Http\App();
$app3->with(Http\Package\std());

$app2->mount('/b', $app3);
$app1->mount('/a', $app2);

$app3->get('/c/d', function() { return 2; });
// allow the return of 2 instead of converting into a response
$app3['stacks.marshal_response']->push(function($res) { return $res; });

$req = new ServerRequest('GET', '/a/b/c/d');
assert($app1($req, function() {}) == 2);
});
});
3 changes: 3 additions & 0 deletions test/http.spec.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,7 @@ function() {}
describe('Resolve Argument', function() {
require_once __DIR__ . '/resolve-argument.php';
});
describe('App', function() {
require_once __DIR__ . '/app.php';
});
});

0 comments on commit ef1088e

Please sign in to comment.