Created
July 1, 2016 17:10
-
-
Save jm42/83a93dfd1785e12f8f6837d146b99124 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"type": "project", | |
"autoload": { | |
"psr-0": { | |
"Aix\\": "src/", | |
"Bossa\\": "src/" | |
} | |
}, | |
"require": { | |
"nikic/fast-route": "dev-master", | |
"PHP": "^7.0" | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace Aix; | |
use Aix\DI\{Resolver, Injector}; | |
use Aix\HTTP\{Request, Response, Dispatcher}; | |
use Aix\Routing\Directory; | |
/** Holds the state of the system and dispatch requests. | |
*/ | |
class Application { | |
/** Transforms string identifiers to actual objects, hopefully it also | |
* does dependency injection. | |
* | |
* @var Resolver | |
*/ | |
private $resolver; | |
/** Holds the routing information. | |
* | |
* @var Directory | |
*/ | |
private $directory; | |
/** List of middleware to dispatch. | |
*/ | |
private $queue = []; | |
private function build(array $carry, $handler) { | |
if (!is_callable($handler)) { | |
$handler = call_user_func($this->resolver, $handler); | |
} | |
return $carry + $handler(); | |
} | |
/** Builds and configure the resolver. | |
*/ | |
function configure(array $config, string $class=Injector::class) { | |
$this->resolver = new $class(array_reduce($config, [$this, 'build'], [])); | |
$this->resolver[Resolver::class] = $this->resolver; | |
} | |
/** Creates the project's directory. | |
*/ | |
function route(array $modules) { | |
$this->resolver[Directory::class] = $this->resolver[Directory::class] | |
->withRoutes(array_reduce($modules, [$this, 'build'], [])); | |
} | |
/** Sets the middleware queue to execute when handling a request. | |
*/ | |
function queue(array $queue) { | |
$this->queue = call_user_func($this->resolver, $queue); | |
} | |
/** Transforms a request into a response by passing it through the queue | |
* using a dispatcher. | |
*/ | |
function handle(Request $req, string $class=Dispatcher::class) { | |
$dispatcher = new $class($this->queue); | |
$dispatcher($req); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace Aix\Config; | |
use Aix\Handler\{ResponseEmitter, ExceptionHandler, RoutingHandler}; | |
use Aix\Routing\Router; | |
use Aix\DI\Resolver; | |
class MiddlewareConfig { | |
function __invoke() { | |
return array( | |
ResponseEmitter::class => (object) [], | |
ExceptionHandler::class => (object) [], | |
RoutingHandler::class => (object) [ | |
'arguments' => [Router::class, Resolver::class], | |
], | |
); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace Aix\Config; | |
use Aix\Routing\{Router, Directory, FastRoute}; | |
class RoutingConfig { | |
function __invoke() { | |
return array( | |
Directory::class => (object) [], | |
FastRoute::class => (object) ['arguments' => [Directory::class]], | |
Router::class => (object) ['alias' => FastRoute::class], | |
); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace Aix\DI; | |
/** Dependency injection container holds the rules to create new services | |
* and injecting dependencies. | |
*/ | |
class Injector implements \ArrayAccess, Resolver { | |
/** Resolver for dependencies. By default same instance is used. | |
* | |
* @var ResolverInterface | |
*/ | |
private $resolver; | |
/** A dictionary with the class or interface name as key and the structure | |
* and rules of the object to construct. | |
* | |
* @var array | |
*/ | |
private $rules; | |
/** Services already initializated and user defined parameters. | |
*/ | |
private $services = []; | |
/** List of services names being initializated at the moment. Used to avoid | |
* circular reference. | |
*/ | |
private $visited = []; | |
function __construct(array $rules, ResolverInterface $resolver=null) { | |
$this->rules = $rules; | |
$this->resolver = $resolver ?: $this; | |
} | |
function __invoke($spec) { | |
if (is_array($spec)) { | |
$ret = []; | |
foreach ($spec as $index => $item) { | |
$ret[$index] = $this($item); // recursion | |
} | |
return $ret; | |
} elseif (isset($this[$spec])) { | |
return $this[$spec]; | |
} | |
return $spec; | |
} | |
function offsetExists($id) { | |
return isset($this->services[$id]) || isset($this->rules[$id]); | |
} | |
function offsetGet($id) { | |
if (isset($this->services[$id])) { | |
return $this->services[$id]; | |
} | |
return $this->services[$id] = $this->make($id); | |
} | |
function offsetSet($id, $service) { | |
$this->services[$id] = $service; | |
} | |
function offsetUnset($id) { | |
if (isset($this->services[$id])) { | |
unset($this->services[$id]); | |
} | |
} | |
function make($id) { | |
if (!class_exists($id, true) && !interface_exists($id, true)) { | |
throw new ServiceNotFound("Service `$id` not found"); | |
} | |
$rule = $this->resolve($id); | |
$inst = new $rule->class(...$rule->arguments ?? []); | |
foreach ($rule->methods ?? [] as $method => $params) { | |
$inst->{$method}(...$params); | |
} | |
return $inst; | |
} | |
function resolve($id) { | |
if (empty($this->rules[$id])) { | |
throw new ServiceNotFound("Rule for `$id` not found"); | |
} | |
if (array_key_exists($id, $this->visited)) { | |
throw new ServiceCircularReference(sprintf("Circular dependency: `%s`", | |
implode('` -> `', array_keys($this->visited) + [$id]) | |
)); | |
} | |
$this->visited[$id] = null; | |
$rule = clone $this->rules[$id]; | |
if (isset($rule->alias)) { | |
return $this->resolve($rule->alias); | |
} | |
if (isset($rule->inherit) && isset($this->rules[$rule->inherit])) { | |
$parent = $this->resolve($rule->inherit); | |
$rule = (object) ((array) $rule + (array) $parent); | |
} | |
$rule->class = $id; | |
$rule->arguments = array_map([$this, 'valueOf'], $rule->arguments ?? []); | |
foreach ($rule->methods ?? [] as $method => $params) { | |
$rule->methods[$method] = array_map([$this, 'valueOf'], $params); | |
} | |
unset($rule->inherit); | |
unset($this->visited[$id]); | |
return $rule; | |
} | |
function valueOf($v) { | |
if (is_string($v) && strpos($v, '\\') !== false) { | |
return call_user_func($this->resolver, $v); | |
} | |
return $v; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace Aix\DI; | |
/** Resolves dependencies and do injections. | |
*/ | |
interface Resolver { | |
/** Creates a new object from the given specification. | |
*/ | |
function __invoke($spec); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace Aix\DI; | |
/** Raised when the Injector found a circular reference. | |
*/ | |
class ServiceCircularReference extends \LogicException { | |
/** List of the processed services before circular reference was found. | |
* | |
* @var array | |
*/ | |
public $path; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace Aix\DI; | |
/** Raised when a service is not found to be instanciated. | |
*/ | |
class ServiceNotFound extends \LogicException { | |
/** Holds the service name that failed. | |
* | |
* @var unknown | |
*/ | |
public $service; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace Aix\Handler; | |
use Exception; | |
use Aix\HTTP\{Request, Response}; | |
use Aix\HTTP\Response\ExceptionResponse; | |
class ExceptionHandler { | |
function __invoke(Request $req, callable $next): Response { | |
try { | |
$res = $next($req); | |
} catch (Exception $exception) { | |
$res = new ExceptionResponse($exception); | |
} | |
return $res; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace Aix\Handler; | |
use LogicException, RuntimeException; | |
use Aix\HTTP\{Request, Response}; | |
/** First and last middleware that sends the response back to the browser. | |
*/ | |
class ResponseEmitter { | |
function __invoke(Request $req, callable $next): Response { | |
static $call = false; | |
$send = $call === false; | |
$call = true; | |
$res = $next($req, function() { | |
throw new LogicException('ResponseEmitter must never be piped last'); | |
}); | |
if ($send) { | |
$this->send($res); | |
} | |
return $res; | |
} | |
private function send(Response $res) { | |
if (headers_sent()) { | |
throw new RuntimeException('Headers already sent'); | |
} | |
header(sprintf('HTTP/1.1 %d', $res->getStatusCode())); | |
foreach ($res->getHeaders() as $name => $values) { | |
$name = str_replace(' ', '-', ucwords(str_replace('-', ' ', $name))); | |
foreach ($values as $value) { | |
header("$name: $value"); | |
} | |
} | |
echo $res->getContent(); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace Aix\Handler; | |
use Aix\HTTP\{Request, Response}; | |
use Aix\Routing\{Router, NotFoundException, MethodNotAllowedException}; | |
use Aix\HTTP\Response\ExceptionResponse; | |
use Aix\DI\Resolver; | |
/** Matches the request with the router. | |
*/ | |
class RoutingHandler { | |
/** Used to match agains the method and path from the request. | |
* | |
* @var Router | |
*/ | |
private $router; | |
/** Used to resolve at last minute the action parts. | |
* | |
* @var Resolver | |
*/ | |
private $resolver; | |
function __construct(Router $router, Resolver $resolver) { | |
$this->router = $router; | |
$this->resolver = $resolver; | |
} | |
function __invoke(Request $req, callable $next): Response { | |
try { | |
$route = $this->router->match($req->getMethod(), $req->getPath()); | |
} catch (NotFoundException $e) { | |
return new ExceptionResponse($e); | |
} catch (MethodNotAllowedException $e) { | |
return new ExceptionResponse($e); | |
} | |
return $next($req); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace Aix\HTTP; | |
use Aix\HTTP\{Request, Response}; | |
use Aix\HTTP\InvalidMiddlewareException; | |
use Aix\HTTP\Response\EmptyResponse; | |
/** Calls a list of middleware as if it were a queue. | |
*/ | |
class Dispatcher { | |
/** List of middleware to dispatch. | |
*/ | |
private $queue = []; | |
function __construct(array $queue) { | |
$this->queue = $queue; | |
} | |
function __invoke(Request $req, callable $next=null): Response { | |
$done = $next ?: function() { return new EmptyResponse; }; | |
if (empty($this->queue)) { | |
return $done($req, null); | |
} | |
$handler = array_shift($this->queue); | |
if (!is_callable($handler)) { | |
throw new InvalidMiddlewareException( | |
"Middleware `$handler` must be callable"); | |
} | |
$res = $handler($req, $this); | |
if (!$res instanceof Response) { | |
throw new InvalidMiddlewareException( | |
"Middleware `$handler` must return a reponse"); | |
} | |
return $res; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace Aix\HTTP; | |
class InvalidMiddlewareException extends \LogicException { | |
/** Holds the handler that was invalid. | |
*/ | |
public $handler; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace Aix\HTTP; | |
/** Input to the application. | |
*/ | |
interface Request { | |
/** Retrieve HTTP method. | |
*/ | |
function getMethod(): string; | |
/** Retrieve target path. | |
*/ | |
function getPath(): string; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace Aix\HTTP\Request; | |
use Aix\HTTP\Request; | |
/** Request based on PHP's superglobal variables. | |
*/ | |
class ServerRequest implements Request { | |
private $method; | |
private $path; | |
function __construct(array $server) { | |
$requestUrl = $server['REQUEST_URI'] ?: '/'; | |
$requestUrl = preg_replace('#^\w++://[^/]++#', '', $requestUrl); | |
$this->method = mb_convert_case($server['REQUEST_METHOD'], MB_CASE_UPPER); | |
$this->path = current(explode('?', $requestUrl, 2)); | |
} | |
function getMethod(): string { | |
return $this->method; | |
} | |
function getPath(): string { | |
return $this->path; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace Aix\HTTP; | |
/** Output from the application. | |
*/ | |
interface Response { | |
/** Retrieve HTTP status code. | |
*/ | |
function getStatusCode(): int; | |
/** Retrieve HTTP headers. | |
* | |
* @example array( | |
* 'HOST' => ['example.com'], | |
* ); | |
*/ | |
function getHeaders(): array; | |
/** Retrieve HTTP body content. | |
*/ | |
function getContent(): string; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace Aix\HTTP\Response; | |
use Aix\HTTP\Response; | |
/** Response without content. | |
*/ | |
class EmptyResponse implements Response { | |
function getStatusCode(): int { | |
return 204; | |
} | |
function getHeaders(): array { | |
return []; | |
} | |
function getContent(): string { | |
return ''; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace Aix\HTTP\Response; | |
use Exception; | |
use Aix\HTTP\Response; | |
/** Response that shows information from a given exception. Be careful not to | |
* use it on production. | |
*/ | |
class ExceptionResponse implements Response { | |
/** Holds the exception to render. | |
* | |
* @var Exception | |
*/ | |
private $exception; | |
function __construct(Exception $exception) { | |
$this->exception = $exception; | |
} | |
function getStatusCode(): int { | |
return 500; | |
} | |
function getHeaders(): array { | |
return ['Content-Type' => ['text/plain']]; | |
} | |
function getContent(): string { | |
return 'Internal Server Error: ' . $this->exception->getMessage(); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace Aix\Routing; | |
/** Dictionary over the routing with endpoints in the form of `METHOD /path` as | |
* keys and handlers on the values. | |
*/ | |
class Directory { | |
/** Relation between the method/path and a handler. | |
*/ | |
private $routes = []; | |
function getRoutes(): array { | |
return $this->routes; | |
} | |
/** Return a clone of itself with new routes. | |
*/ | |
function withRoutes(array $routes): Directory { | |
$clone = clone $this; | |
$clone->routes = $routes; | |
return $clone; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace Aix\Routing; | |
use FastRoute\{RouteCollector, Dispatcher}; | |
use function FastRoute\simpleDispatcher; | |
class FastRoute implements Router { | |
/** FastRouter dispatcher. | |
* | |
* @var Dispatcher | |
*/ | |
private $dispatcher; | |
function __construct(Directory $dir) { | |
$this->dispatcher = simpleDispatcher(function(RouteCollector $r) use($dir) { | |
foreach ($dir->getRoutes() as $route => $handler) { | |
$data = explode(' ', $route, 2); | |
$path = array_pop($data); | |
$verb = count($data) ? array_pop($data) : 'GET'; | |
$r->addRoute($verb, $path, $handler); | |
} | |
}); | |
} | |
function match(string $method, string $uri): Route { | |
$route = $this->dispatcher->dispatch($method, $uri); | |
$status = array_shift($route); | |
if ($status === Dispatcher::FOUND) { | |
return new Route(array_shift($route), array_shift($route)); | |
} | |
if ($status === Dispatcher::METHOD_NOT_ALLOWED) { | |
$err = new MethodNotAllowedException("Method `$method` not allowed"); | |
$err->allowedMethods = array_shift($route); | |
$err->method = $method; | |
$err->uri = $uri; | |
throw $err; | |
} | |
throw new NotFoundException("Route `$method $uri` not found"); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace Aix\Routing; | |
/** Thrown when a route is matched but the given method is not allowed. | |
*/ | |
class MethodNotAllowedException extends \RuntimeException { | |
/** List of allowed methods. | |
*/ | |
public $allowedMethods = []; | |
/** Current HTTP method. | |
*/ | |
public $method; | |
/** Current HTTP path. | |
*/ | |
public $uri; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace Aix\Routing; | |
/** Thrown when a route is not matched on a router. | |
*/ | |
class NotFoundException extends \RuntimeException {} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace Aix\Routing; | |
class Route { | |
/** Callable that was assigned in the directory and was matched. It could | |
* also be a string waiting to be resolved. | |
*/ | |
private $handler; | |
/** Dictionary with matched parameters. | |
*/ | |
private $parameters; | |
function __construct($handler, array $parameters) { | |
$this->handler = $handler; | |
$this->parameters = $parameters; | |
} | |
function getHandler() { | |
return $this->handler; | |
} | |
function getParameters(): array { | |
return $this->parameters; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace Aix\Routing; | |
/** Router is injected into the routing middleware to be matched against the | |
* request. | |
*/ | |
interface Router { | |
/** Match a method/uri to the list of routes. | |
* | |
* @throws NotFoundException | |
* @throws MethodNotAllowedException | |
* @return mixed | |
*/ | |
function match(string $method, string $uri): Route; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace Bossa; | |
class BossaConfig { | |
function __invoke() { | |
return array( | |
BossaModule::class => (object) [], | |
); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace Bossa; | |
class BossaModule { | |
function __invoke() { | |
return [ | |
'GET /' => Domain\Homepage::class, | |
]; | |
} | |
function homepage() { | |
return '/'; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
include __DIR__ . '/../vendor/autoload.php'; | |
$app = new Aix\Application; | |
$app->configure([ | |
new Aix\Config\MiddlewareConfig, | |
new Aix\Config\RoutingConfig, | |
new Bossa\BossaConfig, | |
]); | |
$app->route([ | |
Bossa\BossaModule::class, | |
]); | |
$app->queue([ | |
Aix\Handler\ResponseEmitter::class, | |
//Aix\Handler\ExceptionHandler::class, | |
Aix\Handler\RoutingHandler::class, | |
]); | |
$app->handle(new Aix\HTTP\Request\ServerRequest($_SERVER)); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment