├── .gitignore
├── .htaccess
├── README.md
├── composer.json
├── composer.lock
├── index.php
└── src
├── BasePathResolverMiddleware.php
├── CacheMiddleware.php
├── CallableHttpKernel.php
├── DecodeJsonMiddleware.php
├── DecodeMyXmlMiddleware.php
├── DispatchingMiddleware.php
├── EventMiddleware.php
├── ForbiddenError.php
├── HttpError.php
├── HttpMiddlewareInterface.php
├── HttpPathMiddleware.php
├── HttpSender.php
├── MyDomainObject.php
├── NegotiationMiddleware.php
├── NotFoundError.php
├── RoutingMiddleware.php
├── StringStream.php
└── StringValue.php
/.gitignore:
--------------------------------------------------------------------------------
1 | # Created by .ignore support plugin (hsz.mobi)
2 | ### JetBrains template
3 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm
4 |
5 | *.iml
6 |
7 | ## Directory-based project format:
8 | .idea/
9 | # if you remove the above rule, at least ignore the following:
10 |
11 | # User-specific stuff:
12 | # .idea/workspace.xml
13 | # .idea/tasks.xml
14 | # .idea/dictionaries
15 |
16 | # Sensitive or high-churn files:
17 | # .idea/dataSources.ids
18 | # .idea/dataSources.xml
19 | # .idea/sqlDataSources.xml
20 | # .idea/dynamic.xml
21 | # .idea/uiDesigner.xml
22 |
23 | # Gradle:
24 | # .idea/gradle.xml
25 | # .idea/libraries
26 |
27 | # Mongo Explorer plugin:
28 | # .idea/mongoSettings.xml
29 |
30 | ## File-based project format:
31 | *.ipr
32 | *.iws
33 |
34 | ## Plugin-specific files:
35 |
36 | # IntelliJ
37 | out/
38 |
39 | # mpeltonen/sbt-idea plugin
40 | .idea_modules/
41 |
42 | # JIRA plugin
43 | atlassian-ide-plugin.xml
44 |
45 | # Crashlytics plugin (for Android Studio and IntelliJ)
46 | com_crashlytics_export_strings.xml
47 | crashlytics.properties
48 | crashlytics-build.properties
49 |
50 |
51 | vendor
--------------------------------------------------------------------------------
/.htaccess:
--------------------------------------------------------------------------------
1 |
My event was here!
\n"; 122 | return $response->withBody(new StringStream($content)); 123 | } 124 | }); 125 | 126 | // Content negotiation, using the Willdurand library. 127 | $kernel = new NegotiationMiddleware($kernel); 128 | 129 | // Body parsing. 130 | $kernel = new \Crell\Stacker\DecodeJsonMiddleware($kernel); 131 | $kernel = new \Crell\Stacker\DecodeMyXmlMiddleware($kernel); 132 | 133 | // A one-off handler. 134 | $kernel = new HttpPathMiddleware($kernel, '/bye', function(RequestInterface $request) { 135 | return new Response(new StringStream('Goodbye World')); 136 | }); 137 | 138 | // The outer-most kernel, strip off a base path. 139 | // In actual usage this would be some derived value or configured or something. 140 | $kernel = new BasePathResolverMiddleware($kernel, '/~crell/stacker'); 141 | 142 | $kernel = new CacheMiddleware($kernel); 143 | 144 | $response = $kernel->handle($request); 145 | 146 | $sender = new HttpSender(); 147 | $sender->send($response); 148 | -------------------------------------------------------------------------------- /src/BasePathResolverMiddleware.php: -------------------------------------------------------------------------------- 1 | inner = $inner; 23 | $this->basePath = $basePath; 24 | } 25 | 26 | public function handle(ServerRequestInterface $request) 27 | { 28 | $uri = $request->getUri(); 29 | $path = $uri->getPath(); 30 | 31 | if (strpos($path, $this->basePath) == 0) { 32 | // This song-and-dance is actually rather annoying. 33 | $newPath = substr($path, strlen($this->basePath)); 34 | $uri = $uri->withPath($newPath); 35 | $request = $request->withUri($uri); 36 | } 37 | 38 | return $this->inner->handle($request); 39 | } 40 | 41 | } -------------------------------------------------------------------------------- /src/CacheMiddleware.php: -------------------------------------------------------------------------------- 1 | inner = $inner; 31 | } 32 | 33 | public function handle(ServerRequestInterface $request) 34 | { 35 | if ($cachedResponse = $this->getFromCache($request)) { 36 | return $cachedResponse; 37 | } 38 | 39 | $response = $this->inner->handle($request); 40 | 41 | $response = $this->setCacheValues($response); 42 | 43 | if ($this->isNotModified($request, $response)) { 44 | return $response 45 | ->withStatus(304) 46 | ->withBody(new StringStream('')) 47 | ; 48 | } 49 | 50 | $response = $this->cache($request, $response); 51 | return $response; 52 | } 53 | 54 | protected function cache(ServerRequestInterface $request, ResponseInterface $response) 55 | { 56 | $this->totallyStupidCache[$request->getUri()->getPath()] = $response; 57 | return $response; 58 | } 59 | 60 | /** 61 | * @param ServerRequestInterface $request 62 | * @param ResponseInterface $response 63 | * @return bool 64 | */ 65 | protected function isNotModified(ServerRequestInterface $request, ResponseInterface $response) 66 | { 67 | // Yeah this could TOTALLY be more robust. :-) 68 | 69 | if ($etag = $request->getHeader('If-none-match')) { 70 | if ($etag == $response->getHeader('Etag')) { 71 | return true; 72 | } 73 | } 74 | 75 | return false; 76 | } 77 | 78 | protected function setCacheValues(ResponseInterface $response) 79 | { 80 | $etag = sha1($response->getBody()->getContents()); 81 | 82 | // This is technically mutation of the body stream. :-( 83 | $response->getBody()->rewind(); 84 | 85 | $response = $response 86 | ->withHeader('Etag', $etag) 87 | // 10 second cache, just enough to show it works. 88 | ->withHeader('Cache-Control', 'max-age=10, public'); 89 | 90 | return $response; 91 | } 92 | 93 | /** 94 | * @param ServerRequestInterface $request 95 | * @return ResponseInterface|null 96 | */ 97 | protected function getFromCache(ServerRequestInterface $request) 98 | { 99 | $uri = $request->getUri(); 100 | $path = $uri->getPath(); 101 | 102 | if (!empty($this->totallyStupidCache[$path])) { 103 | return $this->totallyStupidCache[$path]; 104 | } 105 | 106 | return null; 107 | } 108 | 109 | } -------------------------------------------------------------------------------- /src/CallableHttpKernel.php: -------------------------------------------------------------------------------- 1 | callable = $callable; 25 | } 26 | 27 | public function handle(ServerRequestInterface $request) 28 | { 29 | $call = $this->callable; 30 | $response = $call($request); 31 | 32 | if (!$response instanceof ResponseInterface) { 33 | throw new \UnexpectedValueException('Kernel function did not return an object of type Response'); 34 | } 35 | 36 | return $response; 37 | } 38 | } -------------------------------------------------------------------------------- /src/DecodeJsonMiddleware.php: -------------------------------------------------------------------------------- 1 | inner = $inner; 18 | } 19 | 20 | public function handle(ServerRequestInterface $request) 21 | { 22 | if ($request->getHeader('content-type') == 'application/json') { 23 | $decoded = json_decode($request->getBody()->getContents()); 24 | $request = $request->withBodyParams($decoded); 25 | } 26 | return $this->inner->handle($request); 27 | } 28 | 29 | } 30 | 31 | -------------------------------------------------------------------------------- /src/DecodeMyXmlMiddleware.php: -------------------------------------------------------------------------------- 1 | inner = $inner; 18 | } 19 | 20 | public function handle(ServerRequestInterface $request) 21 | { 22 | if ($request->getHeader('content-type') == 'application/xml') { 23 | $content = $request->getBody()->getContents(); 24 | $simplexml = simplexml_load_string($content); 25 | $data = new MyDomainObject($simplexml); 26 | $request = $request->withBodyParams(['data' => $data]); 27 | } 28 | return $this->inner->handle($request); 29 | } 30 | 31 | } 32 | 33 | -------------------------------------------------------------------------------- /src/DispatchingMiddleware.php: -------------------------------------------------------------------------------- 1 | responderBus = $responderBus; 19 | } 20 | 21 | public function handle(ServerRequestInterface $request) 22 | { 23 | $action = $request->getAttribute('action'); 24 | 25 | $arguments = $this->getArguments($request, $request->getAttributes(), $action); 26 | 27 | $result = call_user_func_array($action, $arguments); 28 | 29 | if (is_string($result)) { 30 | $result = new StringValue($result); 31 | } 32 | else if (is_array($result)) { 33 | $result = new \ArrayObject($result); 34 | } 35 | 36 | return $this->responderBus->transform($result); 37 | } 38 | 39 | // These two functions are ripped *almost* directly from Symfony HttpKernel. 40 | 41 | public function getArguments(RequestInterface $request, $candidates, $action) 42 | { 43 | if (is_array($action)) { 44 | $r = new \ReflectionMethod($action[0], $action[1]); 45 | } elseif (is_object($action) && !$action instanceof \Closure) { 46 | $r = new \ReflectionObject($action); 47 | $r = $r->getMethod('__invoke'); 48 | } else { 49 | $r = new \ReflectionFunction($action); 50 | } 51 | return $this->doGetArguments($request, $candidates, $action, $r->getParameters()); 52 | } 53 | 54 | protected function doGetArguments(RequestInterface $request, array $candidates, $action, array $parameters) 55 | { 56 | $arguments = array(); 57 | foreach ($parameters as $param) { 58 | if (array_key_exists($param->name, $candidates)) { 59 | $arguments[] = $candidates[$param->name]; 60 | } elseif ($param->getClass() && $param->getClass()->isInstance($request)) { 61 | $arguments[] = $request; 62 | } elseif ($param->isDefaultValueAvailable()) { 63 | $arguments[] = $param->getDefaultValue(); 64 | } else { 65 | if (is_array($action)) { 66 | $repr = sprintf('%s::%s()', get_class($action[0]), $action[1]); 67 | } elseif (is_object($action)) { 68 | $repr = get_class($action); 69 | } else { 70 | $repr = $action; 71 | } 72 | throw new \RuntimeException(sprintf('Controller "%s" requires that you provide a value for the "$%s" argument (because there is no default value or because there is a non optional argument after this one).', $repr, $param->name)); 73 | } 74 | } 75 | return $arguments; 76 | } 77 | } -------------------------------------------------------------------------------- /src/EventMiddleware.php: -------------------------------------------------------------------------------- 1 | inner = $inner; 27 | } 28 | 29 | public function addRequestListener(callable $listener, $priority = 0) 30 | { 31 | $this->listeners['request'][$priority][] = $listener; 32 | } 33 | 34 | public function addResponseListener(callable $listener, $priority = 0) 35 | { 36 | $this->listeners['response'][$priority][] = $listener; 37 | } 38 | 39 | public function handle(ServerRequestInterface $request) 40 | { 41 | $request = $this->fireRequestListeners($request); 42 | $response = $this->inner->handle($request); 43 | 44 | $response = $this->fireResponseListeners($request, $response); 45 | return $response; 46 | } 47 | 48 | protected function fireRequestListeners(ServerRequestInterface $request) 49 | { 50 | return $this->fireListeners($request, 'request', ServerRequestInterface::class); 51 | } 52 | 53 | protected function fireResponseListeners(ServerRequestInterface $request, ResponseInterface $response) 54 | { 55 | $priority = $this->listeners['response']; 56 | ksort($priority); 57 | 58 | foreach ($priority as $listeners) { 59 | foreach ($listeners as $listener) { 60 | $ret = $listener($request, $response); 61 | // Listeners can modify the object by returning a new one, but otherwise 62 | // cannot change anything. 63 | // They also cannot short circuit other listeners; if you want to do that, 64 | // use a middleware instead! 65 | if ($ret instanceof ResponseInterface) { 66 | $response = $ret; 67 | } 68 | } 69 | } 70 | return $response; 71 | } 72 | 73 | protected function fireListeners($object, $type, $classType) 74 | { 75 | $priority = $this->listeners[$type]; 76 | ksort($priority); 77 | 78 | foreach ($priority as $listeners) { 79 | foreach ($listeners as $listener) { 80 | $ret = $listener($object); 81 | // Listeners can modify the object by returning a new one, but otherwise 82 | // cannot change anything. 83 | // They also cannot short circuit other listeners; if you want to do that, 84 | // use a middleware instead! 85 | if ($ret instanceof $classType) { 86 | $object = $ret; 87 | } 88 | } 89 | } 90 | 91 | return $object; 92 | } 93 | 94 | } 95 | -------------------------------------------------------------------------------- /src/ForbiddenError.php: -------------------------------------------------------------------------------- 1 | message = $message ?: $this->defaultMessage(); 16 | } 17 | 18 | public function __toString() 19 | { 20 | return $this->message; 21 | } 22 | 23 | protected abstract function defaultMessage(); 24 | } 25 | -------------------------------------------------------------------------------- /src/HttpMiddlewareInterface.php: -------------------------------------------------------------------------------- 1 | inner = $inner; 28 | $this->path = $path; 29 | $this->callable = $callable; 30 | } 31 | 32 | public function handle(ServerRequestInterface $request) 33 | { 34 | if ($request->getUri()->getPath() == $this->path) { 35 | $call = $this->callable; 36 | return $call($request); 37 | } 38 | else { 39 | return $this->inner->handle($request); 40 | } 41 | } 42 | 43 | } 44 | 45 | -------------------------------------------------------------------------------- /src/HttpSender.php: -------------------------------------------------------------------------------- 1 | out = $out; 22 | } 23 | 24 | 25 | public function send(ResponseInterface $response) 26 | { 27 | $this->sendHeaders($response); 28 | $this->sendBody($response); 29 | 30 | } 31 | 32 | public function sendHeaders(ResponseInterface $response) 33 | { 34 | header(sprintf('HTTP/%s %s %s', $response->getProtocolVersion(), $response->getStatusCode(), $response->getReasonPhrase()), true, $response->getStatusCode()); 35 | 36 | foreach ($response->getHeaders() as $name => $values) { 37 | foreach ($values as $value) { 38 | header(sprintf('%s: %s', $name, $value), false); 39 | } 40 | } 41 | } 42 | 43 | public function sendBody(ResponseInterface $response) 44 | { 45 | $body = $response->getBody(); 46 | 47 | // I don't trust that this will be at the beginning of the stream, 48 | // so reset. 49 | $body->rewind(); 50 | 51 | // @todo Use stream operations to make this more robust and allow 52 | // writing to an arbitrary stream. 53 | if ($bytes = $body->getSize() && $bytes < 500) { 54 | print $body->getContents(); 55 | } 56 | else { 57 | while (!$body->eof()) { 58 | $data = $body->read(1024); 59 | print $data; 60 | } 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/MyDomainObject.php: -------------------------------------------------------------------------------- 1 | xml = $xml; 17 | } 18 | 19 | public function getName() 20 | { 21 | return "Larry"; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/NegotiationMiddleware.php: -------------------------------------------------------------------------------- 1 | [], 51 | 'language_priorities' => [], 52 | ]; 53 | 54 | /** 55 | * @var array 56 | */ 57 | private $options; 58 | 59 | public function __construct( 60 | HttpMiddlewareInterface $app, 61 | FormatNegotiatorInterface $formatNegotiator = null, 62 | NegotiatorInterface $languageNegotiator = null, 63 | DecoderProviderInterface $decoderProvider = null, 64 | array $options = [] 65 | ) { 66 | $this->app = $app; 67 | $this->formatNegotiator = $formatNegotiator ?: new FormatNegotiator(); 68 | $this->languageNegotiator = $languageNegotiator ?: new LanguageNegotiator(); 69 | $this->decoderProvider = $decoderProvider ?: new DecoderProvider([ 70 | 'json' => new JsonEncoder(), 71 | 'xml' => new XmlEncoder(), 72 | ]); 73 | $this->options = array_merge($this->defaultOptions, $options); 74 | } 75 | 76 | /** 77 | * @param ServerRequestInterface $request 78 | * @return ResponseInterface 79 | */ 80 | public function handle(ServerRequestInterface $request) 81 | //public function handle(Request $request, $type = HttpKernelInterface::MASTER_REQUEST, $catch = true) 82 | { 83 | // `Accept` header 84 | // Symfony version: 85 | // if (null !== $accept = $request->headers->get('Accept')) { 86 | // PSR-7 version: 87 | if (null !== $accept = $request->getHeader('Accept')) { 88 | $priorities = $this->formatNegotiator->normalizePriorities($this->options['format_priorities']); 89 | $accept = $this->formatNegotiator->getBest($accept, $priorities); 90 | 91 | // Symfony version: 92 | //$request->attributes->set('_accept', $accept); 93 | // PSR-7 version: 94 | $request = $request->withAttribute('_accept', $accept); 95 | 96 | if (null !== $accept && !$accept->isMediaRange()) { 97 | // Symfony version: 98 | //$request->attributes->set('_mime_type', $accept->getValue()); 99 | //$request->attributes->set('_format', $this->formatNegotiator->getFormat($accept->getValue())); 100 | // PSR-7 version: 101 | $request = $request 102 | ->withAttribute('_mime_type', $accept->getValue()) 103 | ->withAttribute('_format', $this->formatNegotiator->getFormat($accept->getValue())); 104 | } 105 | } 106 | 107 | // `Accept-Language` header 108 | // Symfony version: 109 | // if (null !== $accept = $request->headers->get('Accept-Language')) { 110 | if (null !== $accept = $request->getHeader('Accept-Language')) { 111 | $accept = $this->languageNegotiator->getBest($accept, $this->options['language_priorities']); 112 | // Symfony version: 113 | //$request->attributes->set('_accept_language', $accept); 114 | // PSR-7 version: 115 | $request = $request->withAttribute('_accept_language', $accept); 116 | 117 | if (null !== $accept) { 118 | // Symfony version: 119 | // $request->attributes->set('_language', $accept->getValue()); 120 | // PSR-7 version: 121 | $request = $request->withAttribute('_language', $accept->getValue()); 122 | } 123 | } 124 | 125 | // Symfony version: 126 | /* 127 | try { 128 | // `Content-Type` header 129 | $this->decodeBody($request); 130 | } catch (BadRequestHttpException $e) { 131 | if (true === $catch) { 132 | return new Response($e->getMessage(), Response::HTTP_BAD_REQUEST); 133 | } 134 | } 135 | */ 136 | 137 | // PSR-7 version: 138 | $ret = $this->decodeBody($request); 139 | if (is_string($ret)) { 140 | return new Response(new StringStream($ret), 400); 141 | } 142 | else if ($ret instanceof ServerRequestInterface) { 143 | return $this->app->handle($ret); 144 | } 145 | else { 146 | return $this->app->handle($request); 147 | } 148 | } 149 | 150 | // Changed the type hint. 151 | // I'll be honest I don't entirely understand what this method is supposed to do. :-) 152 | private function decodeBody(ServerRequestInterface $request) 153 | { 154 | // This line doesn't change, neat. :-) 155 | if (in_array($request->getMethod(), [ 'POST', 'PUT', 'PATCH', 'DELETE' ])) { 156 | // Symfony version: 157 | // $contentType = $request->headers->get('Content-Type'); 158 | // PSR-7 version: 159 | $contentType = $request->getHeader('Content-Type'); 160 | $format = $this->formatNegotiator->getFormat($contentType); 161 | 162 | if (!$this->decoderProvider->supports($format)) { 163 | return; 164 | } 165 | 166 | $decoder = $this->decoderProvider->getDecoder($format); 167 | // Symfony version: 168 | // $content = $request->getContent(); 169 | // PSR-7 version: (Note that we need the whole body string anyway in order to determine its mime type this way. 170 | $content = $request->getBody()->getContents(); 171 | 172 | // PSR-7: Needed to add the second empty() call to ensure we don't 173 | // conflict with another middleware that wants to parse the body 174 | // before we get here. 175 | if (!empty($content) && empty($request->getBodyParams())) { 176 | try { 177 | $data = $decoder->decode($content, $format); 178 | } catch (\Exception $e) { 179 | $data = null; 180 | } 181 | 182 | if (is_array($data)) { 183 | // Symfony version: 184 | // $request->request->replace($data); 185 | // PSR-7 version, I think: 186 | $request = $request->withBodyParams($data); 187 | } else { 188 | return 'Invalid ' . $format . ' message received'; 189 | } 190 | 191 | return $request; 192 | } 193 | } 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /src/NotFoundError.php: -------------------------------------------------------------------------------- 1 | inner = $inner; 27 | $this->router = $router; 28 | } 29 | 30 | public function handle(ServerRequestInterface $request) 31 | { 32 | $path = $request->getUri()->getPath(); 33 | 34 | $route = $this->router->match($path, $request->getServerParams()); 35 | 36 | return $route 37 | ? $this->delegate($request, $route) 38 | : $this->handleFailure($request, $this->router->getFailedRoute()); 39 | } 40 | 41 | /** 42 | * @param RequestInterface $request 43 | * @param Route $route 44 | * @return ResponseInterface 45 | */ 46 | protected function delegate(ServerRequestInterface $request, Route $route) 47 | { 48 | // We can't use setAttributes here, because there MAY already be attributes set. 49 | foreach ($route->params as $k => $v) { 50 | // Honestly this feels silly. 51 | $request = $request->withAttribute($k, $v); 52 | } 53 | return $this->inner->handle($request); 54 | } 55 | 56 | /** 57 | * @param RequestInterface $request 58 | * @param Route $failure 59 | * @return ResponseInterface 60 | */ 61 | protected function handleFailure(RequestInterface $request, Route $failure) 62 | { 63 | // inspect the failed route 64 | if ($failure->failedMethod()) { 65 | // the route failed on the allowed HTTP methods. 66 | // this is a "405 Method Not Allowed" error. 67 | $response = (new Response(new StringStream('405 Method Not Allowed'))) 68 | ->withStatus(405); 69 | return $response; 70 | 71 | } elseif ($failure->failedAccept()) { 72 | // the route failed on the available content-types. 73 | // this is a "406 Not Acceptable" error. 74 | $response = (new Response(new StringStream('406 Not Acceptable'))) 75 | ->withStatus(406); 76 | return $response; 77 | } else { 78 | // there was some other unknown matching problem. 79 | 80 | // I'm going to assume it's a 404 for now, just for kicks. 81 | $response = (new Response(new StringStream('404 Not Found'))) 82 | ->withStatus(404); 83 | return $response; 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/StringStream.php: -------------------------------------------------------------------------------- 1 | resource, $string); 25 | fseek($this->resource, 0); 26 | 27 | // This is for debugging. 28 | $this->string = $string; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/StringValue.php: -------------------------------------------------------------------------------- 1 | string = $string; 21 | $this->code = $code; 22 | } 23 | 24 | public function code() 25 | { 26 | return $this->code; 27 | } 28 | 29 | public function __toString() 30 | { 31 | return $this->string; 32 | } 33 | } --------------------------------------------------------------------------------