├── .gitignore ├── .travis.yml ├── README.md ├── TODO.md ├── bin └── server.php ├── composer.json ├── phpunit.xml ├── src ├── Application.php ├── AsyncMessageInterface.php ├── AsyncMessageTrait.php ├── BufferedStream.php ├── Container │ └── ApplicationFactory.php ├── DeferredResponse.php ├── Emitter │ └── AsyncEmitter.php ├── ExpressiveConnectionHandler.php ├── MessageTrait.php ├── PromiseResponseInterface.php ├── RequestParser.php ├── Response.php ├── Server.php └── ServerRequest.php └── tests ├── ApplicationTest.php ├── CallableStub.php ├── ConnectionStub.php ├── RequestParserTest.php ├── ServerStub.php ├── ServerTest.php ├── TestCase.php └── bootstrap.php /.gitignore: -------------------------------------------------------------------------------- 1 | vendor/ 2 | composer.lock 3 | 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | php: 4 | - 5.4 5 | - 5.5 6 | - 5.6 7 | - 7.0 8 | - hhvm 9 | 10 | matrix: 11 | allow_failures: 12 | - php: 5.4 13 | - php: 5.5 14 | - php: hhvm 15 | 16 | before_install: 17 | - composer self-update 18 | 19 | install: 20 | - COMPOSER_ROOT_VERSION=0.4.1 travis_retry composer install --no-interaction --ignore-platform-reqs --prefer-source 21 | - composer info -i 22 | 23 | script: 24 | - ./vendor/bin/phpunit 25 | 26 | notifications: 27 | email: true 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/steverhoades/expressive-async.svg?branch=master)](https://travis-ci.org/steverhoades/expressive-async) 2 | 3 | # Overview 4 | This library was created to experiment with zend expressive using React PHP and is based on React\Http. Mileage may vary. 5 | 6 | A large portion of this code leverages existing libraries such as guzzlehttp's psr7 library, zend diactoros, zend expressive, zend stratigility and zend service manager. 7 | 8 | ## Requirements 9 | * PHP 5.6+ 10 | 11 | ## Installation 12 | $ composer install 13 | 14 | ## Usage 15 | This library alters the functionality of Zend\Expressive\Application slightly to allow for deferred processing from middleware. If a middleware application returns an instance of DeferredResponse the application will wait to emit until the promise has been resolved. DeferredResponse must be created with a React\Promise\Promise object. 16 | 17 | Example: 18 | ```php 19 | function($request, $response) use ($eventLoop) { 20 | // create a request, wait 1-5 seconds and then return a response. 21 | $deferred = new Deferred(); 22 | $eventLoop->addTimer(rand(1, 5), function() use ($deferred){ 23 | echo 'Timer executed' . PHP_EOL; 24 | $deferred->resolve(new Diactoros\Response\HtmlResponse('Deferred response.')); 25 | }); 26 | 27 | return new \ExpressiveAsync\DeferredResponse($deferred->promise()); 28 | } 29 | ``` 30 | 31 | ## Example 32 | 33 | ```php 34 | $serviceManager = new \Zend\ServiceManager\ServiceManager(); 35 | $eventLoop = Factory::create(); 36 | $socketServer = new SocketServer($eventLoop); 37 | $httpServer = new Server($socketServer); 38 | 39 | $serviceManager->setFactory('EventLoop',function() use ($eventLoop) { return $eventLoop; }); 40 | $serviceManager->setInvokableClass( 41 | 'Zend\Expressive\Router\RouterInterface', 42 | 'Zend\Expressive\Router\FastRouteRouter' 43 | ); 44 | 45 | $router = new \Zend\Expressive\Router\FastRouteRouter(); 46 | 47 | // Example of a regular request 48 | $router->addRoute(new \Zend\Expressive\Router\Route( 49 | '/', 50 | function($request, $response) use ($eventLoop) { 51 | return new Diactoros\Response\HtmlResponse('Hello World.'); 52 | }, 53 | ['GET'], 54 | 'home' 55 | )); 56 | 57 | // Example of a deferred request 58 | $router->addRoute(new \Zend\Expressive\Router\Route( 59 | '/deferred', 60 | function($request, $response) use ($eventLoop) { 61 | // create a request, wait 1-5 seconds and then return a response. 62 | $deferred = new Deferred(); 63 | $eventLoop->addTimer(rand(1, 5), function() use ($deferred){ 64 | echo 'Timer executed' . PHP_EOL; 65 | $deferred->resolve(new Diactoros\Response\HtmlResponse('Deferred response.')); 66 | }); 67 | 68 | return new \ExpressiveAsync\DeferredResponse($deferred->promise()); 69 | }, 70 | ['GET'], 71 | 'deferred' 72 | )); 73 | 74 | $application = new Application( 75 | $router, 76 | $serviceManager, 77 | function($request, $response) { 78 | echo 'final handler was called.' . PHP_EOL; 79 | return new Diactoros\Response\HtmlResponse('Not Found.', 404); 80 | } 81 | ); 82 | $connectionHandler = new ExpressiveAsync\ExpressiveConnectionHandler($application); 83 | $httpServer->on('request', $connectionHandler); 84 | $socketServer->listen('10091'); 85 | $eventLoop->run(); 86 | ``` 87 | ## Connection Events 88 | The ExpressiveConnectionHandler emits several events during the lifecycle of a request. 89 | 90 | #### ExpressiveConnectionHandler::EVENT_REQUEST [$connection, &$request, &$response] 91 | Before the middleware application is executed this event will be emitted. The request and response objects are passed in by reference allowing for modification. 92 | 93 | ```php 94 | $connectionHandler = new ExpressiveAsync\ExpressiveConnectionHandler($application); 95 | $connectionHandler->on(ExpressiveConnectionHandler::EVENT_REQUEST, function ($conn, &$request, $response) { 96 | $request = $request->withAttribute('request-start', microtime(true)) 97 | }); 98 | ``` 99 | 100 | #### ExpressiveConnectionHandler::EVENT_END [$connection, $request, &$response] 101 | Before the response is written to the connection but after the middleware has executed. The response object is passed by reference. 102 | ```php 103 | $connectionHandler->on('end', function($conn, $request, $response) use ($logger) { 104 | $logger->timing('app.timing', (microtime(true) - $request->getAttribute('request-start')) * 1000); 105 | }); 106 | ``` 107 | 108 | #### ExpressiveConnectionHandler::EVENT_CONNECTION_END [$connection] 109 | Emitted once the connection has been ended. 110 | 111 | #### ExpressiveConnectionHandler::EVENT_CONNECTION_CLOSE [$connection] 112 | Emitted once the connection close has been called. 113 | 114 | ## Run the Example 115 | An example server has been provided. To run simply execute the following in the terminal from the root directory. 116 | 117 | ``` 118 | $ php bin/server.php 119 | ``` 120 | 121 | Open your browser to http://127.0.0.1:10091 122 | 123 | ## More Information 124 | For additional information on how to get started see the [zend-expressive](https://github.com/zendframework/zend-expressive) github page. 125 | 126 | ## Notes 127 | Currently this example contains BufferedStream, which isn't using streams at all. The Response and Request objects created by zend and guzzle all use php://memory or php://temp, I am currently unsure as to the impacts this will have in an environment that we wish to stay non-blocking. 128 | 129 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | # TODO 2 | - [x] test a request with POST request and parsed data 3 | - [ ] test a request with FILE Upload 4 | - [ ] add a request which performs a MySQL query in a non-blocking way, or performs some other non-blocking operation 5 | - [x] unit tests 6 | -------------------------------------------------------------------------------- /bin/server.php: -------------------------------------------------------------------------------- 1 | setFactory('EventLoop',function() use ($eventLoop) { return $eventLoop; }); 18 | $serviceManager->setInvokableClass( 19 | 'Zend\Expressive\Router\RouterInterface', 20 | 'Zend\Expressive\Router\FastRouteRouter' 21 | ); 22 | 23 | $router = new \Zend\Expressive\Router\FastRouteRouter(); 24 | 25 | $router->addRoute(new \Zend\Expressive\Router\Route( 26 | '/', 27 | function($request, $response) use ($eventLoop) { 28 | echo 'Home executed' . PHP_EOL; 29 | return new Diactoros\Response\HtmlResponse('Hello World.'); 30 | }, 31 | ['GET'], 32 | 'home' 33 | )); 34 | 35 | 36 | $router->addRoute(new \Zend\Expressive\Router\Route( 37 | '/deferred', 38 | function($request, $response) use ($eventLoop) { 39 | // create a request, wait 1-5 seconds and then return a response. 40 | $deferred = new Deferred(); 41 | $eventLoop->addTimer(rand(1, 5), function() use ($deferred){ 42 | echo 'Timer executed' . PHP_EOL; 43 | $deferred->resolve(new Diactoros\Response\HtmlResponse('Deferred response.')); 44 | }); 45 | 46 | return new \ExpressiveAsync\DeferredResponse($deferred->promise()); 47 | }, 48 | ['GET'], 49 | 'deferred' 50 | )); 51 | 52 | $router->addRoute(new \Zend\Expressive\Router\Route( 53 | '/form', 54 | function($request, $response) use ($eventLoop) { 55 | echo "Form post received" . PHP_EOL; 56 | 57 | $params = $request->getParsedBody(); 58 | return new Diactoros\Response\HtmlResponse("Hi {$params['name']}"); 59 | }, 60 | ['POST'], 61 | 'form' 62 | )); 63 | 64 | $application = new Application( 65 | $router, 66 | $serviceManager, 67 | function($request, $response) { 68 | echo 'final handler was called.' . PHP_EOL; 69 | return new Diactoros\Response\HtmlResponse('Not Found.', 404); 70 | } 71 | ); 72 | 73 | $connectionHandler = new ExpressiveAsync\ExpressiveConnectionHandler($application); 74 | $httpServer->on('request', $connectionHandler); 75 | $socketServer->listen('10091'); 76 | $eventLoop->run(); 77 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "steverhoades/expressive-async", 3 | "description": "An asynchronous middleware server build using React PHP and Zend Expressive", 4 | "license": "MIT", 5 | "require": { 6 | "zendframework/zend-diactoros": "~1.1", 7 | "zendframework/zend-expressive": "1.0.0-RC2", 8 | "zendframework/zend-servicemanager": "^2.6", 9 | "zendframework/zend-expressive-fastroute": "^0.2", 10 | "react/event-loop": "0.4.*", 11 | "react/http": "dev-master as 0.4.1", 12 | "react/promise": "^2.2" 13 | }, 14 | "require-dev": { 15 | "phpunit/phpunit": "^4.7" 16 | }, 17 | "autoload": { 18 | "psr-4": { 19 | "ExpressiveAsync\\": "src" 20 | } 21 | }, 22 | "autoload-dev": { 23 | "psr-4": { 24 | "ExpressiveAsync\\Test\\": "tests/" 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 14 | 15 | 16 | ./tests/ 17 | 18 | 19 | 20 | 21 | 22 | ./src/ 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/Application.php: -------------------------------------------------------------------------------- 1 | emitter = new AsyncEmitter($connection);; 38 | 39 | return $newapp; 40 | } 41 | 42 | /** 43 | * Overload the parent as we need the emitter on this application and not the parents. 44 | * 45 | * @return mixed 46 | */ 47 | public function getEmitter() 48 | { 49 | return $this->emitter; 50 | } 51 | 52 | /** 53 | * Run the application 54 | * 55 | * If no request or response are provided, the method will use 56 | * ServerRequestFactory::fromGlobals to create a request instance, and 57 | * instantiate a default response instance. 58 | * 59 | * It then will invoke itself with the request and response, and emit 60 | * the returned response using the composed emitter. 61 | * 62 | * @param null|ServerRequestInterface $request 63 | * @param null|ResponseInterface $response 64 | */ 65 | public function run(ServerRequestInterface $request = null, ResponseInterface $response = null) 66 | { 67 | $request = $request ?: new ServerRequest(); 68 | $response = $response ?: new Response(); 69 | 70 | $response = $this($request, $response); 71 | 72 | /** 73 | * If a deferred was returned, than wait for it to be done and then emit. 74 | */ 75 | if ($response instanceof PromiseResponseInterface) { 76 | $response->promise()->done(function(ResponseInterface $response) use ($request) { 77 | $this->emit('end', [$request, &$response]); 78 | 79 | $emitter = $this->getEmitter(); 80 | $emitter->emit($response); 81 | }); 82 | 83 | return; 84 | } 85 | 86 | $this->emit('end', [$request, $response]); 87 | 88 | $emitter = $this->getEmitter(); 89 | $emitter->emit($response); 90 | } 91 | 92 | public function __clone() 93 | { 94 | $this->listeners = []; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/AsyncMessageInterface.php: -------------------------------------------------------------------------------- 1 | connection; 23 | } 24 | 25 | /** 26 | * @param ConnectionInterface $connection 27 | * @return ServerRequest 28 | */ 29 | public function withConnection(ConnectionInterface $connection) 30 | { 31 | $new = clone $this; 32 | $new->connection = $connection; 33 | 34 | return $new; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/BufferedStream.php: -------------------------------------------------------------------------------- 1 | getContents(); 29 | } 30 | 31 | public function getContents() 32 | { 33 | $buffer = $this->buffer; 34 | 35 | return $buffer; 36 | } 37 | 38 | public function close() 39 | { 40 | $this->buffer = ''; 41 | } 42 | 43 | public function detach() 44 | { 45 | $this->close(); 46 | } 47 | 48 | public function getSize() 49 | { 50 | return strlen($this->buffer); 51 | } 52 | 53 | public function isReadable() 54 | { 55 | return true; 56 | } 57 | 58 | public function isWritable() 59 | { 60 | return true; 61 | } 62 | 63 | public function isSeekable() 64 | { 65 | return false; 66 | } 67 | 68 | public function rewind() 69 | { 70 | $this->position = 0; 71 | } 72 | 73 | public function seek($offset, $whence = SEEK_SET) 74 | { 75 | throw new \RuntimeException('Cannot seek a BufferStream'); 76 | } 77 | 78 | public function eof() 79 | { 80 | return strlen($this->buffer) === 0; 81 | } 82 | 83 | public function tell() 84 | { 85 | throw new \RuntimeException('Cannot determine the position of a BufferStream'); 86 | } 87 | 88 | /** 89 | * Reads data from the buffer. 90 | */ 91 | public function read($length) 92 | { 93 | $currentLength = strlen($this->buffer); 94 | 95 | if ($length >= $currentLength) { 96 | // No need to slice the buffer because we don't have enough data. 97 | $result = $this->buffer; 98 | $this->position = strlen($this->buffer); 99 | } else { 100 | // Slice up the result to provide a subset of the buffer. 101 | $result = substr($this->buffer, $this->position, $length); 102 | $this->position = $this->position + $length; 103 | } 104 | 105 | return $result; 106 | } 107 | 108 | /** 109 | * Writes data to the buffer. 110 | */ 111 | public function write($string) 112 | { 113 | $this->buffer .= $string; 114 | 115 | return strlen($string); 116 | } 117 | 118 | public function getMetadata($key = null) 119 | { 120 | return $key ? null : []; 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/Container/ApplicationFactory.php: -------------------------------------------------------------------------------- 1 | has(RouterInterface::class) 36 | ? $container->get(RouterInterface::class) 37 | : new FastRouteRouter(); 38 | 39 | $finalHandler = $container->has('Zend\Expressive\FinalHandler') 40 | ? $container->get('Zend\Expressive\FinalHandler') 41 | : null; 42 | 43 | $emitter = $container->has(EmitterInterface::class) 44 | ? $container->get(EmitterInterface::class) 45 | : null; 46 | 47 | $app = new Application($router, $container, $finalHandler, $emitter); 48 | 49 | $this->injectPreMiddleware($app, $container); 50 | $this->injectRoutes($app, $container); 51 | $this->injectPostMiddleware($app, $container); 52 | 53 | return $app; 54 | } 55 | 56 | /** 57 | * Inject routes from configuration, if any. 58 | * 59 | * @param Application $app 60 | * @param ContainerInterface $container 61 | */ 62 | private function injectRoutes(Application $app, ContainerInterface $container) 63 | { 64 | $config = $container->has('config') ? $container->get('config') : []; 65 | if (! isset($config['routes'])) { 66 | $app->pipeRoutingMiddleware(); 67 | return; 68 | } 69 | 70 | foreach ($config['routes'] as $spec) { 71 | if (! isset($spec['path']) || ! isset($spec['middleware'])) { 72 | continue; 73 | } 74 | 75 | $methods = (isset($spec['allowed_methods']) && is_array($spec['allowed_methods'])) 76 | ? $spec['allowed_methods'] 77 | : null; 78 | $name = isset($spec['name']) ? $spec['name'] : null; 79 | $methods = (null === $methods) ? Route::HTTP_METHOD_ANY : $methods; 80 | $route = new Route($spec['path'], $spec['middleware'], $methods, $name); 81 | 82 | if (isset($spec['options']) && is_array($spec['options'])) { 83 | $route->setOptions($spec['options']); 84 | } 85 | 86 | $app->route($route); 87 | } 88 | } 89 | 90 | /** 91 | * Given a collection of middleware specifications, pipe them to the application. 92 | * 93 | * @param array $collection 94 | * @param Application $app 95 | * @param ContainerInterface $container 96 | * @throws Container\Exception\InvalidMiddlewareException for invalid middleware. 97 | */ 98 | private function injectMiddleware(array $collection, Application $app, ContainerInterface $container) 99 | { 100 | foreach ($collection as $spec) { 101 | if (! array_key_exists('middleware', $spec)) { 102 | continue; 103 | } 104 | 105 | $path = isset($spec['path']) ? $spec['path'] : '/'; 106 | $middleware = $spec['middleware']; 107 | $error = array_key_exists('error', $spec) ? (bool) $spec['error'] : false; 108 | $pipe = $error ? 'pipeErrorHandler' : 'pipe'; 109 | 110 | $app->{$pipe}($path, $middleware); 111 | } 112 | } 113 | 114 | /** 115 | * Inject middleware to pipe before the routing middleware. 116 | * 117 | * Pre-routing middleware is specified as the configuration subkey 118 | * middleware_pipeline.pre_routing. 119 | * 120 | * @param Application $app 121 | * @param ContainerInterface $container 122 | */ 123 | private function injectPreMiddleware(Application $app, ContainerInterface $container) 124 | { 125 | $config = $container->has('config') ? $container->get('config') : []; 126 | if (! isset($config['middleware_pipeline']['pre_routing']) || 127 | ! is_array($config['middleware_pipeline']['pre_routing']) 128 | ) { 129 | return; 130 | } 131 | 132 | $this->injectMiddleware($config['middleware_pipeline']['pre_routing'], $app, $container); 133 | } 134 | 135 | /** 136 | * Inject middleware to pipe after the routing middleware. 137 | * 138 | * Post-routing middleware is specified as the configuration subkey 139 | * middleware_pipeline.post_routing. 140 | * 141 | * @param Application $app 142 | * @param ContainerInterface $container 143 | */ 144 | private function injectPostMiddleware(Application $app, ContainerInterface $container) 145 | { 146 | $config = $container->has('config') ? $container->get('config') : []; 147 | if (! isset($config['middleware_pipeline']['post_routing']) || 148 | ! is_array($config['middleware_pipeline']['post_routing']) 149 | ) { 150 | return; 151 | } 152 | 153 | $this->injectMiddleware($config['middleware_pipeline']['post_routing'], $app, $container); 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /src/DeferredResponse.php: -------------------------------------------------------------------------------- 1 | promise = $promise; 38 | } 39 | 40 | /** 41 | * @return Promise 42 | */ 43 | public function promise() 44 | { 45 | return $this->promise; 46 | } 47 | 48 | /** 49 | * ResponseInterface stubs 50 | */ 51 | public function getProtocolVersion() 52 | { 53 | return '1.1'; 54 | } 55 | 56 | public function withProtocolVersion($version) 57 | { 58 | return $this; 59 | } 60 | 61 | public function getHeaders() 62 | { 63 | return []; 64 | } 65 | 66 | public function hasHeader($name) 67 | { 68 | return false; 69 | } 70 | 71 | public function getHeader($name) 72 | { 73 | return null; 74 | } 75 | 76 | public function getHeaderLine($name) 77 | { 78 | return null; 79 | } 80 | 81 | public function withHeader($name, $value) 82 | { 83 | return $this; 84 | } 85 | 86 | public function withAddedHeader($name, $value) 87 | { 88 | return $this; 89 | } 90 | 91 | public function withoutHeader($name) 92 | { 93 | return $this; 94 | } 95 | 96 | public function getBody() 97 | { 98 | return new BufferedStream(); 99 | } 100 | 101 | public function withBody(StreamInterface $body) 102 | { 103 | return $this; 104 | } 105 | 106 | public function getStatusCode() 107 | { 108 | return 200; 109 | } 110 | 111 | public function withStatus($code, $reasonPhrase = '') 112 | { 113 | return $this; 114 | } 115 | 116 | public function getReasonPhrase() 117 | { 118 | return ''; 119 | } 120 | 121 | } 122 | -------------------------------------------------------------------------------- /src/Emitter/AsyncEmitter.php: -------------------------------------------------------------------------------- 1 | conn = $conn; 14 | } 15 | 16 | /** 17 | * Emits a response for a PHP SAPI environment. 18 | * 19 | * Emits the status line and headers via the header() function, and the 20 | * body content via the output buffer. 21 | * 22 | * @param ResponseInterface $response 23 | * @param null|int $maxBufferLevel Maximum output buffering level to unwrap. 24 | */ 25 | public function emit(ResponseInterface $response, $maxBufferLevel = null) 26 | { 27 | $this->emitStatusLine($response); 28 | $this->emitHeaders($response); 29 | $this->emitBody($response, $maxBufferLevel); 30 | $this->conn->end(); 31 | } 32 | 33 | /** 34 | * Emit the status line. 35 | * 36 | * Emits the status line using the protocol version and status code from 37 | * the response; if a reason phrase is availble, it, too, is emitted. 38 | * 39 | * @param ResponseInterface $response 40 | */ 41 | private function emitStatusLine(ResponseInterface $response) 42 | { 43 | $reasonPhrase = $response->getReasonPhrase(); 44 | $header = sprintf( 45 | "HTTP/%s %d%s\r\n", 46 | $response->getProtocolVersion(), 47 | $response->getStatusCode(), 48 | ($reasonPhrase ? ' ' . $reasonPhrase : '') 49 | ); 50 | $this->conn->write($header); 51 | } 52 | 53 | /** 54 | * Emit response headers. 55 | * 56 | * Loops through each header, emitting each; if the header value 57 | * is an array with multiple values, ensures that each is sent 58 | * in such a way as to create aggregate headers (instead of replace 59 | * the previous). 60 | * 61 | * @param ResponseInterface $response 62 | */ 63 | private function emitHeaders(ResponseInterface $response) 64 | { 65 | foreach ($response->getHeaders() as $header => $values) { 66 | $name = $this->filterHeader($header); 67 | foreach ($values as $value) { 68 | $header = sprintf( 69 | "%s: %s\r\n", 70 | $name, 71 | $value 72 | ); 73 | $this->conn->write($header); 74 | } 75 | } 76 | } 77 | 78 | /** 79 | * Emit the message body. 80 | * 81 | * Loops through the output buffer, flushing each, before emitting 82 | * the response body using `echo()`. 83 | * 84 | * @param ResponseInterface $response 85 | * @param int $maxBufferLevel Flush up to this buffer level. 86 | */ 87 | private function emitBody(ResponseInterface $response, $maxBufferLevel) 88 | { 89 | $this->conn->write("\r\n"); 90 | $this->conn->write($response->getBody()); 91 | } 92 | 93 | /** 94 | * Filter a header name to wordcase 95 | * 96 | * @param string $header 97 | * @return string 98 | */ 99 | private function filterHeader($header) 100 | { 101 | $filtered = str_replace('-', ' ', $header); 102 | $filtered = ucwords($filtered); 103 | return str_replace(' ', '-', $filtered); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/ExpressiveConnectionHandler.php: -------------------------------------------------------------------------------- 1 | application = $application; 39 | } 40 | 41 | /** 42 | * @param ConnectionInterface $conn 43 | * @param ServerRequestInterface $request 44 | * @param ResponseInterface $response 45 | */ 46 | public function __invoke(ConnectionInterface $conn, ServerRequestInterface $request, ResponseInterface $response) 47 | { 48 | $this->emit('request', [$conn, &$request, &$response]); 49 | 50 | $application = $this->application->getApplicationForConnection($conn); 51 | $application->on('end', function($request, &$response) use ($conn){ 52 | $this->emit('end', [$conn, $request, &$response]); 53 | }); 54 | 55 | $conn->on('end', function() use ($conn) { 56 | $this->emit('connection.end', [$conn]); 57 | }); 58 | 59 | $conn->on('close', function() use ($conn) { 60 | $this->emit('connection.close', [$conn]); 61 | }); 62 | 63 | $application->run($request, $response); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/MessageTrait.php: -------------------------------------------------------------------------------- 1 | protocol; 26 | } 27 | 28 | public function withProtocolVersion($version) 29 | { 30 | if ($this->protocol === $version) { 31 | return $this; 32 | } 33 | 34 | $new = clone $this; 35 | $new->protocol = $version; 36 | return $new; 37 | } 38 | 39 | public function getHeaders() 40 | { 41 | return $this->headerLines; 42 | } 43 | 44 | public function hasHeader($header) 45 | { 46 | return isset($this->headers[strtolower($header)]); 47 | } 48 | 49 | public function getHeader($header) 50 | { 51 | $name = strtolower($header); 52 | return isset($this->headers[$name]) ? $this->headers[$name] : []; 53 | } 54 | 55 | public function getHeaderLine($header) 56 | { 57 | return implode(', ', $this->getHeader($header)); 58 | } 59 | 60 | public function withHeader($header, $value) 61 | { 62 | $new = clone $this; 63 | $header = trim($header); 64 | $name = strtolower($header); 65 | 66 | if (!is_array($value)) { 67 | $new->headers[$name] = [trim($value)]; 68 | } else { 69 | $new->headers[$name] = $value; 70 | foreach ($new->headers[$name] as &$v) { 71 | $v = trim($v); 72 | } 73 | } 74 | 75 | // Remove the header lines. 76 | foreach (array_keys($new->headerLines) as $key) { 77 | if (strtolower($key) === $name) { 78 | unset($new->headerLines[$key]); 79 | } 80 | } 81 | 82 | // Add the header line. 83 | $new->headerLines[$header] = $new->headers[$name]; 84 | 85 | return $new; 86 | } 87 | 88 | public function withAddedHeader($header, $value) 89 | { 90 | if (!$this->hasHeader($header)) { 91 | return $this->withHeader($header, $value); 92 | } 93 | 94 | $new = clone $this; 95 | $new->headers[strtolower($header)][] = $value; 96 | $new->headerLines[$header][] = $value; 97 | return $new; 98 | } 99 | 100 | public function withoutHeader($header) 101 | { 102 | if (!$this->hasHeader($header)) { 103 | return $this; 104 | } 105 | 106 | $new = clone $this; 107 | $name = strtolower($header); 108 | unset($new->headers[$name]); 109 | 110 | foreach (array_keys($new->headerLines) as $key) { 111 | if (strtolower($key) === $name) { 112 | unset($new->headerLines[$key]); 113 | } 114 | } 115 | 116 | return $new; 117 | } 118 | 119 | 120 | /** 121 | * @return Psr7Stream 122 | */ 123 | public function getBody() 124 | { 125 | if (!$this->stream) { 126 | $this->stream = new BufferedStream(); 127 | } 128 | 129 | return $this->stream; 130 | } 131 | 132 | /** 133 | * @param StreamInterface $body 134 | * @return $this|Request 135 | */ 136 | public function withBody(StreamInterface $body) 137 | { 138 | if ($body === $this->stream) { 139 | return $this; 140 | } 141 | 142 | $new = clone $this; 143 | $new->stream = $body; 144 | return $new; 145 | } 146 | 147 | private function setHeaders(array $headers) 148 | { 149 | $this->headerLines = $this->headers = []; 150 | foreach ($headers as $header => $value) { 151 | $header = trim($header); 152 | $name = strtolower($header); 153 | if (!is_array($value)) { 154 | $value = trim($value); 155 | $this->headers[$name][] = $value; 156 | $this->headerLines[$header][] = $value; 157 | } else { 158 | foreach ($value as $v) { 159 | $v = trim($v); 160 | $this->headers[$name][] = $v; 161 | $this->headerLines[$header][] = $v; 162 | } 163 | } 164 | } 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /src/PromiseResponseInterface.php: -------------------------------------------------------------------------------- 1 | buffer .= $data; 29 | 30 | if (!$this->request && false !== strpos($this->buffer, "\r\n\r\n")) { 31 | 32 | // Extract the header from the buffer 33 | // in case the content isn't complete 34 | list($headers, $this->buffer) = explode("\r\n\r\n", $this->buffer, 2); 35 | 36 | // Fail before parsing if the 37 | if (strlen($headers) > $this->maxSize) { 38 | $this->headerSizeExceeded(); 39 | return; 40 | } 41 | 42 | $this->request = gPsr\parse_request($headers . "\r\n\r\n"); 43 | } 44 | 45 | // if there is a request (meaning the headers are parsed) and 46 | // we have the right content size, we can finish the parsing 47 | if ($this->request && $this->isRequestComplete()) { 48 | $body = substr($this->buffer, 0, $this->length); 49 | // create a stream for the body. 50 | $stream = new BufferedStream(); 51 | $stream->write($body); 52 | // add stream to the request. 53 | $this->request = $this->request->withBody($stream); 54 | 55 | // create server request object 56 | $this->request = new ServerRequest($this->request); 57 | 58 | // todo this should really belong in the header parsing. clean this up. 59 | $parsedQuery = []; 60 | $queryString = $this->request->getUri()->getQuery(); 61 | if ($queryString) { 62 | parse_str($queryString, $parsedQuery); 63 | if (!empty($parsedQuery)) { 64 | $this->request = $this->request->withQueryParams($parsedQuery); 65 | } 66 | } 67 | 68 | // add server request information to the request object. 69 | $this->request = $this->parseBody($body, $this->request); 70 | 71 | $this->emit('headers', array($this->request)); 72 | $this->removeAllListeners(); 73 | $this->request = null; 74 | return; 75 | } 76 | 77 | // fail if the header hasn't finished but it is already too large 78 | if (!$this->request && strlen($this->buffer) > $this->maxSize) { 79 | $this->headerSizeExceeded(); 80 | return; 81 | } 82 | } 83 | 84 | public function parseBody($content, $request) 85 | { 86 | $headers = $request->getHeaders(); 87 | 88 | if (array_key_exists('Content-Type', $headers)) { 89 | $contentType = $headers['Content-Type'][0]; 90 | if (strpos($contentType, 'multipart/') === 0) { 91 | //TODO :: parse the content while it is streaming 92 | preg_match("/boundary=\"?(.*)\"?$/", $contentType, $matches); 93 | $boundary = $matches[1]; 94 | 95 | $parser = new MultipartParser($content, $boundary); 96 | $parser->parse(); 97 | 98 | $request = $request->withParsedBody($parser->getPost()); 99 | $request = $request->withUploadedFiles($parser->getFiles()); 100 | } else if (strpos(strtolower($contentType), 'application/x-www-form-urlencoded') === 0) { 101 | $result = []; 102 | parse_str(urldecode($content), $result); 103 | $request = $request->withParsedBody($result); 104 | } 105 | } 106 | 107 | return $request; 108 | } 109 | 110 | protected function isRequestComplete() 111 | { 112 | $contentLength = $this->request->getHeader('Content-Length'); 113 | 114 | // if there is no content length, there should 115 | // be no content so we can say it's done 116 | if (!$contentLength) { 117 | return true; 118 | } 119 | 120 | // if the content is present and has the 121 | // right length, we're good to go 122 | if ($contentLength[0] && strlen($this->buffer) >= $contentLength[0] ) { 123 | 124 | // store the expected content length 125 | $this->length = $contentLength[0]; 126 | 127 | return true; 128 | } 129 | 130 | return false; 131 | } 132 | 133 | protected function headerSizeExceeded() 134 | { 135 | $this->emit('error', array(new \OverflowException("Maximum header size of {$this->maxSize} exceeded."), $this)); 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/Response.php: -------------------------------------------------------------------------------- 1 | 'Continue', 15 | 101 => 'Switching Protocols', 16 | 102 => 'Processing', 17 | 200 => 'OK', 18 | 201 => 'Created', 19 | 202 => 'Accepted', 20 | 203 => 'Non-Authoritative Information', 21 | 204 => 'No Content', 22 | 205 => 'Reset Content', 23 | 206 => 'Partial Content', 24 | 207 => 'Multi-status', 25 | 208 => 'Already Reported', 26 | 300 => 'Multiple Choices', 27 | 301 => 'Moved Permanently', 28 | 302 => 'Found', 29 | 303 => 'See Other', 30 | 304 => 'Not Modified', 31 | 305 => 'Use Proxy', 32 | 306 => 'Switch Proxy', 33 | 307 => 'Temporary Redirect', 34 | 400 => 'Bad Request', 35 | 401 => 'Unauthorized', 36 | 402 => 'Payment Required', 37 | 403 => 'Forbidden', 38 | 404 => 'Not Found', 39 | 405 => 'Method Not Allowed', 40 | 406 => 'Not Acceptable', 41 | 407 => 'Proxy Authentication Required', 42 | 408 => 'Request Time-out', 43 | 409 => 'Conflict', 44 | 410 => 'Gone', 45 | 411 => 'Length Required', 46 | 412 => 'Precondition Failed', 47 | 413 => 'Request Entity Too Large', 48 | 414 => 'Request-URI Too Large', 49 | 415 => 'Unsupported Media Type', 50 | 416 => 'Requested range not satisfiable', 51 | 417 => 'Expectation Failed', 52 | 418 => 'I\'m a teapot', 53 | 422 => 'Unprocessable Entity', 54 | 423 => 'Locked', 55 | 424 => 'Failed Dependency', 56 | 425 => 'Unordered Collection', 57 | 426 => 'Upgrade Required', 58 | 428 => 'Precondition Required', 59 | 429 => 'Too Many Requests', 60 | 431 => 'Request Header Fields Too Large', 61 | 500 => 'Internal Server Error', 62 | 501 => 'Not Implemented', 63 | 502 => 'Bad Gateway', 64 | 503 => 'Service Unavailable', 65 | 504 => 'Gateway Time-out', 66 | 505 => 'HTTP Version not supported', 67 | 506 => 'Variant Also Negotiates', 68 | 507 => 'Insufficient Storage', 69 | 508 => 'Loop Detected', 70 | 511 => 'Network Authentication Required', 71 | ]; 72 | 73 | /** @var null|string */ 74 | private $reasonPhrase = ''; 75 | 76 | /** @var int */ 77 | private $statusCode = 200; 78 | 79 | /** 80 | * @param int $status Status code for the response, if any. 81 | * @param array $headers Headers for the response, if any. 82 | * @param mixed $body Stream body. 83 | * @param string $version Protocol version. 84 | * @param string $reason Reason phrase (a default will be used if possible). 85 | */ 86 | public function __construct( 87 | $status = 200, 88 | array $headers = [], 89 | $body = null, 90 | $version = '1.1', 91 | $reason = null 92 | ) { 93 | $this->statusCode = (int) $status; 94 | 95 | if (is_string($body)) { 96 | $this->stream = new BufferedStream($body); 97 | } else if ($body instanceof StreamInterface) { 98 | $this->stream = $body; 99 | } 100 | 101 | $this->setHeaders($headers); 102 | if (!$reason && isset(self::$phrases[$this->statusCode])) { 103 | $this->reasonPhrase = self::$phrases[$status]; 104 | } else { 105 | $this->reasonPhrase = (string) $reason; 106 | } 107 | 108 | $this->protocol = $version; 109 | } 110 | 111 | public function getStatusCode() 112 | { 113 | return $this->statusCode; 114 | } 115 | 116 | public function getReasonPhrase() 117 | { 118 | return $this->reasonPhrase; 119 | } 120 | 121 | public function withStatus($code, $reasonPhrase = '') 122 | { 123 | $new = clone $this; 124 | $new->statusCode = (int) $code; 125 | if (!$reasonPhrase && isset(self::$phrases[$new->statusCode])) { 126 | $reasonPhrase = self::$phrases[$new->statusCode]; 127 | } 128 | $new->reasonPhrase = $reasonPhrase; 129 | return $new; 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/Server.php: -------------------------------------------------------------------------------- 1 | io = $io; 23 | $this->io->on('connection', array($this, 'handleNewConnection')); 24 | } 25 | 26 | /** 27 | * On a new connection instantiate a new RequestParser and listen for the data event until such time as all headers 28 | * are parsed. When this happens a Request object will be generated and Server::handleRequest will be called. 29 | * 30 | * @param ConnectionInterface $conn 31 | */ 32 | protected function handleNewConnection(ConnectionInterface $conn) 33 | { 34 | $parser = new RequestParser(); 35 | $parser->on('headers', function (ServerRequest $request) use ($conn, $parser) { 36 | // attach remote ip to the request as metadata 37 | $request = $request->withConnection($conn); 38 | 39 | $conn->removeListener('data', array($parser, 'feed')); 40 | $this->handleRequest($conn, $request); 41 | 42 | }); 43 | 44 | $conn->on('data', array($parser, 'feed')); 45 | } 46 | 47 | /** 48 | * @param ConnectionInterface $conn 49 | * @param Request $request 50 | * @param $bodyBuffer 51 | */ 52 | public function handleRequest(ConnectionInterface $conn, ServerRequest $request) 53 | { 54 | $response = new Response(); 55 | $response = $response->withConnection($conn); 56 | 57 | if (!$this->listeners('request')) { 58 | $conn->end(); 59 | 60 | return; 61 | } 62 | 63 | $this->emit('request', array($conn, $request, $response)); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/ServerRequest.php: -------------------------------------------------------------------------------- 1 | request = $request; 68 | } 69 | 70 | /** 71 | * {@inheritdoc} 72 | */ 73 | public function getServerParams() 74 | { 75 | return $this->serverParams; 76 | } 77 | 78 | /** 79 | * {@inheritdoc} 80 | */ 81 | public function getUploadedFiles() 82 | { 83 | return $this->uploadedFiles; 84 | } 85 | 86 | /** 87 | * {@inheritdoc} 88 | */ 89 | public function withUploadedFiles(array $uploadedFiles) 90 | { 91 | $new = clone $this; 92 | $new->uploadedFiles = $uploadedFiles; 93 | return $new; 94 | } 95 | 96 | /** 97 | * {@inheritdoc} 98 | */ 99 | public function getCookieParams() 100 | { 101 | return $this->cookieParams; 102 | } 103 | 104 | /** 105 | * {@inheritdoc} 106 | */ 107 | public function withCookieParams(array $cookies) 108 | { 109 | $new = clone $this; 110 | $new->cookieParams = $cookies; 111 | return $new; 112 | } 113 | 114 | /** 115 | * {@inheritdoc} 116 | */ 117 | public function getQueryParams() 118 | { 119 | return $this->queryParams; 120 | } 121 | 122 | /** 123 | * {@inheritdoc} 124 | */ 125 | public function withQueryParams(array $query) 126 | { 127 | $new = clone $this; 128 | $new->queryParams = $query; 129 | return $new; 130 | } 131 | 132 | /** 133 | * {@inheritdoc} 134 | */ 135 | public function getParsedBody() 136 | { 137 | return $this->parsedBody; 138 | } 139 | 140 | /** 141 | * {@inheritdoc} 142 | */ 143 | public function withParsedBody($data) 144 | { 145 | $new = clone $this; 146 | $new->parsedBody = $data; 147 | return $new; 148 | } 149 | 150 | /** 151 | * {@inheritdoc} 152 | */ 153 | public function getAttributes() 154 | { 155 | return $this->attributes; 156 | } 157 | 158 | /** 159 | * {@inheritdoc} 160 | */ 161 | public function getAttribute($attribute, $default = null) 162 | { 163 | if (! array_key_exists($attribute, $this->attributes)) { 164 | return $default; 165 | } 166 | 167 | return $this->attributes[$attribute]; 168 | } 169 | 170 | /** 171 | * {@inheritdoc} 172 | */ 173 | public function withAttribute($attribute, $value) 174 | { 175 | $new = clone $this; 176 | $new->attributes[$attribute] = $value; 177 | return $new; 178 | } 179 | 180 | /** 181 | * {@inheritdoc} 182 | */ 183 | public function withoutAttribute($attribute) 184 | { 185 | if (! isset($this->attributes[$attribute])) { 186 | return clone $this; 187 | } 188 | 189 | $new = clone $this; 190 | unset($new->attributes[$attribute]); 191 | return $new; 192 | } 193 | 194 | /** 195 | * Proxy to receive the request method. 196 | * 197 | * This overrides the parent functionality to ensure the method is never 198 | * empty; if no method is present, it returns 'GET'. 199 | * 200 | * @return string 201 | */ 202 | public function getMethod() 203 | { 204 | $method = $this->request->getMethod(); 205 | if (empty($method)) { 206 | return 'GET'; 207 | } 208 | return $method; 209 | } 210 | 211 | /** 212 | * @return string 213 | */ 214 | public function getRequestTarget() 215 | { 216 | return $this->request->getRequestTarget(); 217 | } 218 | 219 | /** 220 | * @param mixed $requestTarget 221 | * @return RequestInterface 222 | */ 223 | public function withRequestTarget($requestTarget) 224 | { 225 | $this->request = $this->request->withRequestTarget($requestTarget); 226 | 227 | return $this; 228 | } 229 | 230 | /** 231 | * @param string $method 232 | * @return RequestInterface 233 | */ 234 | public function withMethod($method) 235 | { 236 | $this->request = $this->request->withMethod($method); 237 | 238 | return $this; 239 | } 240 | 241 | /** 242 | * @return UriInterface 243 | */ 244 | public function getUri() 245 | { 246 | return $this->request->getUri(); 247 | } 248 | 249 | /** 250 | * @param UriInterface $uri 251 | * @param bool|false $preserveHost 252 | * @return RequestInterface 253 | */ 254 | public function withUri(UriInterface $uri, $preserveHost = false) 255 | { 256 | $this->request = $this->request->withUri($uri, $preserveHost); 257 | 258 | return $this; 259 | } 260 | 261 | /** 262 | * @param string $header 263 | * @param string|\string[] $value 264 | * @return \Psr\Http\Message\MessageInterface 265 | */ 266 | public function withHeader($header, $value) 267 | { 268 | $this->request = $this->request->withHeader($header, $value); 269 | 270 | return $this; 271 | } 272 | 273 | /** 274 | * @return string 275 | */ 276 | public function getProtocolVersion() 277 | { 278 | return $this->request->getProtocolVersion(); 279 | } 280 | 281 | /** 282 | * @param string $version 283 | * @return \Psr\Http\Message\MessageInterface 284 | */ 285 | public function withProtocolVersion($version) 286 | { 287 | $this->request = $this->withProtocolVersion($version); 288 | 289 | return $this; 290 | } 291 | 292 | /** 293 | * @return array 294 | */ 295 | public function getHeaders() 296 | { 297 | return $this->request->getHeaders(); 298 | } 299 | 300 | /** 301 | * @param string $header 302 | * @return bool 303 | */ 304 | public function hasHeader($header) 305 | { 306 | return $this->request->hasHeader($header); 307 | } 308 | 309 | /** 310 | * @param string $header 311 | * @return \string[] 312 | */ 313 | public function getHeader($header) 314 | { 315 | return $this->getHeader($header); 316 | } 317 | 318 | /** 319 | * @param string $header 320 | * @return string 321 | */ 322 | public function getHeaderLine($header) 323 | { 324 | return $this->request->getHeaderLine($header); 325 | } 326 | 327 | /** 328 | * @param string $header 329 | * @param string|\string[] $value 330 | * @return \Psr\Http\Message\MessageInterface 331 | */ 332 | public function withAddedHeader($header, $value) 333 | { 334 | $this->request = $this->request->withAddedHeader($header, $value); 335 | 336 | return $this; 337 | } 338 | 339 | /** 340 | * @param string $header 341 | * @return \Psr\Http\Message\MessageInterface 342 | */ 343 | public function withoutHeader($header) 344 | { 345 | $this->request = $this->request->withoutHeader($header); 346 | 347 | return $this; 348 | } 349 | 350 | 351 | 352 | /** 353 | * Get the remote address 354 | * 355 | * @return string 356 | */ 357 | public function getRemoteAddress() 358 | { 359 | if (!is_null($this->connection)) { 360 | return $this->connection->getRemoteAddress(); 361 | } 362 | 363 | return $this->remoteAddress; 364 | } 365 | 366 | /** 367 | * @param $address 368 | * @return ServerRequest 369 | */ 370 | public function withRemoteAddress($address) 371 | { 372 | $new = clone $this; 373 | $new->remoteAddress = $address; 374 | 375 | return $new; 376 | } 377 | 378 | /** 379 | * @return Stream 380 | */ 381 | public function getBody() 382 | { 383 | return $this->request->getBody(); 384 | } 385 | 386 | /** 387 | * @param StreamInterface $body 388 | * @return $this|Request 389 | */ 390 | public function withBody(StreamInterface $body) 391 | { 392 | $this->request = $this->request->withBody($body); 393 | 394 | return $this; 395 | } 396 | 397 | } 398 | -------------------------------------------------------------------------------- /tests/ApplicationTest.php: -------------------------------------------------------------------------------- 1 | noopMiddleware = function ($req, $res, $next) { 12 | }; 13 | 14 | $this->router = $this->getMock('Zend\Expressive\Router\RouterInterface'); 15 | } 16 | 17 | public function getApp() 18 | { 19 | return new Application($this->router); 20 | } 21 | 22 | public function testGetApplicationForConnection() 23 | { 24 | $conn = new ConnectionStub(); 25 | $application = $this->getApp(); 26 | 27 | $application = $application->getApplicationForConnection($conn); 28 | $this->assertInstanceOf('ExpressiveAsync\Application', $application); 29 | } 30 | 31 | public function getEmitterIsAsyncEmitter() 32 | { 33 | $conn = new ConnectionStub(); 34 | $application = $this->getApp(); 35 | 36 | $application = $application->getApplicationForConnection($conn); 37 | $this->assertInstanceOf('ExpressiveAsync\AsyncEmitter', $application->getEmitter()); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /tests/CallableStub.php: -------------------------------------------------------------------------------- 1 | data .= $data; 42 | 43 | return true; 44 | } 45 | 46 | public function end($data = null) 47 | { 48 | } 49 | 50 | public function close() 51 | { 52 | } 53 | 54 | public function getData() 55 | { 56 | return $this->data; 57 | } 58 | 59 | public function getRemoteAddress() 60 | { 61 | return '127.0.0.1'; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /tests/RequestParserTest.php: -------------------------------------------------------------------------------- 1 | on('headers', $this->expectCallableNever()); 13 | 14 | $parser->feed("GET / HTTP/1.1\r\n"); 15 | $parser->feed("Host: example.com:80\r\n"); 16 | $parser->feed("Connection: close\r\n"); 17 | 18 | $parser->removeAllListeners(); 19 | $parser->on('headers', $this->expectCallableOnce()); 20 | 21 | $parser->feed("\r\n"); 22 | } 23 | 24 | public function testFeedInOneGo() 25 | { 26 | $parser = new RequestParser(); 27 | $parser->on('headers', $this->expectCallableOnce()); 28 | 29 | $data = $this->createGetRequest(); 30 | $parser->feed($data); 31 | } 32 | 33 | public function testHeadersEventShouldReturnRequestAndBodyBuffer() 34 | { 35 | $request = null; 36 | $bodyBuffer = null; 37 | 38 | $parser = new RequestParser(); 39 | $parser->on('headers', function ($parsedRequest) use (&$request) { 40 | $request = $parsedRequest; 41 | }); 42 | 43 | $data = $this->createGetRequest('RANDOM DATA', 11); 44 | $parser->feed($data); 45 | 46 | $this->assertInstanceOf('ExpressiveAsync\ServerRequest', $request); 47 | $this->assertSame('GET', $request->getMethod()); 48 | $this->assertSame('/', $request->getUri()->getPath()); 49 | $this->assertSame(array(), $request->getQueryParams()); 50 | $this->assertSame('1.1', $request->getProtocolVersion()); 51 | $this->assertSame( 52 | array('Host' => array('example.com:80'), 'Connection' => array('close'), 'Content-Length' => array('11')), 53 | $request->getHeaders() 54 | ); 55 | 56 | $this->assertSame('RANDOM DATA', $request->getBody()->getContents()); 57 | } 58 | 59 | public function testHeadersEventShouldReturnBinaryBodyBuffer() 60 | { 61 | $request = null; 62 | 63 | $parser = new RequestParser(); 64 | $parser->on('headers', function ($parsedRequest) use (&$request) { 65 | $request = $parsedRequest; 66 | }); 67 | 68 | $data = $this->createGetRequest("\0x01\0x02\0x03\0x04\0x05", strlen("\0x01\0x02\0x03\0x04\0x05")); 69 | $parser->feed($data); 70 | 71 | $this->assertSame("\0x01\0x02\0x03\0x04\0x05", $request->getBody()->getContents()); 72 | } 73 | 74 | /** 75 | * @group issue 76 | */ 77 | public function testHeadersEventShouldParsePathAndQueryString() 78 | { 79 | $request = null; 80 | 81 | $parser = new RequestParser(); 82 | $parser->on('headers', function ($parsedRequest) use (&$request) { 83 | $request = $parsedRequest; 84 | }); 85 | 86 | $data = $this->createAdvancedPostRequest(); 87 | $parser->feed($data); 88 | 89 | $this->assertInstanceOf('ExpressiveAsync\ServerRequest', $request); 90 | $this->assertSame('/foo', $request->getUri()->getPath()); 91 | $this->assertSame(array('bar' => 'baz'), $request->getQueryParams()); 92 | $this->assertSame('1.1', $request->getProtocolVersion()); 93 | $headers = array( 94 | 'Host' => array('example.com:80'), 95 | 'User-Agent' => array('react/alpha'), 96 | 'Connection' => array('close'), 97 | ); 98 | $this->assertSame($headers, $request->getHeaders()); 99 | $this->assertSame('POST', $request->getMethod()); 100 | } 101 | 102 | public function testShouldReceiveBodyContent() 103 | { 104 | $content1 = "{\"test\":"; $content2 = " \"value\"}"; 105 | 106 | $request = null; 107 | 108 | $parser = new RequestParser(); 109 | $parser->on('headers', function ($parsedRequest) use (&$request) { 110 | $request = $parsedRequest; 111 | }); 112 | 113 | $data = $this->createAdvancedPostRequest('', 17); 114 | $parser->feed($data); 115 | $parser->feed($content1); 116 | $parser->feed($content2 . "\r\n"); 117 | 118 | $this->assertInstanceOf('ExpressiveAsync\ServerRequest', $request); 119 | $this->assertEquals($content1 . $content2, $request->getBody()->getContents()); 120 | } 121 | 122 | public function testShouldReceiveMultiPartBody() 123 | { 124 | 125 | $request = null; 126 | 127 | $parser = new RequestParser(); 128 | $parser->on('headers', function ($parsedRequest) use (&$request) { 129 | $request = $parsedRequest; 130 | }); 131 | 132 | $parser->feed($this->createMultipartRequest()); 133 | 134 | $this->assertInstanceOf('ExpressiveAsync\ServerRequest', $request); 135 | $this->assertEquals( 136 | $request->getParsedBody(), 137 | ['user' => 'single', 'user2' => 'second', 'users' => ['first in array', 'second in array']] 138 | ); 139 | $this->assertEquals(2, count($request->getUploadedFiles())); 140 | $this->assertEquals(2, count($request->getUploadedFiles()['files'])); 141 | } 142 | 143 | public function testShouldReceivePostInBody() 144 | { 145 | $request = null; 146 | 147 | $parser = new RequestParser(); 148 | $parser->on('headers', function ($parsedRequest) use (&$request) { 149 | $request = $parsedRequest; 150 | }); 151 | 152 | $parser->feed($this->createPostWithContent()); 153 | 154 | $this->assertInstanceOf('ExpressiveAsync\ServerRequest', $request); 155 | $this->assertSame( 156 | 'user=single&user2=second&users%5B%5D=first+in+array&users%5B%5D=second+in+array', 157 | $request->getBody()->getContents() 158 | ); 159 | $this->assertEquals( 160 | $request->getParsedBody(), 161 | ['user' => 'single', 'user2' => 'second', 'users' => ['first in array', 'second in array']] 162 | ); 163 | } 164 | 165 | public function testHeaderOverflowShouldEmitError() 166 | { 167 | $error = null; 168 | 169 | $parser = new RequestParser(); 170 | $parser->on('headers', $this->expectCallableNever()); 171 | $parser->on('error', function ($message) use (&$error) { 172 | $error = $message; 173 | }); 174 | 175 | $data = str_repeat('A', 4097); 176 | $parser->feed($data); 177 | 178 | $this->assertInstanceOf('OverflowException', $error); 179 | $this->assertSame('Maximum header size of 4096 exceeded.', $error->getMessage()); 180 | } 181 | 182 | public function testOnePassHeaderTooLarge() 183 | { 184 | $error = null; 185 | 186 | $parser = new RequestParser(); 187 | $parser->on('headers', $this->expectCallableNever()); 188 | $parser->on('error', function ($message) use (&$error) { 189 | $error = $message; 190 | }); 191 | 192 | $data = "POST /foo?bar=baz HTTP/1.1\r\n"; 193 | $data .= "Host: example.com:80\r\n"; 194 | $data .= "Cookie: " . str_repeat('A', 4097) . "\r\n"; 195 | $data .= "\r\n"; 196 | $parser->feed($data); 197 | 198 | $this->assertInstanceOf('OverflowException', $error); 199 | $this->assertSame('Maximum header size of 4096 exceeded.', $error->getMessage()); 200 | } 201 | 202 | public function testBodyShouldNotOverflowHeader() 203 | { 204 | $error = null; 205 | 206 | $parser = new RequestParser(); 207 | $parser->on('headers', $this->expectCallableOnce()); 208 | $parser->on('error', function ($message) use (&$error) { 209 | $error = $message; 210 | }); 211 | 212 | $data = str_repeat('A', 4097); 213 | $parser->feed($this->createAdvancedPostRequest() . $data); 214 | 215 | $this->assertNull($error); 216 | } 217 | 218 | private function createGetRequest($content = '', $len = 0) 219 | { 220 | $data = "GET / HTTP/1.1\r\n"; 221 | $data .= "Host: example.com:80\r\n"; 222 | $data .= "Connection: close\r\n"; 223 | if($len) { 224 | $data .= "Content-Length: $len\r\n"; 225 | } 226 | $data .= "\r\n"; 227 | $data .= $content; 228 | 229 | return $data; 230 | } 231 | 232 | private function createAdvancedPostRequest($content = '', $len = 0) 233 | { 234 | $data = "POST /foo?bar=baz HTTP/1.1\r\n"; 235 | $data .= "Host: example.com:80\r\n"; 236 | $data .= "User-Agent: react/alpha\r\n"; 237 | $data .= "Connection: close\r\n"; 238 | if($len) { 239 | $data .= "Content-Length: $len\r\n"; 240 | } 241 | $data .= "\r\n"; 242 | $data .= $content; 243 | 244 | return $data; 245 | } 246 | 247 | private function createPostWithContent() 248 | { 249 | $data = "POST /foo?bar=baz HTTP/1.1\r\n"; 250 | $data .= "Host: localhost:8080\r\n"; 251 | $data .= "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.7; rv:32.0) Gecko/20100101 Firefox/32.0\r\n"; 252 | $data .= "Connection: close\r\n"; 253 | $data .= "Content-Type: application/x-www-form-urlencoded\r\n"; 254 | $data .= "Content-Length: 79\r\n"; 255 | $data .= "\r\n"; 256 | $data .= "user=single&user2=second&users%5B%5D=first+in+array&users%5B%5D=second+in+array\r\n"; 257 | 258 | return $data; 259 | } 260 | 261 | private function createMultipartRequest() 262 | { 263 | $data = "POST / HTTP/1.1\r\n"; 264 | $data .= "Host: localhost:8080\r\n"; 265 | $data .= "Connection: close\r\n"; 266 | $data .= "Content-Type: multipart/form-data; boundary=---------------------------12758086162038677464950549563\r\n"; 267 | $data .= "Content-Length: 1097\r\n"; 268 | $data .= "\r\n"; 269 | 270 | $data .= "-----------------------------12758086162038677464950549563\r\n"; 271 | $data .= "Content-Disposition: form-data; name=\"user\"\r\n"; 272 | $data .= "\r\n"; 273 | $data .= "single\r\n"; 274 | $data .= "-----------------------------12758086162038677464950549563\r\n"; 275 | $data .= "Content-Disposition: form-data; name=\"user2\"\r\n"; 276 | $data .= "\r\n"; 277 | $data .= "second\r\n"; 278 | $data .= "-----------------------------12758086162038677464950549563\r\n"; 279 | $data .= "Content-Disposition: form-data; name=\"users[]\"\r\n"; 280 | $data .= "\r\n"; 281 | $data .= "first in array\r\n"; 282 | $data .= "-----------------------------12758086162038677464950549563\r\n"; 283 | $data .= "Content-Disposition: form-data; name=\"users[]\"\r\n"; 284 | $data .= "\r\n"; 285 | $data .= "second in array\r\n"; 286 | $data .= "-----------------------------12758086162038677464950549563\r\n"; 287 | $data .= "Content-Disposition: form-data; name=\"file\"; filename=\"User.php\"\r\n"; 288 | $data .= "Content-Type: text/php\r\n"; 289 | $data .= "\r\n"; 290 | $data .= "on('request', $this->expectCallableOnce()); 15 | 16 | $conn = new ConnectionStub(); 17 | $io->emit('connection', array($conn)); 18 | 19 | $data = $this->createGetRequest(); 20 | $conn->emit('data', array($data)); 21 | } 22 | 23 | public function testRequestEvent() 24 | { 25 | $io = new ServerStub(); 26 | 27 | $i = 0; 28 | 29 | $server = new Server($io); 30 | $server->on('request', function ($conn, $request, $response) use (&$i) { 31 | $i++; 32 | 33 | $this->assertInstanceOf('ExpressiveAsync\Test\ConnectionStub', $conn); 34 | $this->assertInstanceOf('ExpressiveAsync\ServerRequest', $request); 35 | $this->assertSame('/', $request->getUri()->getPath()); 36 | $this->assertSame('GET', $request->getMethod()); 37 | $this->assertSame('127.0.0.1', $request->getRemoteAddress()); 38 | 39 | $this->assertInstanceOf('ExpressiveAsync\Response', $response); 40 | }); 41 | 42 | $conn = new ConnectionStub(); 43 | $io->emit('connection', array($conn)); 44 | 45 | $data = $this->createGetRequest(); 46 | $conn->emit('data', array($data)); 47 | 48 | $this->assertSame(1, $i); 49 | } 50 | 51 | private function createPostRequest() 52 | { 53 | $data = "GET / HTTP/1.1\r\n"; 54 | $data .= "Host: example.com:80\r\n"; 55 | $data .= "Connection: close\r\n"; 56 | $data .= "\r\n"; 57 | 58 | return $data; 59 | } 60 | 61 | private function createGetRequest() 62 | { 63 | $data = "GET / HTTP/1.1\r\n"; 64 | $data .= "Host: example.com:80\r\n"; 65 | $data .= "Connection: close\r\n"; 66 | $data .= "\r\n"; 67 | 68 | return $data; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | createCallableMock(); 10 | $mock 11 | ->expects($this->exactly($amount)) 12 | ->method('__invoke'); 13 | 14 | return $mock; 15 | } 16 | 17 | protected function expectCallableOnce() 18 | { 19 | $mock = $this->createCallableMock(); 20 | $mock 21 | ->expects($this->once()) 22 | ->method('__invoke'); 23 | 24 | return $mock; 25 | } 26 | 27 | protected function expectCallableNever() 28 | { 29 | $mock = $this->createCallableMock(); 30 | $mock 31 | ->expects($this->never()) 32 | ->method('__invoke'); 33 | 34 | return $mock; 35 | } 36 | 37 | protected function createCallableMock() 38 | { 39 | return $this->getMock('ExpressiveAsync\Test\CallableStub'); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | addPsr4('ExpressiveAsync\\Test\\', __DIR__); 5 | --------------------------------------------------------------------------------