├── .github └── FUNDING.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── composer.json ├── phpunit.xml.dist ├── src ├── CallableRunner.php ├── LaravelRunner.php ├── Runtime.php ├── ServerFactory.php ├── SymfonyHttpBridge.php └── SymfonyRunner.php └── tests ├── E2E ├── StaticFileHandlerTest.php ├── runtime.php └── static │ └── file.txt └── Unit ├── CallableRunnerTest.php ├── LaravelRunnerTest.php ├── RuntimeTest.php ├── ServerFactoryTest.php ├── SymfonyHttpBridgeTest.php └── SymfonyRunnerTest.php /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [nyholm] 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /composer.lock 2 | /phpunit.xml 3 | /vendor/ 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## 0.4.0 4 | 5 | - Add support for Symfony 7 6 | - Drop support for PHP 7 and PHP 8.0 7 | 8 | ## 0.3.1 9 | 10 | ### Fixed 11 | 12 | - Buffer each StreamedResponse chunk when sending data to swoole worker output 13 | 14 | ## 0.3.0 15 | 16 | ### Added 17 | 18 | - Support for kernel.terminate Symfony event 19 | - Binary file and streamed response 20 | 21 | ### Fixed 22 | 23 | - Overwritten headers in Swoole response 24 | 25 | ## 0.2.1 26 | 27 | ### Added 28 | 29 | - Option for SWOOLE_SOCK_TCP 30 | 31 | ### Fixed 32 | 33 | - Response status code 34 | - Session handling for Symfony 5.4 and up. 35 | 36 | ## 0.2.0 37 | 38 | ### Added 39 | 40 | - Support for Swoole's Server mode 41 | - Support for more settings 42 | 43 | ## 0.1.1 44 | 45 | ### Fixed 46 | 47 | - Fixed case of SERVER variables and added request headers 48 | 49 | ### Added 50 | 51 | - Add support for Symfony 6 52 | 53 | ## 0.1.0 54 | 55 | First version 56 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2021 Tobias Nyholm 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Swoole Runtime 2 | 3 | A runtime for [Swoole](https://www.swoole.com/). 4 | 5 | If you are new to the Symfony Runtime component, read more in the [main readme](https://github.com/php-runtime/runtime). 6 | 7 | ## Installation 8 | 9 | ``` 10 | composer require runtime/swoole 11 | ``` 12 | 13 | ## Usage 14 | 15 | Define the environment variable `APP_RUNTIME` for your application. 16 | 17 | ``` 18 | APP_RUNTIME=Runtime\Swoole\Runtime 19 | ``` 20 | 21 | ### Pure PHP 22 | 23 | ```php 24 | // public/index.php 25 | 26 | use Swoole\Http\Request; 27 | use Swoole\Http\Response; 28 | 29 | require_once dirname(__DIR__) . '/vendor/autoload_runtime.php'; 30 | 31 | return function () { 32 | return function (Request $request, Response $response) { 33 | $response->header("Content-Type", "text/plain"); 34 | $response->end("Hello World\n"); 35 | }; 36 | }; 37 | ``` 38 | 39 | ### Symfony 40 | 41 | ```php 42 | // public/index.php 43 | 44 | use App\Kernel; 45 | 46 | require_once dirname(__DIR__) . '/vendor/autoload_runtime.php'; 47 | 48 | return function (array $context) { 49 | return new Kernel($context['APP_ENV'], (bool) $context['APP_DEBUG']); 50 | }; 51 | ``` 52 | 53 | ## Using Options 54 | 55 | You can define some configurations using Symfony's Runtime [`APP_RUNTIME_OPTIONS` API](https://symfony.com/doc/current/components/runtime.html#using-options). 56 | 57 | | Option | Description | Default | 58 | | --- | --- | --- | 59 | | `host` | The host where the server should binds to (precedes `SWOOLE_HOST` environment variable) | `127.0.0.1` | 60 | | `port` | The port where the server should be listing (precedes `SWOOLE_PORT` environment variable) | `8000` | 61 | | `mode` | Swoole's server mode (precedes `SWOOLE_MODE` environment variable) | `SWOOLE_PROCESS` | 62 | | `settings` | All Swoole's server settings ([wiki.swoole.com/en/#/server/setting](https://wiki.swoole.com/en/#/server/setting) and [wiki.swoole.com/en/#/http_server?id=configuration-options](https://wiki.swoole.com/en/#/http_server?id=configuration-options)) | `[]` | 63 | 64 | ```php 65 | // public/index.php 66 | 67 | use App\Kernel; 68 | 69 | $_SERVER['APP_RUNTIME_OPTIONS'] = [ 70 | 'host' => '0.0.0.0', 71 | 'port' => 9501, 72 | 'mode' => SWOOLE_BASE, 73 | 'settings' => [ 74 | 'worker_num' => swoole_cpu_num() * 2, 75 | 'enable_static_handler' => true, 76 | 'document_root' => dirname(__DIR__) . '/public' 77 | ], 78 | ]; 79 | 80 | require_once dirname(__DIR__) . '/vendor/autoload_runtime.php'; 81 | 82 | return function (array $context) { 83 | return new Kernel($context['APP_ENV'], (bool) $context['APP_DEBUG']); 84 | }; 85 | ``` 86 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "runtime/swoole", 3 | "description": "Swoole runtime", 4 | "license": "MIT", 5 | "type": "library", 6 | "authors": [ 7 | { 8 | "name": "Tobias Nyholm", 9 | "email": "tobias.nyholm@gmail.com" 10 | } 11 | ], 12 | "require": { 13 | "php": ">=8.1", 14 | "symfony/runtime": "^5.4.26 || ^6.3.2 || ^7.0" 15 | }, 16 | "require-dev": { 17 | "illuminate/http": "^9.14", 18 | "phpunit/phpunit": "^9.6.15", 19 | "swoole/ide-helper": "^4.6", 20 | "symfony/http-foundation": "^5.4.32 || ^6.3.9 || ^7.0", 21 | "symfony/http-kernel": "^5.4.33 || ^6.3.10 || ^7.0" 22 | }, 23 | "conflict": { 24 | "ext-swoole": "<4.6.0" 25 | }, 26 | "minimum-stability": "dev", 27 | "prefer-stable": true, 28 | "autoload": { 29 | "psr-4": { 30 | "Runtime\\Swoole\\": "src/" 31 | } 32 | }, 33 | "autoload-dev": { 34 | "psr-4": { 35 | "Runtime\\Swoole\\Tests\\": "tests/" 36 | } 37 | }, 38 | "config": { 39 | "allow-plugins": { 40 | "symfony/runtime": true 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 12 | 13 | 14 | 15 | ./tests/Unit 16 | 17 | 18 | 19 | 20 | src 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/CallableRunner.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | class CallableRunner implements RunnerInterface 13 | { 14 | /** @var ServerFactory */ 15 | private $serverFactory; 16 | /** @var callable */ 17 | private $application; 18 | 19 | public function __construct(ServerFactory $serverFactory, callable $application) 20 | { 21 | $this->serverFactory = $serverFactory; 22 | $this->application = $application; 23 | } 24 | 25 | public function run(): int 26 | { 27 | $this->serverFactory->createServer($this->application)->start(); 28 | 29 | return 0; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/LaravelRunner.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | class LaravelRunner implements RunnerInterface 17 | { 18 | /** @var ServerFactory */ 19 | private $serverFactory; 20 | /** @var Kernel */ 21 | private $application; 22 | 23 | public function __construct(ServerFactory $serverFactory, Kernel $application) 24 | { 25 | $this->serverFactory = $serverFactory; 26 | $this->application = $application; 27 | } 28 | 29 | public function run(): int 30 | { 31 | $this->serverFactory->createServer([$this, 'handle'])->start(); 32 | 33 | return 0; 34 | } 35 | 36 | public function handle(Request $request, Response $response): void 37 | { 38 | $sfRequest = LaravelRequest::createFromBase(SymfonyHttpBridge::convertSwooleRequest($request)); 39 | 40 | $sfResponse = $this->application->handle($sfRequest); 41 | SymfonyHttpBridge::reflectSymfonyResponse($sfResponse, $response); 42 | 43 | $this->application->terminate($sfRequest, $sfResponse); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Runtime.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | class Runtime extends SymfonyRuntime 16 | { 17 | /** @var ?ServerFactory */ 18 | private $serverFactory; 19 | 20 | public function __construct(array $options, ?ServerFactory $serverFactory = null) 21 | { 22 | $this->serverFactory = $serverFactory ?? new ServerFactory($options); 23 | parent::__construct($this->serverFactory->getOptions()); 24 | } 25 | 26 | public function getRunner(?object $application): RunnerInterface 27 | { 28 | if (is_callable($application)) { 29 | return new CallableRunner($this->serverFactory, $application); 30 | } 31 | 32 | if ($application instanceof HttpKernelInterface) { 33 | return new SymfonyRunner($this->serverFactory, $application); 34 | } 35 | 36 | if ($application instanceof Kernel) { 37 | return new LaravelRunner($this->serverFactory, $application); 38 | } 39 | 40 | return parent::getRunner($application); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/ServerFactory.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | class ServerFactory 13 | { 14 | private const DEFAULT_OPTIONS = [ 15 | 'host' => '127.0.0.1', 16 | 'port' => 8000, 17 | 'mode' => 2, // SWOOLE_PROCESS 18 | 'sock_type' => 1, // SWOOLE_SOCK_TCP 19 | 'settings' => [], 20 | ]; 21 | 22 | /** @var array */ 23 | private $options; 24 | 25 | public static function getDefaultOptions(): array 26 | { 27 | return self::DEFAULT_OPTIONS; 28 | } 29 | 30 | public function __construct(array $options = []) 31 | { 32 | $options['host'] = $options['host'] ?? $_SERVER['SWOOLE_HOST'] ?? $_ENV['SWOOLE_HOST'] ?? self::DEFAULT_OPTIONS['host']; 33 | $options['port'] = $options['port'] ?? $_SERVER['SWOOLE_PORT'] ?? $_ENV['SWOOLE_PORT'] ?? self::DEFAULT_OPTIONS['port']; 34 | $options['mode'] = $options['mode'] ?? $_SERVER['SWOOLE_MODE'] ?? $_ENV['SWOOLE_MODE'] ?? self::DEFAULT_OPTIONS['mode']; 35 | $options['sock_type'] = $options['sock_type'] ?? $_SERVER['SWOOLE_SOCK_TYPE'] ?? $_ENV['SWOOLE_SOCK_TYPE'] ?? self::DEFAULT_OPTIONS['sock_type']; 36 | 37 | $this->options = array_replace_recursive(self::DEFAULT_OPTIONS, $options); 38 | } 39 | 40 | public function createServer(callable $requestHandler): Server 41 | { 42 | $server = new Server($this->options['host'], (int) $this->options['port'], (int) $this->options['mode'], (int) $this->options['sock_type']); 43 | $server->set($this->options['settings']); 44 | $server->on('request', $requestHandler); 45 | 46 | return $server; 47 | } 48 | 49 | public function getOptions(): array 50 | { 51 | return $this->options; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/SymfonyHttpBridge.php: -------------------------------------------------------------------------------- 1 | 17 | * 18 | * @internal 19 | */ 20 | final class SymfonyHttpBridge 21 | { 22 | public static function convertSwooleRequest(Request $request): SymfonyRequest 23 | { 24 | $sfRequest = new SymfonyRequest( 25 | $request->get ?? [], 26 | $request->post ?? [], 27 | [], 28 | $request->cookie ?? [], 29 | $request->files ?? [], 30 | array_change_key_case($request->server ?? [], CASE_UPPER), 31 | $request->rawContent() 32 | ); 33 | $sfRequest->headers = new HeaderBag($request->header ?? []); 34 | 35 | return $sfRequest; 36 | } 37 | 38 | public static function reflectSymfonyResponse(SymfonyResponse $sfResponse, Response $response): void 39 | { 40 | foreach ($sfResponse->headers->all() as $name => $values) { 41 | foreach ((array) $values as $value) { 42 | $response->header((string) $name, $value); 43 | } 44 | } 45 | 46 | $response->status($sfResponse->getStatusCode()); 47 | 48 | switch (true) { 49 | case $sfResponse instanceof BinaryFileResponse && $sfResponse->headers->has('Content-Range'): 50 | case $sfResponse instanceof StreamedResponse: 51 | ob_start(function ($buffer) use ($response) { 52 | $response->write($buffer); 53 | 54 | return ''; 55 | }, 4096); 56 | $sfResponse->sendContent(); 57 | ob_end_clean(); 58 | $response->end(); 59 | break; 60 | case $sfResponse instanceof BinaryFileResponse: 61 | $response->sendfile($sfResponse->getFile()->getPathname()); 62 | break; 63 | default: 64 | $response->end($sfResponse->getContent()); 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/SymfonyRunner.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | class SymfonyRunner implements RunnerInterface 17 | { 18 | /** @var ServerFactory */ 19 | private $serverFactory; 20 | /** @var HttpKernelInterface */ 21 | private $application; 22 | 23 | public function __construct(ServerFactory $serverFactory, HttpKernelInterface $application) 24 | { 25 | $this->serverFactory = $serverFactory; 26 | $this->application = $application; 27 | } 28 | 29 | public function run(): int 30 | { 31 | $this->serverFactory->createServer([$this, 'handle'])->start(); 32 | 33 | return 0; 34 | } 35 | 36 | public function handle(Request $request, Response $response): void 37 | { 38 | $sfRequest = SymfonyHttpBridge::convertSwooleRequest($request); 39 | 40 | $sfResponse = $this->application->handle($sfRequest); 41 | SymfonyHttpBridge::reflectSymfonyResponse($sfResponse, $response); 42 | 43 | if ($this->application instanceof TerminableInterface) { 44 | $this->application->terminate($sfRequest, $sfResponse); 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /tests/E2E/StaticFileHandlerTest.php: -------------------------------------------------------------------------------- 1 | getBody()); 16 | }); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /tests/E2E/runtime.php: -------------------------------------------------------------------------------- 1 | 8001, 14 | 'mode' => SWOOLE_BASE, 15 | 'settings' => [ 16 | Constant::OPTION_WORKER_NUM => 1, 17 | Constant::OPTION_ENABLE_STATIC_HANDLER => true, 18 | Constant::OPTION_DOCUMENT_ROOT => __DIR__.'/static', 19 | ], 20 | ]; 21 | 22 | $runtime = new Runtime($options); 23 | 24 | $app = static function (Request $request, Response $response): void { 25 | $response->end(); 26 | }; 27 | 28 | $runner = $runtime->getRunner($app); 29 | 30 | echo "Exec: phpunit --testsuit E2E\n"; 31 | $runner->run(); 32 | -------------------------------------------------------------------------------- /tests/E2E/static/file.txt: -------------------------------------------------------------------------------- 1 | Static file 2 | -------------------------------------------------------------------------------- /tests/Unit/CallableRunnerTest.php: -------------------------------------------------------------------------------- 1 | createMock(Server::class); 20 | $server->expects(self::once())->method('start'); 21 | 22 | $factory = $this->createMock(ServerFactory::class); 23 | $factory->expects(self::once())->method('createServer')->with(self::equalTo($application))->willReturn($server); 24 | 25 | $runner = new CallableRunner($factory, $application); 26 | 27 | self::assertSame(0, $runner->run()); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /tests/Unit/LaravelRunnerTest.php: -------------------------------------------------------------------------------- 1 | createMock(Kernel::class); 21 | 22 | $server = $this->createMock(Server::class); 23 | $server->expects(self::once())->method('start'); 24 | 25 | $factory = $this->createMock(ServerFactory::class); 26 | $factory->expects(self::once())->method('createServer')->willReturn($server); 27 | 28 | $runner = new LaravelRunner($factory, $application); 29 | 30 | self::assertSame(0, $runner->run()); 31 | } 32 | 33 | public function testHandle(): void 34 | { 35 | $sfResponse = new SymfonyResponse('foo'); 36 | 37 | $application = $this->createMock(Kernel::class); 38 | $application->expects(self::once())->method('handle')->willReturn($sfResponse); 39 | 40 | $response = $this->createMock(Response::class); 41 | $response->expects(self::once())->method('end')->with('foo'); 42 | 43 | $request = $this->createMock(Request::class); 44 | $factory = $this->createMock(ServerFactory::class); 45 | 46 | $runner = new LaravelRunner($factory, $application); 47 | $runner->handle($request, $response); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /tests/Unit/RuntimeTest.php: -------------------------------------------------------------------------------- 1 | false]; 19 | $runtime = new Runtime($options); 20 | 21 | $application = static function (): void { 22 | }; 23 | $runner = $runtime->getRunner($application); 24 | 25 | self::assertInstanceOf(CallableRunner::class, $runner); 26 | } 27 | 28 | public function testGetRunnerCreatesARunnerForSymfony(): void 29 | { 30 | $options = ['error_handler' => false]; 31 | $runtime = new Runtime($options); 32 | 33 | $application = $this->createMock(HttpKernelInterface::class); 34 | $runner = $runtime->getRunner($application); 35 | 36 | self::assertInstanceOf(SymfonyRunner::class, $runner); 37 | } 38 | 39 | public function testGetRunnerCreatesARunnerForLaravel(): void 40 | { 41 | $options = ['error_handler' => false]; 42 | $runtime = new Runtime($options); 43 | 44 | $application = $this->createMock(Kernel::class); 45 | $runner = $runtime->getRunner($application); 46 | 47 | self::assertInstanceOf(LaravelRunner::class, $runner); 48 | } 49 | 50 | public function testGetRunnerFallbacksToClosureRunner(): void 51 | { 52 | $options = ['error_handler' => false]; 53 | $runtime = new Runtime($options); 54 | 55 | $runner = $runtime->getRunner(null); 56 | 57 | self::assertInstanceOf(ClosureRunner::class, $runner); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /tests/Unit/ServerFactoryTest.php: -------------------------------------------------------------------------------- 1 | createServer(static function (): void { 17 | }); 18 | $defaults = ServerFactory::getDefaultOptions(); 19 | 20 | self::assertSame($defaults['host'], $server->host); 21 | self::assertSame($defaults['port'], $server->port); 22 | self::assertSame($defaults['mode'], $server->mode); 23 | self::assertSame($defaults['sock_type'], $server->type); 24 | self::assertSame($defaults['settings'], $server->setting); 25 | } 26 | 27 | public function testCreateServerWithGivenOptions(): void 28 | { 29 | $options = [ 30 | 'host' => '0.0.0.0', 31 | 'port' => 9501, 32 | 'mode' => 1, 33 | 'sock_type' => 2, 34 | 'settings' => [ 35 | 'worker_num' => 1, 36 | ], 37 | ]; 38 | 39 | $factory = new ServerFactory($options); 40 | $server = $factory->createServer(static function (): void { 41 | }); 42 | 43 | self::assertSame('0.0.0.0', $server->host); 44 | self::assertSame(9501, $server->port); 45 | self::assertSame(1, $server->mode); 46 | self::assertSame(2, $server->type); 47 | self::assertSame(['worker_num' => 1], $server->setting); 48 | } 49 | 50 | public function testCreateServerWithPartialOptionsOverride(): void 51 | { 52 | $options = [ 53 | 'mode' => 1, 54 | 'sock_type' => 2, 55 | 'settings' => [ 56 | 'worker_num' => 1, 57 | ], 58 | ]; 59 | 60 | $factory = new ServerFactory($options); 61 | $server = $factory->createServer(static function (): void { 62 | }); 63 | $defaults = ServerFactory::getDefaultOptions(); 64 | 65 | self::assertSame($defaults['host'], $server->host); 66 | self::assertSame($defaults['port'], $server->port); 67 | self::assertSame(1, $server->mode); 68 | self::assertSame(2, $server->type); 69 | self::assertSame(['worker_num' => 1], $server->setting); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /tests/Unit/SymfonyHttpBridgeTest.php: -------------------------------------------------------------------------------- 1 | 19 | */ 20 | class SymfonyHttpBridgeTest extends TestCase 21 | { 22 | public function testThatSwooleRequestIsConverted(): void 23 | { 24 | $request = $this->createMock(Request::class); 25 | $request->server = ['request_method' => 'post']; 26 | $request->header = ['content-type' => 'application/json']; 27 | $request->cookie = ['foo' => 'cookie']; 28 | $request->get = ['foo' => 'get']; 29 | $request->post = ['foo' => 'post']; 30 | $request->files = [ 31 | 'foo' => [ 32 | 'name' => 'file', 33 | 'type' => 'image/png', 34 | 'tmp_name' => '/tmp/file', 35 | 'error' => UPLOAD_ERR_CANT_WRITE, 36 | 'size' => 0, 37 | ], 38 | ]; 39 | $request->expects(self::once())->method('rawContent')->willReturn('{"foo": "body"}'); 40 | 41 | $sfRequest = SymfonyHttpBridge::convertSwooleRequest($request); 42 | 43 | $this->assertSame(['REQUEST_METHOD' => 'post'], $sfRequest->server->all()); 44 | $this->assertSame(['content-type' => ['application/json']], $sfRequest->headers->all()); 45 | $this->assertSame(['foo' => 'cookie'], $sfRequest->cookies->all()); 46 | $this->assertSame(['foo' => 'get'], $sfRequest->query->all()); 47 | $this->assertSame(['foo' => 'post'], $sfRequest->request->all()); 48 | $this->assertEquals('{"foo": "body"}', $sfRequest->getContent()); 49 | 50 | $this->assertCount(1, $sfRequest->files); 51 | 52 | /** @var UploadedFile $file */ 53 | $file = $sfRequest->files->get('foo'); 54 | $this->assertNotNull($file); 55 | $this->assertEquals('file', $file->getClientOriginalName()); 56 | $this->assertEquals('image/png', $file->getClientMimeType()); 57 | $this->assertEquals('/tmp/file', $file->getPathname()); 58 | $this->assertEquals(UPLOAD_ERR_CANT_WRITE, $file->getError()); 59 | } 60 | 61 | public function testThatSymfonyResponseIsReflected(): void 62 | { 63 | $fooCookie = (string) new Cookie('foo', '123'); 64 | $barCookie = (string) new Cookie('bar', '234'); 65 | 66 | $sfResponse = $this->createMock(SymfonyResponse::class); 67 | $sfResponse->headers = new HeaderBag([ 68 | 'X-Test' => 'Swoole-Runtime', 69 | 'Set-Cookie' => [$fooCookie, $barCookie], 70 | ]); 71 | $sfResponse->expects(self::once())->method('getStatusCode')->willReturn(201); 72 | $sfResponse->expects(self::once())->method('getContent')->willReturn('Test'); 73 | 74 | $response = $this->createMock(Response::class); 75 | $response->expects(self::exactly(3))->method('header')->withConsecutive( 76 | ['x-test', 'Swoole-Runtime'], 77 | ['set-cookie', $fooCookie], 78 | ['set-cookie', $barCookie] 79 | ); 80 | $response->expects(self::once())->method('status')->with(201); 81 | $response->expects(self::once())->method('end')->with('Test'); 82 | 83 | SymfonyHttpBridge::reflectSymfonyResponse($sfResponse, $response); 84 | } 85 | 86 | public function testThatSymfonyStreamedResponseIsReflected(): void 87 | { 88 | $sfResponse = new StreamedResponse(function () { 89 | echo "Foo\n"; 90 | ob_flush(); 91 | 92 | echo "Bar\n"; 93 | ob_flush(); 94 | }); 95 | 96 | $response = $this->createMock(Response::class); 97 | $response->expects(self::exactly(3))->method('write')->withConsecutive(["Foo\n"], ["Bar\n"], ['']); 98 | $response->expects(self::once())->method('end'); 99 | 100 | SymfonyHttpBridge::reflectSymfonyResponse($sfResponse, $response); 101 | } 102 | 103 | public function testThatSymfonyBinaryFileResponseIsReflected(): void 104 | { 105 | $file = tempnam(sys_get_temp_dir(), uniqid()); 106 | file_put_contents($file, 'Foo'); 107 | 108 | $sfResponse = new BinaryFileResponse($file); 109 | 110 | $response = $this->createMock(Response::class); 111 | $response->expects(self::once())->method('sendfile')->with($file, null, null); 112 | 113 | SymfonyHttpBridge::reflectSymfonyResponse($sfResponse, $response); 114 | } 115 | 116 | public function testThatSymfonyBinaryFileResponseWithRangeIsReflected(): void 117 | { 118 | $file = tempnam(sys_get_temp_dir(), uniqid()); 119 | file_put_contents($file, 'FooBar'); 120 | 121 | $request = new SymfonyRequest(); 122 | $request->headers->set('Range', 'bytes=2-4'); 123 | 124 | $sfResponse = new BinaryFileResponse($file); 125 | $sfResponse->prepare($request); 126 | 127 | $response = $this->createMock(Response::class); 128 | $response->expects(self::once())->method('write')->with('oBa'); 129 | $response->expects(self::once())->method('end'); 130 | 131 | SymfonyHttpBridge::reflectSymfonyResponse($sfResponse, $response); 132 | } 133 | 134 | public function testStreamedResponseWillRespondWithOneChunkAtATime(): void 135 | { 136 | $sfResponse = new StreamedResponse(static function () { 137 | echo str_repeat('a', 4096); 138 | echo str_repeat('b', 4095); 139 | }); 140 | 141 | $response = $this->createMock(Response::class); 142 | $response->expects(self::exactly(2)) 143 | ->method('write') 144 | ->with(self::logicalOr( 145 | str_repeat('a', 4096), 146 | str_repeat('b', 4095) 147 | )); 148 | $response->expects(self::once())->method('end'); 149 | 150 | SymfonyHttpBridge::reflectSymfonyResponse($sfResponse, $response); 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /tests/Unit/SymfonyRunnerTest.php: -------------------------------------------------------------------------------- 1 | createMock(HttpKernelInterface::class); 21 | 22 | $server = $this->createMock(Server::class); 23 | $server->expects(self::once())->method('start'); 24 | 25 | $factory = $this->createMock(ServerFactory::class); 26 | $factory->expects(self::once())->method('createServer')->willReturn($server); 27 | 28 | $runner = new SymfonyRunner($factory, $application); 29 | 30 | self::assertSame(0, $runner->run()); 31 | } 32 | 33 | public function testHandle(): void 34 | { 35 | $sfResponse = new SymfonyResponse('foo'); 36 | 37 | $application = $this->createMock(HttpKernelInterface::class); 38 | $application->expects(self::once())->method('handle')->willReturn($sfResponse); 39 | 40 | $response = $this->createMock(Response::class); 41 | $response->expects(self::once())->method('end')->with('foo'); 42 | 43 | $request = $this->createMock(Request::class); 44 | $factory = $this->createMock(ServerFactory::class); 45 | 46 | $runner = new SymfonyRunner($factory, $application); 47 | $runner->handle($request, $response); 48 | } 49 | } 50 | --------------------------------------------------------------------------------