Skip to content

Instantly share code, notes, and snippets.

@jm42
Created July 1, 2016 17:10
Show Gist options
  • Save jm42/83a93dfd1785e12f8f6837d146b99124 to your computer and use it in GitHub Desktop.
Save jm42/83a93dfd1785e12f8f6837d146b99124 to your computer and use it in GitHub Desktop.
{
"type": "project",
"autoload": {
"psr-0": {
"Aix\\": "src/",
"Bossa\\": "src/"
}
},
"require": {
"nikic/fast-route": "dev-master",
"PHP": "^7.0"
}
}
<?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);
}
}
<?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],
],
);
}
}
<?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],
);
}
}
<?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;
}
}
<?php
namespace Aix\DI;
/** Resolves dependencies and do injections.
*/
interface Resolver {
/** Creates a new object from the given specification.
*/
function __invoke($spec);
}
<?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;
}
<?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;
}
<?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;
}
}
<?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();
}
}
<?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);
}
}
<?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;
}
}
<?php
namespace Aix\HTTP;
class InvalidMiddlewareException extends \LogicException {
/** Holds the handler that was invalid.
*/
public $handler;
}
<?php
namespace Aix\HTTP;
/** Input to the application.
*/
interface Request {
/** Retrieve HTTP method.
*/
function getMethod(): string;
/** Retrieve target path.
*/
function getPath(): string;
}
<?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;
}
}
<?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;
}
<?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 '';
}
}
<?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();
}
}
<?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;
}
}
<?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");
}
}
<?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;
}
<?php
namespace Aix\Routing;
/** Thrown when a route is not matched on a router.
*/
class NotFoundException extends \RuntimeException {}
<?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;
}
}
<?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;
}
<?php
namespace Bossa;
class BossaConfig {
function __invoke() {
return array(
BossaModule::class => (object) [],
);
}
}
<?php
namespace Bossa;
class BossaModule {
function __invoke() {
return [
'GET /' => Domain\Homepage::class,
];
}
function homepage() {
return '/';
}
}
<?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