├── .travis.yml ├── README.md ├── app.phpsgi ├── bin └── funk ├── composer.json ├── phpunit.xml ├── phpunit.xml.dist ├── src ├── App │ ├── MuxApp.php │ └── MuxAppTest.php ├── Buffer │ └── SAPIInputBuffer.php ├── Compositor.php ├── CompositorTest.php ├── Console.php ├── Environment.php ├── EnvironmentTest.php ├── Middleware │ ├── CORSMiddleware.php │ ├── ContentNegotiationMiddleware.php │ ├── ContentNegotiationMiddlewareTest.php │ ├── GeocoderMiddleware.php │ ├── GeocoderMiddlewareTest.php │ ├── HeadMiddleware.php │ ├── MiddlewareTest.php │ ├── TryCatchMiddleware.php │ ├── XHProfMiddleware.php │ ├── XHProfMiddlewareTest.php │ └── XHTTPMiddleware.php ├── Request.php ├── Responder │ ├── SAPIResponder.php │ └── SAPIResponderTest.php ├── Server │ ├── BaseServer.php │ ├── EventHttpServer.php │ └── StreamSocketServer.php └── Testing │ └── TestUtils.php └── tests ├── bootstrap.php ├── controllers.php └── travis-setup.sh /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | php: 3 | - '7.0' 4 | - '7.1' 5 | - hhvm 6 | matrix: 7 | fast_finish: true 8 | allow_failures: 9 | - php: hhvm 10 | install: 11 | - composer require "satooshi/php-coveralls" "^1" --dev --no-update 12 | - composer install 13 | script: 14 | - phpunit -c phpunit.xml.dist 15 | after_success: 16 | - php vendor/bin/coveralls -v 17 | cache: 18 | apt: true 19 | directories: 20 | - vendor 21 | notifications: 22 | email: 23 | on_success: change 24 | on_failure: change 25 | slack: 26 | secure: z/QbNbfOj8oHvJWjviKe3oU/g2DDqvqHv/Fb8ONIJuqJt0fv/mmy8+QWWFp2mXrF+c6Ncn9W77spLWMCDlAdXNvm9Sf61KVVO0PlFuA48V3AnfbyCF/1txZ1OpxKkVCzzOsmq6yVef2eHgXtZFdk+uGaLs4R1AaL33urOrFZT4zXF8wE2rKd4davdtimP30++hJEO3JUrInQkv0AmrC/A66y1G/hPLpVu3Xf+sYDvgnGoXsTKBcrvcWQeB7684hKHAPLwHLiLe1Tnbsn6h8zdF+HZuTLfNTE/FsesTnZ5zGvDc3wSFBBd3Fu6xceUhQdRoTPfan8o5JlYAIqKRgsPLvpv9xK0AdWTxy7RRDKuwhsNHvOtwBK14HE+XHUv7lL6CYiQi9NN/UOA89Tqox9wEruEY8ab15JtW+2d5lN6ZKkloTFujoVazxedpjX2IC5ahzKfGjxrChUHJRP9QJcRWFVbh3gWux+VYVSBZaJZ91ACK0pPvvR0xd2a7zlKT/X4cUfPv5BLRB4t1NVFW3lnxuMhdXmdPh5mOuxpbBaudA7wrYqiyXv6ZGD7AyRnvejC8afx+Yzu411Zy2iXh9MP0X+ZReryvdfhEB6jk/YQRs3Cn7ouvSQQu0LTDfTX9oorfYJHkQkaB0PJJku3Srm08Oi1k/vxviN86sJyNxRjrA= 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Funk 2 | 3 | [![Build Status](https://travis-ci.org/phpsgi/Funk.svg?branch=master)](https://travis-ci.org/phpsgi/Funk) 4 | [![Coverage Status](https://img.shields.io/coveralls/phpsgi/Funk.svg)](https://coveralls.io/r/phpsgi/Funk) 5 | [![Latest Stable Version](https://poser.pugx.org/phpsgi/funk/v/stable.svg)](https://packagist.org/packages/phpsgi/funk) 6 | [![Total Downloads](https://poser.pugx.org/phpsgi/funk/downloads.svg)](https://packagist.org/packages/phpsgi/funk) 7 | [![Monthly Downloads](https://poser.pugx.org/phpsgi/funk/d/monthly)](https://packagist.org/packages/phpsgi/funk) 8 | [![Daily Downloads](https://poser.pugx.org/phpsgi/funk/d/daily)](https://packagist.org/packages/phpsgi/funk) 9 | [![Latest Unstable Version](https://poser.pugx.org/phpsgi/funk/v/unstable.svg)](https://packagist.org/packages/phpsgi/funk) 10 | [![License](https://poser.pugx.org/phpsgi/funk/license.svg)](https://packagist.org/packages/phpsgi/funk) 11 | [![Join the chat at https://gitter.im/phpsgi/funk](https://badges.gitter.im/phpsgi/funk.svg)](https://gitter.im/phpsgi/funk?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 12 | [![Made in Taiwan](https://img.shields.io/badge/made%20in-taiwan-green.svg)](README.md) 13 | 14 | Funk is an implementation of PHPSGI. It supports HTTP servers implemented with PHP SAPI (Apache2 `mod_php`, `php-fpm`, `fastcgi`), therefore you can integrate your application with Funk and switch to different HTTP server implementation. 15 | 16 | PHPSGI and Funk aims to provide lightweight HTTP interfaces, middlewares for 17 | web frameworks. It's a bit different from the PSR-7 spec. PHPSGI focuses on the core data structure instead of 18 | forcing components to implement the interface requirements. 19 | 20 | ## Components 21 | 22 | - HTTP server (with event extension or `socket_select`) 23 | - SAPI support (php-fpm, apache2 php handler servers) 24 | - Middlewares 25 | - Middleware Compositor 26 | - A Simple Mux Builder (integrated with Pux) 27 | 28 | 29 | ### Environment 30 | 31 | ```php 32 | // This creates $env array from $_SERVER, $_REQUEST, $_POST, $_GET ... 33 | $env = Environment::createFromGlobals(); 34 | ``` 35 | 36 | ### Application 37 | 38 | ```php 39 | $app = function(array & $environment, array $response) { 40 | return [ 200, [ 'Content-Type' => 'text/plain' ], 'Hello World' ]; 41 | }; 42 | ``` 43 | 44 | 45 | ### Responder 46 | 47 | #### SAPIResponder 48 | 49 | You can integrate your application with SAPIResponder to support Apache2 php handler / php-fpm / fastcgi. 50 | 51 | ```php 52 | use Funk\Responder\SAPIResponder; 53 | 54 | $fd = fopen('php://output', 'w'); 55 | $responder = new SAPIResponder($fd); 56 | $responder->respond([ 200, [ 'Content-Type: text/plain' ], 'Hello World' ]); 57 | fclose($fd); 58 | ``` 59 | 60 | 61 | ```php 62 | use Funk\Responder\SAPIResponder; 63 | 64 | $env = Environment::createFromGlobals(); 65 | $app = function(array & $environment, array $response) { 66 | return [ 200, [ 'Content-Type' => 'text/plain' ], 'Hello World' ]; 67 | }; 68 | $fd = fopen('php://output', 'w'); 69 | $responder = new SAPIResponder($fd); 70 | $responder->respond($app($env, [])); 71 | fclose($fd); 72 | ``` 73 | 74 | 75 | 76 | ### Middleware 77 | 78 | - `Funk\Middleware\ContentNegotiationMiddleware` 79 | - `Funk\Middleware\CORSMiddleware` 80 | - `Funk\Middleware\GeocoderMiddleware` 81 | - `Funk\Middleware\HeadMiddleware` 82 | - `Funk\Middleware\TryCatchMiddleware` 83 | - `Funk\Middleware\XHProfMiddleware` 84 | - `Funk\Middleware\XHTTPMiddleware` 85 | 86 | 87 | ```php 88 | use Funk\Environment; 89 | use Funk\Middleware\TryCacheMiddleware; 90 | 91 | $app = function(array $environment, array $response) { 92 | return [ 200, ['Content-Type' => 'text/html' ], 'Hello World' ]; 93 | }; 94 | $middleware = new TryCatchMiddleware($app); 95 | 96 | 97 | $env = Environment::createFromGlobals(); 98 | $response = $middleware($env, [200, [], []]); 99 | ``` 100 | 101 | 102 | 103 | ## Contributing 104 | 105 | ### Testing XHProf Middleware 106 | 107 | 108 | Define your XHPROF_ROOT in your `phpunit.xml`, you can copy `phpunit.xml.dist` to `phpunit.xml`, 109 | for example: 110 | 111 | ```xml 112 | 113 | 114 | 115 | ``` 116 | 117 | -------------------------------------------------------------------------------- /app.phpsgi: -------------------------------------------------------------------------------- 1 | runWithTry($argv); 13 | exit($ret); 14 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "phpsgi/funk", 3 | "description": "An implementation of PHPSGI middleware specification", 4 | "homepage": "https://github.com/phpsgi/Funk", 5 | "keywords": [ 6 | "http", 7 | "middleware", 8 | "server", 9 | "phpsgi" 10 | ], 11 | "authors": [ 12 | { 13 | "name": "c9s", 14 | "email": "cornelius.howl@gmail.com" 15 | } 16 | ], 17 | "require": { 18 | "universal/universal": "2.0.x-dev", 19 | "phpsgi/phpsgi": "@dev" 20 | }, 21 | "license": "MIT", 22 | "suggest": { 23 | "corneltek/pux": "2.0.x-dev", 24 | "corneltek/cliframework": "4.0.x-dev" 25 | }, 26 | "require-dev": { 27 | "corneltek/pux": "2.0.x-dev", 28 | "willdurand/negotiation": "^1.4", 29 | "willdurand/geocoder": "^3.1" 30 | }, 31 | "autoload": { 32 | "psr-4": { "Funk\\": "src/" } 33 | }, 34 | "extra": { "branch-alias": { "dev-master": "1.0.x-dev" } } 35 | } 36 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | src 13 | 14 | 15 | 16 | src/Middleware 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | src 9 | 10 | 11 | src/Middleware 12 | 13 | 14 | 15 | 16 | 17 | src 18 | 19 | src 20 | 21 | 22 | 23 | 24 | 25 | 26 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /src/App/MuxApp.php: -------------------------------------------------------------------------------- 1 | mux = $mux ?: new Mux; 16 | } 17 | 18 | /** 19 | * Mount app on a specific path 20 | * 21 | * @param string $path 22 | * @param App $app 23 | * @return MuxApp 24 | */ 25 | public function mount($path, App $app) 26 | { 27 | $this->mux->any($path, $app); 28 | return $this; 29 | } 30 | 31 | public function getMux() 32 | { 33 | return $this->mux; 34 | } 35 | 36 | public function call(array & $environment, array $response) 37 | { 38 | if ($route = $this->mux->dispatch($environment['PATH_INFO'])) { 39 | $path = $route[1]; 40 | $app = $route[2]; 41 | 42 | // Save the original PATH_INFO in ORIG_PATH_INFO 43 | // Note that some SAPI implementation will save 44 | // use the ORIG_PATH_INFO (not noticed yet) 45 | if (!isset($environment['ORIG_PATH_INFO'])) { 46 | $environment['ORIG_PATH_INFO'] = $environment['PATH_INFO']; 47 | } 48 | $environment['PATH_INFO'] = substr($environment['PATH_INFO'], strlen($path)); 49 | 50 | // If callback object complies the App call prorotype 51 | if ($app instanceof App || $app instanceof Middleware) { 52 | 53 | return $app($environment, $response); 54 | 55 | } else { 56 | 57 | return RouteExecutor::execute($route); 58 | 59 | } 60 | } 61 | return $response; 62 | } 63 | 64 | public function __invoke(array $environment, array $response) 65 | { 66 | return $this->call($environment, $response); 67 | } 68 | 69 | static public function mountWithUrlMap(array $map) 70 | { 71 | $mux = new Mux; 72 | foreach ($map as $path => $app) { 73 | if ($app instanceof Compositor) { 74 | $app = $app->wrap(); 75 | } 76 | $mux->any($path, $app); 77 | } 78 | return new self($mux); 79 | } 80 | 81 | 82 | } 83 | 84 | 85 | 86 | -------------------------------------------------------------------------------- /src/App/MuxAppTest.php: -------------------------------------------------------------------------------- 1 | ['ProductController', 'fooAction'], 17 | "/bar" => ['ProductController', 'barAction'], 18 | ]); 19 | $this->assertNotNull($app); 20 | $this->assertInstanceOf(MuxApp::class,$app); 21 | $this->assertInstanceOf(App::class, $app, 'Must be an instanceof PHPSGI App'); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Buffer/SAPIInputBuffer.php: -------------------------------------------------------------------------------- 1 | fd = fopen('php://input', 'r'); 13 | } 14 | 15 | public function read($bytes) 16 | { 17 | return fread($this->fd, $bytes); 18 | } 19 | 20 | public function __destruct() 21 | { 22 | fclose($this->fd); 23 | } 24 | } 25 | 26 | -------------------------------------------------------------------------------- /src/Compositor.php: -------------------------------------------------------------------------------- 1 | app = $app; 31 | } 32 | 33 | public function enable($appClass) 34 | { 35 | if ($appClass instanceof Closure) { 36 | $this->stacks[] = $appClass; 37 | } else { 38 | $args = func_get_args(); 39 | array_shift($args); 40 | $this->stacks[] = [$appClass, $args]; 41 | } 42 | return $this; 43 | } 44 | 45 | public function app($app) 46 | { 47 | $this->app = $app; 48 | return $this; 49 | } 50 | 51 | public function wrap() 52 | { 53 | $app = $this->app; 54 | 55 | for ($i = count($this->stacks) - 1; $i > 0; $i--) { 56 | $stack = $this->stacks[$i]; 57 | 58 | // middleware closure 59 | if (is_callable($stack)) { 60 | $app = $stack($app); 61 | } else { 62 | list($appClass, $args) = $stack; 63 | $refClass = new ReflectionClass($appClass); 64 | array_unshift($args, $app); 65 | $app = $refClass->newInstanceArgs($args); 66 | } 67 | } 68 | 69 | return $app; 70 | } 71 | 72 | public function call(array & $environment, array $response) 73 | { 74 | if ($app = $this->wrappedApp) { 75 | return $app($environment, $response); 76 | } 77 | $this->wrappedApp = $app = $this->wrap(); 78 | return $app($environment, $response); 79 | } 80 | 81 | public function __invoke(array & $environment, array $response) 82 | { 83 | return $this->call($environment, $response); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/CompositorTest.php: -------------------------------------------------------------------------------- 1 | enable(TryCatchMiddleware::class, [ 'throw' => true ]); 19 | $compositor->enable(function($app) { 20 | return function(array & $environment, array $response) use ($app) { 21 | $environment['middleware.app'] = true; 22 | return $app($environment, $response); 23 | }; 24 | }); 25 | 26 | $compositor->app(function(array & $environment, array $response) { 27 | $request = RouteRequest::createFromEnv($environment); 28 | if ($request->pathStartWith('/foo')) { 29 | 30 | } 31 | 32 | $response[0] = 200; 33 | return $response; 34 | }); 35 | 36 | $env = TestUtils::createEnv('GET', '/foo/bar'); 37 | $response = $compositor($env, []); 38 | $this->assertNotEmpty($response); 39 | } 40 | 41 | public function testCompositorWithRecursiveUrlMap() 42 | { 43 | $appcomp = new Compositor; 44 | $appcomp->app(MuxApp::mountWithUrlMap([ 45 | "/hack" => new Compositor(MuxApp::mountWithUrlMap([ 46 | "/foo" => ['ProductController', 'fooAction'], 47 | "/bar" => ['ProductController', 'barAction'], 48 | ])), 49 | ])); 50 | $app = $appcomp->wrap(); 51 | 52 | $env = TestUtils::createEnv('GET', '/hack/foo'); 53 | $response = $app($env, []); 54 | $this->assertNotEmpty($response); 55 | $this->assertEquals('foo',$response); 56 | } 57 | 58 | public function testCompositorWithUrlMap() 59 | { 60 | $compositor = new Compositor; 61 | $compositor->app(MuxApp::mountWithUrlMap([ 62 | "/foo" => ['ProductController', 'fooAction'], 63 | "/bar" => ['ProductController', 'barAction'], 64 | ])); 65 | $app = $compositor->wrap(); 66 | $this->assertInstanceOf('Funk\\App\\MuxApp', $app, 67 | 'When there is only one app and no middleware, the returned type should be just MuxApp'); 68 | $env = TestUtils::createEnv('GET', '/foo'); 69 | $response = $app($env, []); 70 | $this->assertNotEmpty($response); 71 | $this->assertEquals('foo',$response); 72 | } 73 | 74 | 75 | 76 | public function testCompositor() 77 | { 78 | $compositor = new Compositor; 79 | $compositor->enable(TryCatchMiddleware::class, [ 'throw' => true ]); 80 | $compositor->enable(function($app) { 81 | return function(array & $environment, array $response) use ($app) { 82 | $environment['middleware.app'] = true; 83 | return $app($environment, $response); 84 | }; 85 | }); 86 | 87 | // TODO 88 | // $compositor->mount('/foo', function() { }); 89 | 90 | $compositor->app(function(array & $environment, array $response) { 91 | $request = RouteRequest::createFromEnv($environment); 92 | 93 | // $mux = new Mux; 94 | 95 | if ($request->pathStartWith('/foo')) { 96 | 97 | } 98 | 99 | $response[0] = 200; 100 | return $response; 101 | }); 102 | $app = $compositor->wrap(); 103 | 104 | $env = TestUtils::createEnv('GET', '/foo'); 105 | $res = [ ]; 106 | $res = $app($env, $res); 107 | $this->assertNotEmpty($res); 108 | $this->assertEquals(200, $res[0]); 109 | } 110 | } 111 | 112 | -------------------------------------------------------------------------------- /src/Console.php: -------------------------------------------------------------------------------- 1 | 'text/plain' ], 'Hello World' ]; 40 | }; 41 | } 42 | 43 | if (extension_loaded('event')) { 44 | 45 | $logger->info("Found 'event' extension, enabling EventHttpServer server."); 46 | $server = new EventHttpServer($app); 47 | 48 | } else { 49 | 50 | $logger->info("Falling back to StreamSocketServer server."); 51 | $server = new StreamSocketServer($app); 52 | } 53 | return $server->listen(); 54 | } 55 | 56 | } 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /src/Environment.php: -------------------------------------------------------------------------------- 1 | assertNotEmpty($env); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/Middleware/CORSMiddleware.php: -------------------------------------------------------------------------------- 1 | options = $this->normalizeOptions($options); 11 | } 12 | private function normalizeOptions(array $options = array()) 13 | { 14 | $options += array( 15 | 'allowedOrigins' => array(), 16 | 'supportsCredentials' => false, 17 | 'allowedHeaders' => array(), 18 | 'exposedHeaders' => array(), 19 | 'allowedMethods' => array(), 20 | 'maxAge' => 0, 21 | ); 22 | // normalize array('*') to true 23 | if (in_array('*', $options['allowedOrigins'])) { 24 | $options['allowedOrigins'] = true; 25 | } 26 | if (in_array('*', $options['allowedHeaders'])) { 27 | $options['allowedHeaders'] = true; 28 | } else { 29 | $options['allowedHeaders'] = array_map('strtolower', $options['allowedHeaders']); 30 | } 31 | if (in_array('*', $options['allowedMethods'])) { 32 | $options['allowedMethods'] = true; 33 | } else { 34 | $options['allowedMethods'] = array_map('strtoupper', $options['allowedMethods']); 35 | } 36 | return $options; 37 | } 38 | public function isActualRequestAllowed(array $environment) 39 | { 40 | return $this->checkOrigin($environment); 41 | } 42 | 43 | public function isCorsRequest(array $environment) 44 | { 45 | return isset($environment['HTTP_ORIGIN']); 46 | } 47 | 48 | public function isPreflightRequest(array $environment) 49 | { 50 | // if (isset($_SERVER['HTTP_ACCESS_CONTROL_REQUEST_METHOD'])) 51 | return $this->isCorsRequest($environment) 52 | && $environment['REQUEST_METHOD'] == 'OPTIONS' 53 | && isset($environment['HTTP_ACCESS_CONTROL_REQUEST_METHOD']); 54 | } 55 | 56 | public function addActualRequestHeaders(array $environment, array $response) 57 | { 58 | if (! $this->checkOrigin($environment)) { 59 | return $response; 60 | } 61 | $response[1][] = [ 'Access-Control-Allow-Origin' => $request->headers->get('Origin') ]; 62 | /* 63 | if (! $response->headers->has('Vary')) { 64 | $response->headers->set('Vary', 'Origin'); 65 | } else { 66 | $response->headers->set('Vary', $response->headers->get('Vary') . ', Origin'); 67 | } 68 | */ 69 | if ($this->options['supportsCredentials']) { 70 | $response[1][] = ['Access-Control-Allow-Credentials' => 'true']; 71 | } 72 | if ($this->options['exposedHeaders']) { 73 | $response[1][] = ['Access-Control-Expose-Headers' => implode(', ', $this->options['exposedHeaders'])]; 74 | } 75 | return $response; 76 | } 77 | public function handlePreflightRequest(array $environment) 78 | { 79 | if (true !== $check = $this->checkPreflightRequestConditions($environment)) { 80 | return $check; 81 | } 82 | return $this->buildPreflightCheckResponse($environment); 83 | } 84 | 85 | private function buildPreflightCheckResponse(array $environment) 86 | { 87 | $response = [200,[],[]]; 88 | if ($this->options['supportsCredentials']) { 89 | $response[1][] = array('Access-Control-Allow-Credentials' => 'true'); 90 | } 91 | $response[1][] = array('Access-Control-Allow-Origin' => $request->headers->get('Origin')); 92 | 93 | if ($this->options['maxAge']) { 94 | $response[1][] = array('Access-Control-Max-Age' => $this->options['maxAge']); 95 | } 96 | 97 | $allowMethods = $this->options['allowedMethods'] === true 98 | ? strtoupper($environment['HTTP_ACCESS_CONTROL_REQUEST_METHOD']) 99 | : implode(', ', $this->options['allowedMethods']); 100 | $response->headers->set('Access-Control-Allow-Methods', $allowMethods); 101 | $allowHeaders = $this->options['allowedHeaders'] === true 102 | ? strtoupper($environment['HTTP_ACCESS_CONTROL_REQUEST_HEADERS']) 103 | : implode(', ', $this->options['allowedHeaders']); 104 | 105 | $response[1][] = array('Access-Control-Allow-Headers' => $allowHeaders); 106 | return $response; 107 | } 108 | 109 | 110 | private function checkPreflightRequestConditions(array $environment) 111 | { 112 | if ( ! $this->checkOrigin($environment)) { 113 | return $this->createBadRequestResponse(403, 'Origin not allowed'); 114 | } 115 | if ( ! $this->checkMethod($environment)) { 116 | return $this->createBadRequestResponse(405, 'Method not allowed'); 117 | } 118 | $requestHeaders = array(); 119 | // if allowedHeaders has been set to true ('*' allow all flag) just skip this check 120 | if ($this_>options['allowedHeaders'] !== true && isset($environment['HTTP_ACCESS_CONTROL_REQUEST_HEADERS'])) { 121 | $headers = strtolower($environment['HTTP_ACCESS_CONTROL_REQUEST_HEADERS']); 122 | $requestHeaders = explode(',', $headers); 123 | foreach ($requestHeaders as $header) { 124 | if (! in_array(trim($header), $this->options['allowedHeaders'])) { 125 | return $this->createBadRequestResponse(403, 'Header not allowed'); 126 | } 127 | } 128 | } 129 | return true; 130 | } 131 | 132 | private function createBadRequestResponse($code, $reason = '') 133 | { 134 | return [ $code, [], $reason]; 135 | } 136 | 137 | private function checkOrigin(array $environment) 138 | { 139 | if ($this->options['allowedOrigins'] === true) { 140 | // allow all '*' flag 141 | return true; 142 | } 143 | $origin = $environment['HTTP_ORIGIN']; 144 | return in_array($origin, $this->options['allowedOrigins']); 145 | } 146 | 147 | private function checkMethod(array $environment) { 148 | if ($this->options['allowedMethods'] === true) { 149 | // allow all '*' flag 150 | return true; 151 | } 152 | $requestMethod = strtoupper($environment['HTTP_ACCESS_CONTROL_REQUEST_METHOD']); 153 | return in_array($requestMethod, $this->options['allowedMethods']); 154 | } 155 | } 156 | 157 | 158 | class CORSMiddleware extends Middleware 159 | { 160 | private $cors; 161 | 162 | private $defaultOptions = array( 163 | 'allowedHeaders' => array(), 164 | 'allowedMethods' => array(), 165 | 'allowedOrigins' => array(), 166 | 'exposedHeaders' => false, 167 | 'maxAge' => false, 168 | 'supportsCredentials' => false, 169 | ); 170 | 171 | public function __construct($app, array $options = array()) 172 | { 173 | parent::__construct($app); 174 | $this->cors = new CORSService(array_merge($this->defaultOptions, $options)); 175 | } 176 | 177 | public function call(array & $environment, array $response) 178 | { 179 | if (! $this->cors->isCorsRequest($environment)) { 180 | return parent::call($environment, $response); 181 | } 182 | 183 | if ($this->cors->isPreflightRequest($environment)) { 184 | return $this->cors->handlePreflightRequest($environment); 185 | } 186 | if ( ! $this->cors->isActualRequestAllowed($environment)) { 187 | return [403, [], 'Not allowed']; 188 | } 189 | $response = parent::call($environment, $response); 190 | return $this->cors->addActualRequestHeaders($environment, $response); 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /src/Middleware/ContentNegotiationMiddleware.php: -------------------------------------------------------------------------------- 1 | negotiator = $negotiator ?: new Negotiator(); 15 | } 16 | 17 | public function call(array & $environment, array $response) 18 | { 19 | $accept = isset($environment['HTTP_ACCEPT']) ? $environment['HTTP_ACCEPT'] : ''; 20 | $priorities = isset($environment['negotiation.priorities']) ? $environment['negotiation.priorities'] : array(); 21 | $environment['request.best_format'] = $this->negotiator->getBest($accept, $priorities); 22 | return parent::call($environment, $response); 23 | } 24 | } 25 | 26 | -------------------------------------------------------------------------------- /src/Middleware/ContentNegotiationMiddlewareTest.php: -------------------------------------------------------------------------------- 1 | assertEquals('text/html', $environment['request.best_format']->getValue()); 16 | }; 17 | 18 | $env = TestUtils::createEnv('GET', '/'); 19 | $env['HTTP_ACCEPT'] = 'text/html, application/xhtml+xml, application/xml;q=0.9, */*;q=0.8'; 20 | $env['negotiation.priorities'] = array('text/html', 'application/json'); 21 | $m = new ContentNegotiationMiddleware($app, new Negotiator); 22 | $response = $m->call($env, []); 23 | } 24 | } 25 | 26 | -------------------------------------------------------------------------------- /src/Middleware/GeocoderMiddleware.php: -------------------------------------------------------------------------------- 1 | geocoder = $geocoder ?: $this->createDefaultGeocoder(); 17 | } 18 | 19 | 20 | public function createDefaultGeocoder() 21 | { 22 | $adapter = new CurlHttpAdapter(); 23 | $geocoder = new FreeGeoIp($adapter); 24 | return $geocoder; 25 | } 26 | 27 | 28 | public function call(array & $environment, array $response) 29 | { 30 | if (isset($environment['REMOTE_ADDR'])) { 31 | $results = $this->geocoder->geocode($environment['REMOTE_ADDR']); 32 | if ($countryCode = $results->get(0)->getCountryCode()) { 33 | $environment['geoip.country_code'] = $countryCode; 34 | } 35 | } 36 | $n = $this->next; 37 | return $n($environment, $response); 38 | } 39 | } 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /src/Middleware/GeocoderMiddlewareTest.php: -------------------------------------------------------------------------------- 1 | assertEquals('US', $env['geoip.country_code']); 22 | return $res; 23 | }; 24 | 25 | 26 | 27 | // $adapter = new CurlHttpAdapter([ 'CURLOPT_CONNECTTIMEOUT' => 10000 ]); 28 | $adapter = new FileGetContentsHttpAdapter(); 29 | $geocoder = new FreeGeoIp($adapter); 30 | $middleware = new GeocoderMiddleware($app, $geocoder); 31 | $env = TestUtils::createEnv('GET', '/'); 32 | $env['REMOTE_ADDR'] = '173.194.72.113'; 33 | $middleware($env, []); 34 | 35 | } catch (\Ivory\HttpAdapter\HttpAdapterException $e) { 36 | // This allowes connection timeout failture 37 | 38 | } 39 | } 40 | } 41 | 42 | -------------------------------------------------------------------------------- /src/Middleware/HeadMiddleware.php: -------------------------------------------------------------------------------- 1 | assertNotEmpty($response); 27 | } 28 | } 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /src/Middleware/TryCatchMiddleware.php: -------------------------------------------------------------------------------- 1 | options = $options; 14 | } 15 | 16 | 17 | public function call(array & $environment, array $response) 18 | { 19 | try { 20 | return parent::call($environment, $response); 21 | } catch (Exception $e) { 22 | if (isset($this->options['throw'])) { 23 | throw $e; 24 | } 25 | } 26 | return $response; 27 | } 28 | } 29 | 30 | -------------------------------------------------------------------------------- /src/Middleware/XHProfMiddleware.php: -------------------------------------------------------------------------------- 1 | options = array_merge([ 17 | 'flags' => XHPROF_FLAGS_CPU + XHPROF_FLAGS_MEMORY, 18 | 'prefix' => 'pux_', 19 | 'output_dir' => ini_get('xhprof.output_dir') ?: '/tmp', 20 | 'root' => null, 21 | ],$options); 22 | 23 | if (!$this->options['root']) { 24 | throw new LogicException("xhprof root is not defined."); 25 | } 26 | include_once $this->options['root'] . "/xhprof_lib/utils/xhprof_lib.php"; 27 | include_once $this->options['root'] . "/xhprof_lib/utils/xhprof_runs.php"; 28 | } 29 | 30 | public function getLastRunId() 31 | { 32 | return $this->runId; 33 | } 34 | 35 | public function call(array & $environment, array $response) 36 | { 37 | $namespace = $this->options['prefix']; 38 | if (isset($environment['PATH_INFO'])) { 39 | $namespace .= $environment['PATH_INFO']; 40 | } else if (isset($environment['REQUEST_URI'])) { 41 | $namespace .= $environment['REQUEST_URI']; 42 | } 43 | $namespace = preg_replace('#[^\w]+#','_', $namespace); 44 | 45 | xhprof_enable($this->options['flags']); 46 | $response = parent::call($environment, $response); 47 | $profile = xhprof_disable(); 48 | 49 | 50 | $runs = new XHProfRuns_Default($this->options['output_dir']); 51 | $this->runId = $runs->save_run($profile, $namespace); 52 | return $response; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Middleware/XHProfMiddlewareTest.php: -------------------------------------------------------------------------------- 1 | markTestSkipped("'XHPROF_ROOT' environment variable is unset."); 16 | } 17 | 18 | $testing = $this; 19 | $app = function(array & $environment, array $response) use ($testing) { 20 | $cnt = 0; 21 | for ($i = 0 ; $i < 10000; $i++) { 22 | $cnt++; 23 | } 24 | return [200, [], ['Hell Yeah']]; 25 | }; 26 | 27 | $env = TestUtils::createEnv('GET', '/'); 28 | $m = new XHProfMiddleware($app, [ 29 | 'flags' => XHPROF_FLAGS_NO_BUILTINS | XHPROF_FLAGS_CPU | XHPROF_FLAGS_MEMORY, 30 | 'root' => getenv('XHPROF_ROOT'), 31 | 'prefix' => 'pux_testing_', 32 | ]); 33 | $response = $m($env, []); 34 | $this->assertNotEmpty($response); 35 | } 36 | } 37 | 38 | -------------------------------------------------------------------------------- /src/Middleware/XHTTPMiddleware.php: -------------------------------------------------------------------------------- 1 | requestMethod = $requestMethod; 38 | $this->path = $path; 39 | 40 | // Note: It's not neccessary to call parent::__construct because we 41 | // don't depend on superglobal variables. 42 | } 43 | 44 | /** 45 | * Return the current path for route dispatching. 46 | * the path can be PATH_INFO or REQUEST_URI (depends on what the user gives) 47 | * 48 | * @return string path 49 | */ 50 | public function getPath() 51 | { 52 | return $this->path; 53 | } 54 | 55 | /** 56 | * Return the request method 57 | * 58 | * @return string request method. 59 | */ 60 | public function getRequestMethod() 61 | { 62 | return $this->requestMethod; 63 | } 64 | 65 | /** 66 | * @param contraints[] 67 | * 68 | * @return boolean true on match 69 | */ 70 | public function matchConstraints(array $constraints) 71 | { 72 | foreach ($constraints as $constraint) { 73 | $result = true; 74 | if (isset($constraints['host_match'])) { 75 | $result = $result && $this->hostMatch($constraints['host_match']); 76 | } 77 | 78 | if (isset($constraints['host'])) { 79 | $result = $result && $this->hostEqual($constraints['host']); 80 | } 81 | 82 | if (isset($constraints['request_method'])) { 83 | $result = $result && $this->requestMethodEqual($constraints['request_method']); 84 | } 85 | 86 | if (isset($constraints['path_match'])) { 87 | $result = $result && $this->pathMatch($constraints['path_match']); 88 | } 89 | 90 | if (isset($constraints['path'])) { 91 | $result = $result && $this->pathEqual($constraints['path']); 92 | } 93 | 94 | // If it matches all constraints, we simply return true and skip other constraints 95 | if ($result) { 96 | return true; 97 | } 98 | // try next one 99 | } 100 | 101 | return false; 102 | } 103 | 104 | public function queryStringMatch($pattern, array &$matches = array()) 105 | { 106 | return preg_match($pattern, $this->serverParameters['QUERY_STRING'], $matches) !== false; 107 | } 108 | 109 | public function portEqual($port) 110 | { 111 | if (isset($this->serverParameters['SERVER_PORT'])) { 112 | return intval($this->serverParameters['SERVER_PORT']) == intval($port); 113 | } 114 | } 115 | 116 | /** 117 | * Check if the request host is in the list of host. 118 | * 119 | * @param array $hosts 120 | * 121 | * @return bool 122 | */ 123 | public function isOneOfHosts(array $hosts) 124 | { 125 | foreach ($hosts as $host) { 126 | if ($this->matchHost($host)) { 127 | return true; 128 | } 129 | } 130 | 131 | return false; 132 | } 133 | 134 | public function pathLike($path) 135 | { 136 | $pattern = '#'.preg_quote($path, '#').'#i'; 137 | 138 | return preg_match($pattern, $this->path) !== false; 139 | } 140 | 141 | public function pathMatch($pattern, array &$matches = array()) 142 | { 143 | return preg_match($pattern, $this->path, $matches) !== false; 144 | } 145 | 146 | public function pathEqual($path) 147 | { 148 | return strcasecmp($path, $this->path) === 0; 149 | } 150 | 151 | public function pathContain($path) 152 | { 153 | return strpos($this->path, $path) !== false; 154 | } 155 | 156 | public function pathStartWith($path) 157 | { 158 | return strpos($this->path, $path) === 0; 159 | } 160 | 161 | public function pathEndWith($suffix) 162 | { 163 | $p = strrpos($this->path, $suffix); 164 | 165 | return ($p == strlen($this->path) - strlen($suffix)); 166 | } 167 | 168 | public function hostMatch($host, array &$matches = array()) 169 | { 170 | if (isset($this->serverParameters['HTTP_HOST'])) { 171 | return preg_match($host, $this->serverParameters['HTTP_HOST'], $matches) !== false; 172 | } 173 | // the HTTP HOST is not defined. 174 | return false; 175 | } 176 | 177 | public function hostEqual($host) 178 | { 179 | if (isset($this->serverParameters['HTTP_HOST'])) { 180 | return strcasecmp($this->serverParameters['HTTP_HOST'], $host) === 0; 181 | } 182 | 183 | return false; 184 | } 185 | 186 | /** 187 | * requestMethodEqual does not use PCRE pattern to match request method. 188 | * 189 | * @param string $requestMethod 190 | */ 191 | public function requestMethodEqual($requestMethod) 192 | { 193 | return strcasecmp($this->requestMethod, $requestMethod) === 0; 194 | } 195 | 196 | /** 197 | * A helper function for creating request object based on request method and request uri. 198 | * 199 | * @param string $method 200 | * @param string $path 201 | * @param array $headers The headers will be built on $_SERVER if the argument is null. 202 | * 203 | * @return RouteRequest 204 | */ 205 | public static function create($method, $path, array $env = array()) 206 | { 207 | $request = new self($method, $path); 208 | 209 | if (function_exists('getallheaders')) { 210 | $request->headers = getallheaders(); 211 | } else { 212 | // TODO: filter array keys by their prefix, consider adding an extension function for this. 213 | $request->headers = self::createHeadersFromServerGlobal($env); 214 | } 215 | 216 | if (isset($env['_SERVER'])) { 217 | $request->serverParameters = $env['_SERVER']; 218 | } else { 219 | $request->serverParameters = $env; 220 | } 221 | $request->parameters = isset($env['_REQUEST']) ? $env['_REQUEST'] : array(); 222 | $request->queryParameters = isset($env['_GET']) ? $env['_GET'] : array(); 223 | $request->bodyParameters = isset($env['_POST']) ? $env['_POST'] : array(); 224 | $request->cookieParameters = isset($env['_COOKIE']) ? $env['_COOKIE'] : array(); 225 | $request->sessionParameters = isset($env['_SESSION']) ? $env['_SESSION'] : array(); 226 | 227 | return $request; 228 | } 229 | 230 | /** 231 | * Create request object from global variables. 232 | * 233 | * @param array $env 234 | * @return RouteRequest 235 | */ 236 | public static function createFromEnv(array $env) 237 | { 238 | // cache 239 | if (isset($env['__request_object'])) { 240 | return $env['__request_object']; 241 | } 242 | 243 | if (isset($env['PATH_INFO'])) { 244 | $path = $env['PATH_INFO']; 245 | } elseif (isset($env['REQUEST_URI'])) { 246 | $path = $env['REQUEST_URI']; 247 | } elseif (isset($env['_SERVER']['PATH_INFO'])) { 248 | $path = $env['_SERVER']['PATH_INFO']; 249 | } elseif (isset($env['_SERVER']['REQUEST_URI'])) { 250 | $path = $env['_SERVER']['REQUEST_URI']; 251 | } else { 252 | // XXX: check path or throw exception 253 | $path = '/'; 254 | } 255 | 256 | $requestMethod = 'GET'; 257 | if (isset($env['REQUEST_METHOD'])) { 258 | $requestMethod = $env['REQUEST_METHOD']; 259 | } else if (isset($env['_SERVER']['REQUEST_METHOD'])) { // compatibility for superglobal 260 | $requestMethod = $env['_SERVER']['REQUEST_METHOD']; 261 | } 262 | 263 | // create request object with request method and path, 264 | // we can assign other parameters later. 265 | $request = new self($requestMethod, $path); 266 | if (function_exists('getallheaders')) { 267 | $request->headers = getallheaders(); 268 | } else { 269 | // TODO: filter array keys by their prefix, consider adding an extension function for this. 270 | $request->headers = self::createHeadersFromServerGlobal($env); 271 | } 272 | 273 | if (isset($env['_SERVER'])) { 274 | $request->serverParameters = $env['_SERVER']; 275 | } else { 276 | $request->serverParameters = $env; 277 | } 278 | $request->parameters = $env['_REQUEST']; 279 | $request->queryParameters = $env['_GET']; 280 | $request->bodyParameters = $env['_POST']; 281 | $request->cookieParameters = $env['_COOKIE']; 282 | $request->sessionParameters = $env['_SESSION']; 283 | 284 | return $env['__request_object'] = $request; 285 | } 286 | } 287 | -------------------------------------------------------------------------------- /src/Responder/SAPIResponder.php: -------------------------------------------------------------------------------- 1 | resource = $resource; 24 | } 25 | 26 | public function respondWithString($response) 27 | { 28 | http_response_code(200); 29 | fwrite($this->resource, $response); 30 | } 31 | 32 | public function respond($response) 33 | { 34 | // treat string format response as 200 OK 35 | if (is_string($response)) { 36 | // http_response_code is only available after 5.4 37 | http_response_code(200); 38 | fwrite($this->resource, $response); 39 | } else if (is_array($response)) { 40 | list($code, $headers, $body) = $response; 41 | http_response_code($code); 42 | foreach ($headers as $k => $header) { 43 | if (is_numeric($k)) { 44 | if (is_string($header)) { 45 | @header($header); 46 | } else if (is_array($header)) { 47 | // support for [ 'Content-Type' => 'text/html' ] 48 | foreach ($header as $field => $fieldValue) { 49 | // TODO: escape field value correctly 50 | @header($field . ':' . $fieldValue); 51 | } 52 | } else { 53 | throw new RuntimeException('Unexpected header value type.'); 54 | } 55 | } else { 56 | @header($k . ':' . $header); 57 | } 58 | } 59 | 60 | if (is_array($body)) { 61 | fwrite($this->resource, join("",$body)); 62 | } else { 63 | fwrite($this->resource, $body); 64 | } 65 | } else { 66 | // FIXME 67 | // throw new LogicException("Unsupported response value type."); 68 | } 69 | } 70 | } 71 | 72 | -------------------------------------------------------------------------------- /src/Responder/SAPIResponderTest.php: -------------------------------------------------------------------------------- 1 | respond('Hello World'); 15 | 16 | rewind($fd); 17 | $s = fgets($fd); 18 | $this->assertEquals('Hello World', $s); 19 | fclose($fd); 20 | } 21 | 22 | public function testArrayResponse() 23 | { 24 | $fd = fopen('php://memory', 'r+'); 25 | $responder = new SAPIResponder($fd); 26 | $responder->respond([ 200, [ 'Content-Type: text/plain' ], 'Hello World' ]); 27 | 28 | rewind($fd); 29 | $s = fgets($fd); 30 | $this->assertEquals('Hello World', $s); 31 | 32 | fclose($fd); 33 | } 34 | 35 | public function testHeaderListResponse() 36 | { 37 | $fd = fopen('php://memory', 'r+'); 38 | $responder = new SAPIResponder($fd); 39 | $responder->respond([ 200, [ 'Content-Type: text/plain', 'X-Foo: Bar', ['X-Bar' => 'Zoo'] ], 'Hello World' ]); 40 | 41 | rewind($fd); 42 | $s = fgets($fd); 43 | $this->assertEquals('Hello World', $s); 44 | 45 | fclose($fd); 46 | } 47 | 48 | public function testHeaderAssocArrayResponse() 49 | { 50 | $fd = fopen('php://memory', 'r+'); 51 | $responder = new SAPIResponder($fd); 52 | $responder->respond([ 200, [ 'Content-Type' => 'text/plain', 'X-Foo' => 'Bar', ['X-Bar' => 'Zoo'] ], 'Hello World' ]); 53 | 54 | rewind($fd); 55 | $s = fgets($fd); 56 | $this->assertEquals('Hello World', $s); 57 | 58 | fclose($fd); 59 | } 60 | } 61 | 62 | -------------------------------------------------------------------------------- /src/Server/BaseServer.php: -------------------------------------------------------------------------------- 1 | app = $app; 16 | $this->address = $address; 17 | $this->port = intval($port); 18 | } 19 | 20 | 21 | abstract public function listen(); 22 | 23 | 24 | } 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/Server/EventHttpServer.php: -------------------------------------------------------------------------------- 1 | getUri(), PHP_EOL; 24 | 25 | $headers = $request->getInputHeaders(); 26 | var_dump($headers); 27 | 28 | $conn = $request->getConnection(); 29 | // $conn->setCloseCallback([$this, 'requestCloseHandler'], NULL); 30 | 31 | /* 32 | By enabling Event::READ we protect the server against unclosed conections. 33 | This is a peculiarity of Libevent. The library disables Event::READ events 34 | on this connection, and the server is not notified about terminated 35 | connections. 36 | 37 | So each time client terminates connection abruptly, we get an orphaned 38 | connection. For instance, the following is a part of `lsof -p $PID | grep TCP` 39 | command after client has terminated connection: 40 | 41 | 57-php 15057 ruslan 6u unix 0xffff8802fb59c780 0t0 125187 socket 42 | 58:php 15057 ruslan 7u IPv4 125189 0t0 TCP *:4242 (LISTEN) 43 | 59:php 15057 ruslan 8u IPv4 124342 0t0 TCP localhost:4242->localhost:37375 (CLOSE_WAIT) 44 | 45 | where $PID is our process ID. 46 | 47 | The following block of code fixes such kind of orphaned connections. 48 | */ 49 | $bev = $request->getBufferEvent(); 50 | $bev->enable(Event::READ); 51 | 52 | // $request->addHeader('Content-Type', 'text/plain', EventHttpRequest::OUTPUT_HEADER); 53 | 54 | /* 55 | $request->addHeader('Content-Type', 56 | 'multipart/x-mixed-replace;boundary=boundarydonotcross', 57 | EventHttpRequest::OUTPUT_HEADER); 58 | */ 59 | 60 | $buf = new EventBuffer(); 61 | $buf->add("Hello World\r\n"); 62 | 63 | $request->sendReply(200, "OK"); 64 | $request->sendReplyChunk($buf); 65 | $request->sendReplyEnd(); 66 | 67 | // $request->closeConnection(); 68 | } 69 | 70 | 71 | /** 72 | * Start listening 73 | */ 74 | public function listen() 75 | { 76 | echo "Starting server at http://{$this->address}:{$this->port}...\n"; 77 | $base = new EventBase(); 78 | $http = new EventHttp($base); 79 | $http->setDefaultCallback([$this, 'handleRequest'], NULL); 80 | $ret = $http->bind($this->address, $this->port); 81 | if ($ret === false) { 82 | throw new Exception("EventHttp::bind failed on {$this->address}:{$this->port}"); 83 | } 84 | $base->loop(); 85 | } 86 | 87 | 88 | 89 | } 90 | 91 | 92 | 93 | 94 | 95 | -------------------------------------------------------------------------------- /src/Server/StreamSocketServer.php: -------------------------------------------------------------------------------- 1 | address}:{$this->port}...\n"; 15 | 16 | $this->socket = stream_socket_server($this->address . ':' . $this->port, $errNo, $errStr); 17 | if (!$this->socket) { 18 | throw new Exception("Can't connect socket: [$errNo] $errStr"); 19 | } 20 | $this->connections[] = $this->socket; 21 | 22 | 23 | while (1) { 24 | echo "connections:"; 25 | var_dump( $this->connections ); 26 | 27 | $reads = $this->connections; 28 | $writes = NULL; 29 | $excepts = NULL; 30 | $modified = stream_select($reads, $writes, $excepts, 5); 31 | if ($modified === false) { 32 | break; 33 | } 34 | 35 | echo "modified fd:"; 36 | var_dump( $modified ); 37 | 38 | 39 | echo "reads:"; 40 | var_dump( $reads ); 41 | 42 | foreach ($reads as $modifiedRead) { 43 | 44 | if ($modifiedRead === $this->socket) { 45 | $conn = stream_socket_accept($this->socket); 46 | fwrite($conn, "Hello! The time is ".date("n/j/Y g:i a")."\n"); 47 | $this->connections[] = $conn; 48 | } else { 49 | $data = fread($modifiedRead, 1024); 50 | var_dump($data); 51 | 52 | if (strlen($data) === 0) { // connection closed 53 | $idx = array_search($modifiedRead, $this->connections, TRUE); 54 | fclose($modifiedRead); 55 | if ($idx != -1) { 56 | unset($this->connections[$idx]); 57 | } 58 | } else if ($data === FALSE) { 59 | echo "Something bad happened"; 60 | $idx = array_search($modifiedRead, $this->connections, TRUE); 61 | if ($idx != -1) { 62 | unset($this->connections[$idx]); 63 | } 64 | } else { 65 | echo "The client has sent :"; var_dump($data); 66 | fwrite($modifiedRead, "You have sent :[".$data."]\n"); 67 | fclose($modifiedRead); 68 | 69 | $idx = array_search($modifiedRead, $this->connections, TRUE); 70 | if ($idx != -1) { 71 | unset($this->connections[$idx]); 72 | } 73 | } 74 | } 75 | 76 | } 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/Testing/TestUtils.php: -------------------------------------------------------------------------------- 1 | $method, 11 | 'PATH_INFO' => $pathInfo, 12 | ]; 13 | $env['_POST'] = array(); 14 | $env['_REQUEST'] = array(); 15 | $env['_GET'] = array(); 16 | $env['_COOKIE'] = array(); 17 | $env['_SESSION'] = array(); 18 | // fallback (backware compatible for $GLOBALS) 19 | $env['_SERVER'] = array(); 20 | return $env; 21 | } 22 | 23 | 24 | static public function createEnvFromGlobals(array $globals) 25 | { 26 | $env = $globals['_SERVER']; 27 | $env['_REQUEST'] = $env['pux.parameters'] = $globals['_REQUEST']; 28 | $env['_POST'] = $env['pux.body_parameters'] = $globals['_POST']; 29 | $env['_GET'] = $env['pux.query_parameters'] = $globals['_GET']; 30 | $env['_COOKIE'] = $env['pux.cookies'] = $globals['_COOKIE']; 31 | $env['_SESSION'] = $env['pux.session'] = $globals['_SESSION']; 32 | return $env; 33 | } 34 | 35 | /** 36 | * createGlobals helps you define a global object for testing 37 | */ 38 | static public function createGlobals($method, $pathInfo) 39 | { 40 | return [ 41 | '_SERVER' => [ 42 | 'REQUEST_METHOD' => $method, 43 | 'PATH_INFO' => $pathInfo, 44 | ], 45 | '_REQUEST' => [ ], 46 | '_POST' => [ ], 47 | '_GET' => [ ], 48 | '_ENV' => [ ], 49 | '_COOKIE' => [], 50 | '_SESSION' => [], 51 | ]; 52 | } 53 | } 54 | 55 | 56 | -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | > ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini 4 | fi 5 | # if [[ $(phpenv version-name) =~ 5.5 ]] ; then 6 | # # echo yes | pecl install -f APCu-beta 7 | # # echo "extension=apcu.so" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini 8 | # fi 9 | --------------------------------------------------------------------------------