From 4468833c46108f3ca451796a6a5f6824e6abb021 Mon Sep 17 00:00:00 2001 From: Robert Sesek Date: Mon, 28 Nov 2016 02:13:47 -0500 Subject: [PATCH] Initial implementation of a new HTTP library based on pipelines. A Pipeline is a series of chained Middleware that execute in order to produce a response. The goal is for the http2 subsystem to implement the same concepts as http, but with looser coupling between the controllers and output filters. --- http2/api.php | 61 +++++++++++++++++++++++++++++++++ http2/middleware.php | 62 ++++++++++++++++++++++++++++++++++ http2/pipeline.php | 80 ++++++++++++++++++++++++++++++++++++++++++++ http2/request.php | 64 +++++++++++++++++++++++++++++++++++ http2/response.php | 66 ++++++++++++++++++++++++++++++++++++ 5 files changed, 333 insertions(+) create mode 100644 http2/api.php create mode 100644 http2/middleware.php create mode 100644 http2/pipeline.php create mode 100644 http2/request.php create mode 100644 http2/response.php diff --git a/http2/api.php b/http2/api.php new file mode 100644 index 0000000..0a901ac --- /dev/null +++ b/http2/api.php @@ -0,0 +1,61 @@ +. + +namespace hoplite\http2; + +require_once HOPLITE_ROOT . '/http2/middleware.php'; +require_once HOPLITE_ROOT . '/http2/request.php'; +require_once HOPLITE_ROOT . '/http2/response.php'; + +class DataResponse extends Response { + public $data; + + public function __construct($data) { + parent::__construct(); + $this->data = $data; + } + + public function generate() { + throw new \Exception(__CLASS__ . ' cannot generate a response'); + } +} + +class JsonResponse extends Response { + private $data; + + public function __construct(DataResponse $r) { + $this->code = $r->code; + $this->data = $r->data; + } + + public function generate() { + $this->headers['Content-Type'] = 'application/json'; + parent::generate(); + print json_encode($this->data); + } +} + +class JsonResponseFilter extends Middleware { + public function execute(Request $request) { + $response = $this->next->execute($request); + + if ($response instanceof DataResponse) { + return new JsonResponse($response); + } + + return $response; + } +} diff --git a/http2/middleware.php b/http2/middleware.php new file mode 100644 index 0000000..4e51841 --- /dev/null +++ b/http2/middleware.php @@ -0,0 +1,62 @@ +. + +namespace hoplite\http2; + +require_once HOPLITE_ROOT . '/http2/pipeline.php'; +require_once HOPLITE_ROOT . '/http2/request.php'; +require_once HOPLITE_ROOT . '/http2/response.php'; + +abstract class Middleware { + protected $pipeline; + protected $next; + + public function __construct(Pipeline $pipeline, + Middleware $next) { + $this->pipeline = $pipeline; + $this->next = $next; + } + + public abstract /*http\Response*/ function execute(Request $request); +} + +class Sentinel extends Middleware { + private $response; + + public function __construct(Response $response) { + $this->response = $response; + } + + public function execute(Request $request) { + return $this->response; + } +} + +class ClosureMiddleware extends Middleware { + private $closure; + + public function __construct(Pipeline $pipeline, + Middleware $next, + \Closure $closure) { + parent::__construct($pipeline, $next); + $this->closure = $closure; + } + + public function execute(Request $request) { + $closure = $this->closure; + return $closure($request, $this->next); + } +} diff --git a/http2/pipeline.php b/http2/pipeline.php new file mode 100644 index 0000000..5d7f885 --- /dev/null +++ b/http2/pipeline.php @@ -0,0 +1,80 @@ +. + +namespace hoplite\http2; + +require_once HOPLITE_ROOT . '/http2/middleware.php'; +require_once HOPLITE_ROOT . '/http2/request.php'; +require_once HOPLITE_ROOT . '/http2/response.php'; + +class Pipeline { + private $middleware = []; + + public function add($middleware) { + /* + if (!($middleware instanceof Middleware || + $middleware instanceof \Closure || + is_array($middleware))) { + // TODO(php7): TypeError + throw new \Exception(__FUNCTION__ . ' requires a Middleware or Closure'); + } + */ + $this->middleware[] = $middleware; + return $this; + } + + public function build() { + $chain = new Sentinel(new NotFoundResponse()); + for ($i = count($this->middleware) - 1; $i >= 0; --$i) { + $args = $this->middleware[$i]; + if ($args instanceof \Closure) { + $chain = new ClosureMiddleware($this, $chain, $args); + } else if (is_array($args)) { + $class = array_shift($args); + array_unshift($args, $this, $chain); + $chain = new $class(...$args); + } else { + $chain = new $args($this, $chain); + } + } + + return $chain; + } + + public function run(Middleware $chain) { + $response = $chain->execute(new Request()); + $response->generate(); + } + + public function buildAndRun() { + $this->run($this->build()); + } +} + +class SubPipeline extends Middleware { + public function __construct(Pipeline $subpipe) { + // Skip calling super since this is a new pipeline and the + // next middleware will be selected by building the subpipe. + // It would be possible to build the middleware chain here, but + // doing it lazily on execution is more efficient, since SubPipeline + // should be used to partition applications. + $this->subpipe = $subpipe; + } + + public function execute(Request $request) { + return $this->subpipe->build()->execute($request); + } +} diff --git a/http2/request.php b/http2/request.php new file mode 100644 index 0000000..f1ad7eb --- /dev/null +++ b/http2/request.php @@ -0,0 +1,64 @@ +. + +namespace hoplite\http2; + +/*! + A Request represents a HTTP request and holds the data and context associated + with it. +*/ +class Request +{ + /*! @var string The request method (upper case). */ + public $http_method = NULL; + + /*! @var string The URL, relataive to the RootController. */ + public $url = ''; + + /*! @var array HTTP request data. */ + public $data = []; + + /*! @var array Context data. */ + public $context = []; + + /*! + Constructor. Takes an optional URL. + */ + public function __construct($url='') + { + $this->url = $url; + } + + /*! + Wrapper around filter_input() that stores the result in the ::$data field. + */ + public function filter($type, $name, $filter=FILTER_SANITIZE_STRING, $options=NULL) + { + $rv = filter_input($type, $name, $filter, $options); + $this->data[$name] = $rv; + return $rv; + } + + /*! + Wrapper around filter_input() that merges the result in the ::$data field. + */ + public function filterArray($type, $definition, $add_empty=TRUE) + { + $rv = filter_input_array($type, $definition, $add_empty); + $this->data = array_merge($this->data, $rv); + return $rv; + } +} diff --git a/http2/response.php b/http2/response.php new file mode 100644 index 0000000..f29f5f8 --- /dev/null +++ b/http2/response.php @@ -0,0 +1,66 @@ +. + +namespace hoplite\http2; + +require_once HOPLITE_ROOT . '/http/response_code.php'; + +class Response +{ + /*! @var integer The HTTP response code to return. */ + public $code; + + /*! @var array A map of headers to values to be sent with the response. */ + public $headers = []; + + public function __construct($code=\hoplite\http\ResponseCode::OK) { + $this->code = $code; + } + + /*! @var string Raw HTTP response body. */ + public function generate() { + http_response_code($this->code); + foreach ($this->headers as $header => $value) { + header("$header: $value"); + } + } +} + +class NotFoundResponse extends Response { + public function __construct() { + parent::__construct(\hoplite\http\ResponseCode::NOT_FOUND); + } + + public function generate() { + parent::generate(); + print '

404 - Not Found

'; + } +} + +class TextResponse extends Response { + private $text; + + public function __construct($text) { + parent::__construct(); + $this->text = $text; + $this->headers['Content-Type'] = 'text/plain'; + } + + public function generate() { + parent::generate(); + print $this->text; + } +} -- 2.22.5