├── .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 | 2 | Options -MultiViews 3 | RewriteEngine On 4 | RewriteBase /~crell/stacker/ 5 | RewriteCond %{REQUEST_FILENAME} !-f 6 | RewriteRule ^ index.php [L] 7 | 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | A simple proof of concept of a StackPHP-style implementation of the 2 | PSR-7 "immutable" variant. 3 | 4 | To download it, git clone and run composer install to grab dependencies. 5 | 6 | To use, edit the .htaccess file and index.php to change your base path as appropriate, 7 | then try hitting these paths: 8 | 9 | * /bye 10 | * /hello/YourName 11 | * /goodbye/YourName 12 | * /forbidden 13 | 14 | Give feedback on the FIG mailing list. It's not impossible this will turn into 15 | a for-reals codebase later but at the moment it's just one-day a proof of concept. 16 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "require": { 3 | "phly/http": "0.9.1", 4 | "crell/transformer": "dev-master", 5 | "aura/router": "2.2", 6 | "willdurand/stack-negotiation": "~1.0@dev" 7 | }, 8 | "autoload": { 9 | "psr-4": { 10 | "Crell\\Stacker\\": "src/" 11 | } 12 | }, 13 | "minimum-stability": "dev" 14 | } 15 | -------------------------------------------------------------------------------- /composer.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_readme": [ 3 | "This file locks the dependencies of your project to a known state", 4 | "Read more about it at http://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", 5 | "This file is @generated automatically" 6 | ], 7 | "hash": "16b3e78a3931415367a85f8103c591ff", 8 | "packages": [ 9 | { 10 | "name": "aura/router", 11 | "version": "2.2.0", 12 | "source": { 13 | "type": "git", 14 | "url": "https://github.com/auraphp/Aura.Router.git", 15 | "reference": "2544c687a5074ca8ce0dbe6d27a04c006a118e2a" 16 | }, 17 | "dist": { 18 | "type": "zip", 19 | "url": "https://api.github.com/repos/auraphp/Aura.Router/zipball/2544c687a5074ca8ce0dbe6d27a04c006a118e2a", 20 | "reference": "2544c687a5074ca8ce0dbe6d27a04c006a118e2a", 21 | "shasum": "" 22 | }, 23 | "require": { 24 | "php": ">=5.3.0" 25 | }, 26 | "type": "library", 27 | "extra": { 28 | "aura": { 29 | "type": "library", 30 | "config": { 31 | "common": "Aura\\Router\\_Config\\Common" 32 | } 33 | }, 34 | "branch-alias": { 35 | "dev-develop-2": "2.0.x-dev" 36 | } 37 | }, 38 | "autoload": { 39 | "psr-4": { 40 | "Aura\\Router\\": "src/", 41 | "Aura\\Router\\_Config\\": "config/" 42 | } 43 | }, 44 | "notification-url": "https://packagist.org/downloads/", 45 | "license": [ 46 | "BSD-2-Clause" 47 | ], 48 | "authors": [ 49 | { 50 | "name": "Aura.Router Contributors", 51 | "homepage": "https://github.com/auraphp/Aura.Router/contributors" 52 | } 53 | ], 54 | "description": "A web router implementation; given a URI path and a copy of $_SERVER, it will extract path-info parameter values for a specific route.", 55 | "homepage": "https://github.com/auraphp/Aura.Router", 56 | "keywords": [ 57 | "route", 58 | "router", 59 | "routing" 60 | ], 61 | "time": "2014-11-10 15:51:58" 62 | }, 63 | { 64 | "name": "crell/transformer", 65 | "version": "dev-master", 66 | "source": { 67 | "type": "git", 68 | "url": "https://github.com/Crell/Transformer.git", 69 | "reference": "47cbe3aa872091a1afe5557499468d1a2119e05b" 70 | }, 71 | "dist": { 72 | "type": "zip", 73 | "url": "https://api.github.com/repos/Crell/Transformer/zipball/47cbe3aa872091a1afe5557499468d1a2119e05b", 74 | "reference": "47cbe3aa872091a1afe5557499468d1a2119e05b", 75 | "shasum": "" 76 | }, 77 | "require-dev": { 78 | "codeclimate/php-test-reporter": "dev-master", 79 | "phpunit/phpunit": "~4.4" 80 | }, 81 | "type": "library", 82 | "autoload": { 83 | "psr-4": { 84 | "Crell\\Transformer\\": "src/", 85 | "Crell\\Transformer\\Tests\\": "tests/" 86 | } 87 | }, 88 | "notification-url": "https://packagist.org/downloads/", 89 | "license": [ 90 | "MIT" 91 | ], 92 | "authors": [ 93 | { 94 | "name": "Larry Garfield", 95 | "email": "larry@garfieldtech.com" 96 | } 97 | ], 98 | "description": "A simple object transformation library", 99 | "time": "2015-01-22 21:09:50" 100 | }, 101 | { 102 | "name": "phly/http", 103 | "version": "0.9.1", 104 | "source": { 105 | "type": "git", 106 | "url": "https://github.com/phly/http.git", 107 | "reference": "e3033b2ceae4c7b560d85540f8e4251c9b7aa2dd" 108 | }, 109 | "dist": { 110 | "type": "zip", 111 | "url": "https://api.github.com/repos/phly/http/zipball/e3033b2ceae4c7b560d85540f8e4251c9b7aa2dd", 112 | "reference": "e3033b2ceae4c7b560d85540f8e4251c9b7aa2dd", 113 | "shasum": "" 114 | }, 115 | "require": { 116 | "php": ">=5.4.8", 117 | "psr/http-message": "~0.6.0" 118 | }, 119 | "require-dev": { 120 | "phpunit/phpunit": "3.7.*", 121 | "squizlabs/php_codesniffer": "1.5.*" 122 | }, 123 | "type": "library", 124 | "extra": { 125 | "branch-alias": { 126 | "dev-master": "1.0-dev", 127 | "dev-develop": "1.1-dev" 128 | } 129 | }, 130 | "autoload": { 131 | "psr-4": { 132 | "Phly\\Http\\": "src/" 133 | } 134 | }, 135 | "notification-url": "https://packagist.org/downloads/", 136 | "license": [ 137 | "BSD-2-Clause" 138 | ], 139 | "authors": [ 140 | { 141 | "name": "Matthew Weier O'Phinney", 142 | "email": "matthew@weierophinney.net", 143 | "homepage": "http://mwop.net" 144 | } 145 | ], 146 | "description": "PSR HTTP Message implementations", 147 | "homepage": "https://github.com/phly/http", 148 | "keywords": [ 149 | "http" 150 | ], 151 | "time": "2015-01-27 21:53:37" 152 | }, 153 | { 154 | "name": "psr/http-message", 155 | "version": "0.6.0", 156 | "source": { 157 | "type": "git", 158 | "url": "https://github.com/php-fig/http-message.git", 159 | "reference": "70d7c442866f109cbda7f9dea8938e47fc3cc20c" 160 | }, 161 | "dist": { 162 | "type": "zip", 163 | "url": "https://api.github.com/repos/php-fig/http-message/zipball/70d7c442866f109cbda7f9dea8938e47fc3cc20c", 164 | "reference": "70d7c442866f109cbda7f9dea8938e47fc3cc20c", 165 | "shasum": "" 166 | }, 167 | "type": "library", 168 | "extra": { 169 | "branch-alias": { 170 | "dev-master": "1.0.x-dev" 171 | } 172 | }, 173 | "autoload": { 174 | "psr-4": { 175 | "Psr\\Http\\Message\\": "src" 176 | } 177 | }, 178 | "notification-url": "https://packagist.org/downloads/", 179 | "license": [ 180 | "MIT" 181 | ], 182 | "authors": [ 183 | { 184 | "name": "PHP-FIG", 185 | "homepage": "http://www.php-fig.org/" 186 | } 187 | ], 188 | "description": "Common interface for HTTP messages", 189 | "keywords": [ 190 | "http", 191 | "http-message", 192 | "psr", 193 | "psr-7", 194 | "request", 195 | "response" 196 | ], 197 | "time": "2015-01-18 23:26:37" 198 | }, 199 | { 200 | "name": "symfony/serializer", 201 | "version": "2.7.x-dev", 202 | "target-dir": "Symfony/Component/Serializer", 203 | "source": { 204 | "type": "git", 205 | "url": "https://github.com/symfony/Serializer.git", 206 | "reference": "47849771bb13837bdf384481ff75d996c6f0cf20" 207 | }, 208 | "dist": { 209 | "type": "zip", 210 | "url": "https://api.github.com/repos/symfony/Serializer/zipball/47849771bb13837bdf384481ff75d996c6f0cf20", 211 | "reference": "47849771bb13837bdf384481ff75d996c6f0cf20", 212 | "shasum": "" 213 | }, 214 | "require": { 215 | "php": ">=5.3.9" 216 | }, 217 | "require-dev": { 218 | "doctrine/annotations": "~1.0", 219 | "doctrine/cache": "~1.0", 220 | "symfony/config": "~2.2|~3.0.0", 221 | "symfony/yaml": "~2.0|~3.0.0" 222 | }, 223 | "suggest": { 224 | "doctrine/annotations": "For using the annotation mapping. You will also need doctrine/cache.", 225 | "doctrine/cache": "For using the default cached annotation reader and metadata cache.", 226 | "symfony/config": "For using the XML mapping loader.", 227 | "symfony/yaml": "For using the default YAML mapping loader." 228 | }, 229 | "type": "library", 230 | "extra": { 231 | "branch-alias": { 232 | "dev-master": "2.7-dev" 233 | } 234 | }, 235 | "autoload": { 236 | "psr-0": { 237 | "Symfony\\Component\\Serializer\\": "" 238 | } 239 | }, 240 | "notification-url": "https://packagist.org/downloads/", 241 | "license": [ 242 | "MIT" 243 | ], 244 | "authors": [ 245 | { 246 | "name": "Symfony Community", 247 | "homepage": "http://symfony.com/contributors" 248 | }, 249 | { 250 | "name": "Fabien Potencier", 251 | "email": "fabien@symfony.com" 252 | } 253 | ], 254 | "description": "Symfony Serializer Component", 255 | "homepage": "http://symfony.com", 256 | "time": "2015-01-28 07:13:07" 257 | }, 258 | { 259 | "name": "willdurand/negotiation", 260 | "version": "dev-master", 261 | "source": { 262 | "type": "git", 263 | "url": "https://github.com/willdurand/Negotiation.git", 264 | "reference": "17b357c97175840200699012c8c536b4f6d43034" 265 | }, 266 | "dist": { 267 | "type": "zip", 268 | "url": "https://api.github.com/repos/willdurand/Negotiation/zipball/17b357c97175840200699012c8c536b4f6d43034", 269 | "reference": "17b357c97175840200699012c8c536b4f6d43034", 270 | "shasum": "" 271 | }, 272 | "require": { 273 | "php": ">=5.3.0" 274 | }, 275 | "type": "library", 276 | "extra": { 277 | "branch-alias": { 278 | "dev-master": "1.3-dev" 279 | } 280 | }, 281 | "autoload": { 282 | "psr-0": { 283 | "Negotiation": "src/" 284 | } 285 | }, 286 | "notification-url": "https://packagist.org/downloads/", 287 | "license": [ 288 | "MIT" 289 | ], 290 | "authors": [ 291 | { 292 | "name": "William Durand", 293 | "email": "william.durand1@gmail.com" 294 | } 295 | ], 296 | "description": "Content Negotiation tools for PHP provided as a standalone library.", 297 | "homepage": "http://williamdurand.fr/Negotiation/", 298 | "keywords": [ 299 | "accept", 300 | "content", 301 | "format", 302 | "header", 303 | "negotiation" 304 | ], 305 | "time": "2014-10-29 23:21:49" 306 | }, 307 | { 308 | "name": "willdurand/stack-negotiation", 309 | "version": "dev-master", 310 | "source": { 311 | "type": "git", 312 | "url": "https://github.com/willdurand/StackNegotiation.git", 313 | "reference": "d3e185e7de7302fdad6230973f408a8795ab86bd" 314 | }, 315 | "dist": { 316 | "type": "zip", 317 | "url": "https://api.github.com/repos/willdurand/StackNegotiation/zipball/d3e185e7de7302fdad6230973f408a8795ab86bd", 318 | "reference": "d3e185e7de7302fdad6230973f408a8795ab86bd", 319 | "shasum": "" 320 | }, 321 | "require": { 322 | "php": ">=5.4.0", 323 | "symfony/serializer": "~2.1", 324 | "willdurand/negotiation": "~1.3" 325 | }, 326 | "require-dev": { 327 | "phpunit/phpunit": "~3.7", 328 | "symfony/http-kernel": "~2.1" 329 | }, 330 | "type": "library", 331 | "extra": { 332 | "branch-alias": { 333 | "dev-master": "1.0-dev" 334 | } 335 | }, 336 | "autoload": { 337 | "psr-0": { 338 | "Negotiation": "src/" 339 | } 340 | }, 341 | "notification-url": "https://packagist.org/downloads/", 342 | "license": [ 343 | "MIT" 344 | ], 345 | "authors": [ 346 | { 347 | "name": "William Durand", 348 | "email": "william.durand1@gmail.com" 349 | } 350 | ], 351 | "description": "Stack middleware for content negotiation.", 352 | "time": "2014-10-28 15:35:05" 353 | } 354 | ], 355 | "packages-dev": [], 356 | "aliases": [], 357 | "minimum-stability": "dev", 358 | "stability-flags": { 359 | "crell/transformer": 20, 360 | "willdurand/stack-negotiation": 20 361 | }, 362 | "prefer-stable": false, 363 | "prefer-lowest": false, 364 | "platform": [], 365 | "platform-dev": [] 366 | } 367 | -------------------------------------------------------------------------------- /index.php: -------------------------------------------------------------------------------- 1 | setTransformer(StringValue::class, function (StringValue $string) { 48 | return new Response(new StringStream($string)); 49 | }); 50 | $bus->setTransformer(Stream::class, function(Stream $stream) { 51 | return new Response($stream); 52 | }); 53 | $bus->setTransformer(NotFoundError::class, function(NotFoundError $error) { 54 | return (new Response(new StringStream($error)))->withStatus(404); 55 | }); 56 | $bus->setTransformer(ForbiddenError::class, function(ForbiddenError $error) { 57 | return (new Response(new StringStream($error)))->withStatus(403); 58 | }); 59 | $kernel = new DispatchingMiddleware($bus); 60 | 61 | // A routing-based middleware; doesn't actually do anything but the routing resolution. 62 | $router_factory = new RouterFactory; 63 | /** @var Router $router */ 64 | $router = $router_factory->newInstance(); 65 | 66 | $router->add('hello', '/hello/{name}') 67 | ->addValues(array( 68 | 'action' => function(RequestInterface $request, $name) { 69 | return new Response(new StringStream("Hello {$name}")); 70 | }, 71 | )); 72 | $router->add('goodbye', '/goodbye/{name}') 73 | ->addValues(array( 74 | 'action' => function(RequestInterface $request, $name) { 75 | return "Goodbye {$name}"; 76 | }, 77 | )); 78 | $router->add('forbidden', '/forbidden') 79 | ->addValues(array( 80 | 'action' => function() { 81 | return new ForbiddenError(); 82 | }, 83 | )); 84 | $router->addPost('jsonecho', '/jsonecho') 85 | ->addValues(array( 86 | 'action' => function(ServerRequestInterface $request) { 87 | // Just echo the JSON back. 88 | $params = $request->getBodyParams(); 89 | $encoded = json_encode($params); 90 | $response = (new Response(new StringStream($encoded))) 91 | ->withHeader('content-type', 'application/json'); 92 | return $response; 93 | }, 94 | )); 95 | $router->addPost('xmlecho', '/xmlecho') 96 | ->addValues(array( 97 | 'action' => function(ServerRequestInterface $request) { 98 | /** @var MyDomainObject $my */ 99 | $params = $request->getBodyParams(); 100 | $my = $params['data']; 101 | $name = $my->getName(); 102 | $result = "Hello {$name}\n"; 103 | $response = (new Response(new StringStream($result))) 104 | ->withHeader('content-type', 'application/xml'); 105 | return $response; 106 | }, 107 | )); 108 | $kernel = new RoutingMiddleware($kernel, $router); 109 | 110 | // Setup pre-routing event listeners. Remember, order in this file is backwards. 111 | // Putting request and response listeners in the same middleware may not be the 112 | // best idea, but this is just a demo. 113 | $kernel = new EventMiddleware($kernel); 114 | $kernel->addRequestListener(function(ServerRequestInterface $request) { 115 | return $request->withAttribute('some_silliness', 'myvalue'); 116 | }); 117 | $kernel->addResponseListener(function(ServerRequestInterface $request, ResponseInterface $response) { 118 | // This only works with the convention of NegotiationMiddleware. 119 | if ($request->getAttribute('_format') == 'html') { 120 | $content = $response->getBody()->getContents(); 121 | $content .= "

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 | } --------------------------------------------------------------------------------