├── .circleci └── config.yml ├── .editorconfig ├── .formatter.yml ├── .gitignore ├── .php_cs ├── CODE_OF_CONDUCT.md ├── LICENSE.md ├── README.md ├── bin └── server ├── composer.json ├── phpunit.xml ├── src ├── Adapter │ ├── KernelAdapter.php │ └── Symfony4KernelAdapter.php ├── Application.php ├── ConsoleException.php ├── ConsoleMessage.php ├── ConsoleStaticMessage.php ├── ErrorHandler.php ├── Printable.php ├── RequestHandler.php └── ServerResponseWithMessage.php └── tests ├── ApplicationStaticFolderTest.php ├── ApplicationTest.php ├── FakeAdapter.php ├── FakeKernel.php └── public └── app.js /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | build: 4 | docker: 5 | - image: circleci/php:7.1-cli 6 | 7 | working_directory: ~/project 8 | steps: 9 | - checkout 10 | - run: 11 | name: Run tests 12 | command: | 13 | composer install -n --prefer-dist --no-suggest 14 | php vendor/bin/phpunit -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | ; top-most EditorConfig file 2 | root = true 3 | 4 | ; Unix-style newlines 5 | [*] 6 | end_of_line = LF 7 | 8 | [*.php] 9 | indent_style = space 10 | indent_size = 4 -------------------------------------------------------------------------------- /.formatter.yml: -------------------------------------------------------------------------------- 1 | use-sort: 2 | group: 3 | - _main 4 | group-type: each 5 | sort-type: alph 6 | sort-direction: asc 7 | strict: true 8 | header: | 9 | /* 10 | * This file is part of the React Symfony Server package. 11 | * 12 | * For the full copyright and license information, please view the LICENSE 13 | * file that was distributed with this source code. 14 | * 15 | * Feel free to edit as you please, and have fun. 16 | * 17 | * @author Marc Morera 18 | */ -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | vendor 2 | .php_cs.cache 3 | composer.lock 4 | var -------------------------------------------------------------------------------- /.php_cs: -------------------------------------------------------------------------------- 1 | exclude('src') 5 | ->in(__DIR__) 6 | ; 7 | 8 | return PhpCsFixer\Config::create() 9 | ->setRules([ 10 | '@PSR2' => true, 11 | '@Symfony' => true, 12 | 'single_line_after_imports' => false, 13 | ]) 14 | ->setFinder($finder) 15 | ; -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at hello@puntmig.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) Marc Morera 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is furnished 8 | to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Symfony ReactPHP Server 2 | 3 | > This package has been turned a [DriftPHP project](http://driftphp.io) component. That means that has been discontinued in favor of these new packages 4 | -------------------------------------------------------------------------------- /bin/server: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | =2019 Marc Morera 8 | * 9 | * For the full copyright and license information, please view the LICENSE 10 | * file that was distributed with this source code. 11 | * 12 | * Feel free to edit as you please, and have fun. 13 | * 14 | * @author Marc Morera 15 | */ 16 | 17 | /** 18 | * Make key value on arguments. 19 | * 20 | * @param array $originalArguments 21 | * 22 | * @return array 23 | * 24 | * @throws Exception 25 | */ 26 | function buildServerArguments(array $originalArguments) 27 | { 28 | $arguments = array_slice($originalArguments, 2); 29 | $newArguments = []; 30 | foreach ($arguments as $value) { 31 | $parts = explode('=', $value, 2); 32 | $key = $parts[0]; 33 | $value = $parts[1] ?? true; 34 | $newArguments[$key] = $value; 35 | } 36 | 37 | $serverArgs = explode(':', $originalArguments[1], 2); 38 | if (2 !== count($serverArgs)) { 39 | throw new Exception('You should start the server defining a host and a port as a first argument: php/server 0.0.0.0:8000'); 40 | } 41 | 42 | list($host, $port) = $serverArgs; 43 | $newArguments['host'] = $host; 44 | $newArguments['port'] = $port; 45 | 46 | return $newArguments; 47 | } 48 | 49 | /** 50 | * Include the bootstrap file if its found 51 | * 52 | * @param string $file 53 | * 54 | * @return mixed 55 | */ 56 | function requireIfExists(string $file) 57 | { 58 | if (file_exists($file)) { 59 | return require $file; 60 | } 61 | 62 | return null; 63 | } 64 | 65 | use Apisearch\SymfonyReactServer\Adapter\KernelAdapter; 66 | use Apisearch\SymfonyReactServer\Adapter\Symfony4KernelAdapter; 67 | 68 | /** 69 | * Server. 70 | */ 71 | $arguments = buildServerArguments($argv); 72 | $rootPath = getcwd(); 73 | $environment = array_key_exists('--dev', $arguments) ? 'dev' : 'prod'; 74 | $silent = $arguments['--silent'] ?? false; 75 | $staticFolder = $arguments['--static-folder'] ?? ''; 76 | $staticFolder = isset($arguments['--no-static-folder']) ? null : $staticFolder; 77 | $debug = $arguments['--debug'] ?? false; 78 | $nonBlocking = $arguments['--non-blocking'] ?? false; 79 | $adapter = $arguments['--adapter'] ?? 'symfony4'; 80 | $bootstrap = $arguments['--bootstrap'] ?? 'symfony4'; 81 | $host = $arguments['host']; 82 | $port = $arguments['port']; 83 | 84 | $bootstrapFile = [ 85 | 'autoload' => 'vendor/autoload.php', 86 | 'symfony4' => 'config/bootstrap.php', 87 | ][$bootstrap] ?? $bootstrap; 88 | 89 | if ( 90 | !requireIfExists(__DIR__."/../$bootstrapFile") && 91 | !requireIfExists(__DIR__."/../../$bootstrapFile") && 92 | !requireIfExists(__DIR__."/../../../../$bootstrapFile") 93 | ) { 94 | die('You must define an existing kernel bootstrap file, or by an alias or my a file path' . PHP_EOL); 95 | } 96 | 97 | $adapter = [ 98 | 'symfony4' => Symfony4KernelAdapter::class, 99 | ][$adapter] ?? $adapter; 100 | 101 | if (!is_a($adapter, KernelAdapter::class, true)) { 102 | die('You must define an existing kernel adapter, or by an alias or my a namespace. This class MUST implement KernelAdapter' . PHP_EOL); 103 | } 104 | 105 | $application = new \Apisearch\SymfonyReactServer\Application( 106 | $rootPath, 107 | $host, 108 | $port, 109 | $environment, 110 | $debug, 111 | $silent, 112 | $nonBlocking, 113 | $adapter, 114 | $bootstrapFile, 115 | $staticFolder 116 | ); 117 | 118 | $application->run(); -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "apisearch-io/symfony-react-server", 3 | "description": "ReactPHP based server for Symfony Kernel", 4 | "type": "library", 5 | "license": "MIT", 6 | "authors": [ 7 | { 8 | "name": "Marc Morera", 9 | "email": "yuhu@mmoreram.com" 10 | } 11 | ], 12 | "require": { 13 | "php": ">=7.1", 14 | 15 | "symfony/http-kernel": "^4.2", 16 | "react/http": "^0.8", 17 | "react/event-loop": "^1", 18 | "react/socket": "^1", 19 | "react/promise": "^2", 20 | "react/filesystem": "*", 21 | "mmoreram/react-functions": "dev-master" 22 | }, 23 | "require-dev": { 24 | "phpunit/phpunit": "^7.0.0", 25 | "mmoreram/php-formatter": "^1.3.1", 26 | "friendsofphp/php-cs-fixer": "^2.5.0", 27 | "apisearch-io/symfony-async-http-kernel": "dev-master", 28 | "symfony/framework-bundle": "^4.2", 29 | "symfony/process": "^4.2", 30 | "symfony/config": "^4.2", 31 | "ext-fileinfo": "*" 32 | }, 33 | "suggest": { 34 | "apisearch-io/symfony-async-http-kernel": "To enable --non-blocking flag and work with Async Kernel" 35 | }, 36 | "autoload": { 37 | "psr-4": { 38 | "Apisearch\\SymfonyReactServer\\": "src" 39 | } 40 | }, 41 | "autoload-dev": { 42 | "psr-4": { 43 | "Apisearch\\SymfonyReactServer\\Tests\\": "tests" 44 | } 45 | }, 46 | "bin": [ 47 | "bin/server" 48 | ], 49 | "scripts": { 50 | "fix-code": [ 51 | "vendor/bin/php-cs-fixer fix --config=.php_cs", 52 | "vendor/bin/php-formatter f:h:f . --exclude=vendor --exclude=web --exclude=bin --exclude=var", 53 | "vendor/bin/php-formatter f:s:f . --exclude=vendor --exclude=web --exclude=bin --exclude=var", 54 | "vendor/bin/php-formatter f:u:s . --exclude=vendor --exclude=web --exclude=bin --exclude=var" 55 | ] 56 | }, 57 | "prefer-stable": true 58 | } 59 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 13 | 14 | 15 | ./tests 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/Adapter/KernelAdapter.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | 14 | declare(strict_types=1); 15 | 16 | namespace Apisearch\SymfonyReactServer\Adapter; 17 | 18 | use Symfony\Component\HttpKernel\Kernel; 19 | 20 | /** 21 | * Class KernelAdapter 22 | */ 23 | interface KernelAdapter 24 | { 25 | /** 26 | * Build kernel 27 | * 28 | * @param string $environment 29 | * @param bool $debug 30 | * 31 | * @return Kernel 32 | */ 33 | public static function buildKernel( 34 | string $environment, 35 | bool $debug 36 | ) : Kernel; 37 | 38 | /** 39 | * Get static folder by kernel 40 | * 41 | * @param Kernel $kernel 42 | * 43 | * @return string|null 44 | */ 45 | public static function getStaticFolder(Kernel $kernel) : ? string; 46 | } -------------------------------------------------------------------------------- /src/Adapter/Symfony4KernelAdapter.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | 14 | declare(strict_types=1); 15 | 16 | namespace Apisearch\SymfonyReactServer\Adapter; 17 | 18 | use App\Kernel as ApplicationKernel; 19 | use Symfony\Component\HttpKernel\Kernel; 20 | 21 | /** 22 | * Class Symfony4KernelAdapter 23 | */ 24 | class Symfony4KernelAdapter implements KernelAdapter 25 | { 26 | /** 27 | * Build kernel 28 | * 29 | * @param string $environment 30 | * @param bool $debug 31 | * 32 | * @return Kernel 33 | */ 34 | public static function buildKernel( 35 | string $environment, 36 | bool $debug 37 | ) : Kernel 38 | { 39 | return new ApplicationKernel($environment, $debug); 40 | } 41 | 42 | /** 43 | * Get static folder by kernel 44 | * 45 | * @param Kernel $kernel 46 | * 47 | * @return string|null 48 | */ 49 | public static function getStaticFolder(Kernel $kernel) : ? string 50 | { 51 | return '/public'; 52 | } 53 | } -------------------------------------------------------------------------------- /src/Application.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | 14 | declare(strict_types=1); 15 | 16 | namespace Apisearch\SymfonyReactServer; 17 | 18 | use Apisearch\SymfonyReactServer\Adapter\KernelAdapter; 19 | use Exception; 20 | use Psr\Http\Message\ServerRequestInterface; 21 | use React\EventLoop\Factory as EventLoopFactory; 22 | use React\Filesystem\Filesystem; 23 | use React\Http\Server as HttpServer; 24 | use React\Promise\Promise; 25 | use React\Socket\Server as SocketServer; 26 | use Symfony\Component\Debug\Debug; 27 | use Symfony\Component\HttpKernel\AsyncKernel; 28 | use Symfony\Component\HttpKernel\Kernel; 29 | 30 | /** 31 | * Class Application 32 | */ 33 | class Application 34 | { 35 | /** 36 | * @var string 37 | */ 38 | private $rootPath; 39 | 40 | /** 41 | * @var string 42 | */ 43 | private $host; 44 | 45 | /** 46 | * @var int 47 | */ 48 | private $port; 49 | 50 | /** 51 | * @var string 52 | */ 53 | private $environment; 54 | 55 | /** 56 | * @var bool 57 | */ 58 | private $debug; 59 | 60 | /** 61 | * @var bool 62 | */ 63 | private $silent; 64 | 65 | /** 66 | * @var bool 67 | */ 68 | private $nonBlocking; 69 | 70 | /** 71 | * @var string 72 | */ 73 | private $adapter; 74 | 75 | /** 76 | * @var string 77 | */ 78 | private $bootstrapFile; 79 | 80 | /** 81 | * @var string 82 | * 83 | * Static folder 84 | */ 85 | private $staticFolder; 86 | 87 | /** 88 | * @var Kernel 89 | * 90 | * Kernel 91 | */ 92 | private $kernel; 93 | 94 | /** 95 | * Application constructor. 96 | * 97 | * @param string $rootPath 98 | * @param string $host 99 | * @param int $port 100 | * @param string $environment 101 | * @param bool $debug 102 | * @param bool $silent 103 | * @param bool $nonBlocking 104 | * @param string $adapter 105 | * @param string $bootstrapFile 106 | * @param string|null $staticFolder 107 | * 108 | * @throws Exception 109 | */ 110 | public function __construct( 111 | string $rootPath, 112 | string $host, 113 | int $port, 114 | string $environment, 115 | bool $debug, 116 | bool $silent, 117 | bool $nonBlocking, 118 | string $adapter, 119 | string $bootstrapFile, 120 | ?string $staticFolder 121 | ) 122 | { 123 | $this->rootPath = $rootPath; 124 | $this->host = $host; 125 | $this->port = $port; 126 | $this->environment = $environment; 127 | $this->debug = $debug; 128 | $this->silent = $silent; 129 | $this->nonBlocking = $nonBlocking; 130 | $this->adapter = $adapter; 131 | $this->bootstrapFile = $bootstrapFile; 132 | 133 | ErrorHandler::handle(); 134 | if ($this->debug) { 135 | umask(0000); 136 | Debug::enable(); 137 | } 138 | 139 | /** 140 | * @var KernelAdapter $adapter 141 | */ 142 | $this->kernel = $adapter::buildKernel( 143 | $this->environment, 144 | $this->debug 145 | ); 146 | 147 | if ($this->nonBlocking && !$this->kernel instanceof AsyncKernel) { 148 | throw new Exception( 149 | 'You have configured the server to work as a non-blocking application, but you\'re using a synchronous Kernel' 150 | ); 151 | } 152 | 153 | if (!is_null($staticFolder)) { 154 | $this->staticFolder = empty($staticFolder) 155 | ? $adapter::getStaticFolder($this->kernel) 156 | : $staticFolder; 157 | 158 | if (is_string($this->staticFolder)) { 159 | $this->staticFolder = '/' . trim($this->staticFolder, '/') . '/'; 160 | } 161 | } 162 | } 163 | 164 | /** 165 | * Run 166 | */ 167 | public function run() 168 | { 169 | /** 170 | * REACT SERVER. 171 | */ 172 | $loop = EventLoopFactory::create(); 173 | $socket = new SocketServer($this->host . ':' . $this->port, $loop); 174 | $filesystem = Filesystem::create($loop); 175 | $requestHandler = new RequestHandler(); 176 | $this->kernel->boot(); 177 | 178 | if ($this->nonBlocking) { 179 | $this 180 | ->kernel 181 | ->getContainer() 182 | ->set('reactphp.event_loop', $loop); 183 | } 184 | 185 | if (!$this->silent) { 186 | $this->print(); 187 | } 188 | 189 | $http = new HttpServer( 190 | function (ServerRequestInterface $request) use ($requestHandler, $filesystem, $loop) { 191 | return new Promise(function (Callable $resolve) use ($request, $requestHandler, $filesystem, $loop) { 192 | 193 | $resolveResponseCallback = function(ServerResponseWithMessage $serverResponseWithMessage) use ($resolve) { 194 | if (!$this->silent) { 195 | $serverResponseWithMessage->printMessage(); 196 | } 197 | 198 | return $resolve($serverResponseWithMessage->getServerResponse()); 199 | }; 200 | 201 | $uriPath = $request->getUri()->getPath(); 202 | $uriPath = '/' . ltrim($uriPath, '/'); 203 | 204 | if (strpos( 205 | $uriPath, 206 | $this->staticFolder 207 | ) === 0) { 208 | $requestHandler->handleStaticResource( 209 | $loop, 210 | $filesystem, 211 | $this->rootPath, 212 | $uriPath 213 | ) 214 | ->then(function(ServerResponseWithMessage $serverResponseWithMessage) use ($resolveResponseCallback){ 215 | $resolveResponseCallback($serverResponseWithMessage); 216 | }); 217 | 218 | return; 219 | } 220 | 221 | $this->nonBlocking 222 | ? $requestHandler 223 | ->handleAsyncServerRequest($this->kernel, $request) 224 | ->then(function (ServerResponseWithMessage $serverResponseWithMessage) use ($resolveResponseCallback) { 225 | $resolveResponseCallback($serverResponseWithMessage); 226 | }) 227 | : $resolveResponseCallback($requestHandler 228 | ->handleServerRequest($this->kernel, $request) 229 | ); 230 | }); 231 | } 232 | ); 233 | 234 | $http->on('error', function (\Throwable $e) { 235 | (new ConsoleException($e, '/', 'EXC', 0))->print(); 236 | }); 237 | 238 | $http->listen($socket); 239 | $loop->run(); 240 | } 241 | 242 | /** 243 | * Print 244 | */ 245 | private function print() 246 | { 247 | if (!$this->silent) { 248 | echo PHP_EOL; 249 | echo '>' . PHP_EOL; 250 | echo '> ReactPHP Client for Symfony Kernel' . PHP_EOL; 251 | echo '> by Apisearch' . PHP_EOL; 252 | echo '>' . PHP_EOL; 253 | echo "> Host: $this->host" . PHP_EOL; 254 | echo "> Port: $this->port" . PHP_EOL; 255 | echo "> Environment: $this->environment" . PHP_EOL; 256 | echo "> Debug: " . ($this->debug ? 'enabled' : 'disabled') . PHP_EOL; 257 | echo "> Silent: disabled" . PHP_EOL; 258 | echo "> Non Blocking: " . ($this->nonBlocking ? 'enabled' : 'disabled') . PHP_EOL; 259 | echo "> Static Folder: " . (empty($this->staticFolder) ? 'disabled' : $this->staticFolder) . PHP_EOL; 260 | echo "> Adapter: $this->adapter" . PHP_EOL; 261 | echo "> Bootstrap: $this->bootstrapFile" . PHP_EOL; 262 | echo '>' . PHP_EOL . PHP_EOL; 263 | } 264 | } 265 | } 266 | -------------------------------------------------------------------------------- /src/ConsoleException.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | 14 | declare(strict_types=1); 15 | 16 | namespace Apisearch\SymfonyReactServer; 17 | 18 | use Throwable; 19 | 20 | /** 21 | * Class ConsoleException. 22 | */ 23 | class ConsoleException implements Printable 24 | { 25 | protected $url; 26 | protected $method; 27 | 28 | /** 29 | * @var Throwable 30 | * 31 | * Exception 32 | */ 33 | private $exception; 34 | 35 | /** 36 | * @var int 37 | * 38 | * Elapsed time 39 | */ 40 | protected $elapsedTime; 41 | 42 | /** 43 | * ConsoleException constructor. 44 | * 45 | * @param Throwable $exception 46 | * @param string $url 47 | * @param string $method 48 | * @param int $elapsedTime 49 | */ 50 | public function __construct( 51 | Throwable $exception, 52 | string $url, 53 | string $method, 54 | int $elapsedTime 55 | ) 56 | { 57 | $this->exception = $exception; 58 | $this->url = $url; 59 | $this->method = $method; 60 | $this->elapsedTime = $elapsedTime; 61 | } 62 | 63 | /** 64 | * Print. 65 | */ 66 | public function print() 67 | { 68 | $exception = $this->exception; 69 | $color = '31'; 70 | $method = str_pad($this->method, 6, ' '); 71 | echo "\033[01;{$color}m400\033[0m"; 72 | echo " $method $this->url "; 73 | echo "(\e[00;37m".$this->elapsedTime.' ms | '.((int) (memory_get_usage() / 1000000))." MB\e[0m)"; 74 | echo " - \e[00;37m".$exception->getMessage()."\e[0m"; 75 | echo PHP_EOL; 76 | } 77 | } -------------------------------------------------------------------------------- /src/ConsoleMessage.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | 14 | declare(strict_types=1); 15 | 16 | namespace Apisearch\SymfonyReactServer; 17 | 18 | /** 19 | * Class ConsoleMessage. 20 | */ 21 | class ConsoleMessage implements Printable 22 | { 23 | protected $url; 24 | protected $method; 25 | protected $code; 26 | protected $message; 27 | protected $elapsedTime; 28 | 29 | /** 30 | * Message constructor. 31 | * 32 | * @param string $url 33 | * @param string $method 34 | * @param int $code 35 | * @param string $message 36 | * @param int $elapsedTime 37 | */ 38 | public function __construct( 39 | string $url, 40 | string $method, 41 | int $code, 42 | string $message, 43 | int $elapsedTime 44 | ) { 45 | $this->url = $url; 46 | $this->method = $method; 47 | $this->code = $code; 48 | $this->message = $message; 49 | $this->elapsedTime = $elapsedTime; 50 | } 51 | 52 | /** 53 | * Print. 54 | */ 55 | public function print() 56 | { 57 | $method = str_pad($this->method, 6, ' '); 58 | $color = '32'; 59 | if ($this->code >= 300 && $this->code < 400) { 60 | $color = '33'; 61 | } elseif ($this->code >= 400) { 62 | $color = '31'; 63 | } 64 | 65 | echo "\033[01;{$color}m".$this->code."\033[0m"; 66 | echo " $method $this->url "; 67 | echo "(\e[00;37m".$this->elapsedTime.' ms | '.((int) (memory_get_usage() / 1000000))." MB\e[0m)"; 68 | if ($this->code >= 300) { 69 | echo " - \e[00;37m".$this->messageInMessage($this->message)."\e[0m"; 70 | } 71 | echo PHP_EOL; 72 | } 73 | 74 | /** 75 | * Find message. 76 | * 77 | * @param string $message 78 | * 79 | * @return string 80 | */ 81 | private function messageInMessage(string $message): string 82 | { 83 | $decodedMessage = json_decode($message, true); 84 | if ( 85 | is_array($decodedMessage) && 86 | isset($decodedMessage['message']) && 87 | is_string($decodedMessage['message']) 88 | ) { 89 | return $decodedMessage['message']; 90 | } 91 | 92 | return $message; 93 | } 94 | } -------------------------------------------------------------------------------- /src/ConsoleStaticMessage.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | 14 | declare(strict_types=1); 15 | 16 | namespace Apisearch\SymfonyReactServer; 17 | 18 | /** 19 | * Class ConsoleStaticMessage. 20 | */ 21 | class ConsoleStaticMessage implements Printable 22 | { 23 | protected $url; 24 | protected $elapsedTime; 25 | 26 | /** 27 | * Message constructor. 28 | * 29 | * @param string $url 30 | * @param int $elapsedTime 31 | */ 32 | public function __construct( 33 | string $url, 34 | int $elapsedTime 35 | ) { 36 | $this->url = $url; 37 | $this->elapsedTime = $elapsedTime; 38 | } 39 | 40 | /** 41 | * Print. 42 | */ 43 | public function print() 44 | { 45 | $method = str_pad('GET', 6, ' '); 46 | $color = '95'; 47 | 48 | echo "\033[01;{$color}m200\033[0m"; 49 | echo " $method $this->url "; 50 | echo "(\e[00;37m".$this->elapsedTime.' ms | '.((int) (memory_get_usage() / 1000000))." MB\e[0m)"; 51 | echo PHP_EOL; 52 | } 53 | } -------------------------------------------------------------------------------- /src/ErrorHandler.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | 14 | declare(strict_types=1); 15 | 16 | namespace Apisearch\SymfonyReactServer; 17 | 18 | /** 19 | * Class ErrorHandler. 20 | */ 21 | class ErrorHandler 22 | { 23 | /** 24 | * Handle error to exception. 25 | */ 26 | public static function handle() 27 | { 28 | set_error_handler([ErrorHandler::class, 'errorToException'], E_ALL & ~E_NOTICE & ~E_STRICT & ~E_DEPRECATED & ~E_USER_DEPRECATED); 29 | } 30 | 31 | /** 32 | * Errors to Exceptions. 33 | */ 34 | public static function errorToException($code, $message, $file, $line, $context) 35 | { 36 | throw new \Exception($message, $code); 37 | } 38 | } -------------------------------------------------------------------------------- /src/Printable.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | 14 | declare(strict_types=1); 15 | 16 | namespace Apisearch\SymfonyReactServer; 17 | 18 | /** 19 | * Interface Printable. 20 | */ 21 | interface Printable 22 | { 23 | /** 24 | * Print. 25 | */ 26 | public function print(); 27 | } -------------------------------------------------------------------------------- /src/RequestHandler.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | 14 | declare(strict_types=1); 15 | 16 | namespace Apisearch\SymfonyReactServer; 17 | 18 | /* 19 | * This file is part of the {Package name}. 20 | * 21 | * For the full copyright and license information, please view the LICENSE 22 | * file that was distributed with this source code. 23 | * 24 | * Feel free to edit as you please, and have fun. 25 | * 26 | * @author Marc Morera 27 | */ 28 | 29 | use Psr\Http\Message\ServerRequestInterface; 30 | use React\EventLoop\LoopInterface; 31 | use React\Filesystem\FilesystemInterface; 32 | use React\Promise; 33 | use React\Promise\FulfilledPromise; 34 | use React\Promise\PromiseInterface; 35 | use Symfony\Component\HttpFoundation\Request; 36 | use Symfony\Component\HttpFoundation\Response; 37 | use Symfony\Component\HttpKernel\AsyncKernel; 38 | use Symfony\Component\HttpKernel\Kernel; 39 | use Throwable; 40 | 41 | /** 42 | * Class RequestHandler 43 | */ 44 | class RequestHandler 45 | { 46 | /** 47 | * Handle server request and return response. 48 | * 49 | * Return an array of an instance of ResponseInterface and an array of 50 | * Printable instances 51 | * 52 | * @param Kernel $kernel 53 | * @param ServerRequestInterface $request 54 | * 55 | * @return ServerResponseWithMessage 56 | */ 57 | public function handleServerRequest( 58 | Kernel $kernel, 59 | ServerRequestInterface $request 60 | ): ServerResponseWithMessage 61 | { 62 | $from = microtime(true); 63 | $uriPath = $request->getUri()->getPath(); 64 | $method = $request->getMethod(); 65 | 66 | try { 67 | $symfonyRequest = $this->toSymfonyRequest( 68 | $request, 69 | $method, 70 | $uriPath 71 | ); 72 | 73 | $symfonyResponse = $kernel->handle($symfonyRequest); 74 | 75 | return $this->toServerResponse( 76 | $symfonyRequest, 77 | $symfonyResponse, 78 | $from 79 | ); 80 | } catch (Throwable $exception) { 81 | return $this->createExceptionServerResponse( 82 | $exception, 83 | $from, 84 | $uriPath, 85 | $method 86 | ); 87 | } 88 | } 89 | 90 | /** 91 | * Handle server request and return response. 92 | * 93 | * Return an array of an instance of ResponseInterface and an array of 94 | * Printable instances 95 | * 96 | * @param AsyncKernel $kernel 97 | * @param ServerRequestInterface $request 98 | * 99 | * @return PromiseInterface 100 | */ 101 | public function handleAsyncServerRequest( 102 | AsyncKernel $kernel, 103 | ServerRequestInterface $request 104 | ): PromiseInterface 105 | { 106 | $from = microtime(true); 107 | $uriPath = $request->getUri()->getPath(); 108 | $method = $request->getMethod(); 109 | 110 | return (new FulfilledPromise($from)) 111 | ->then(function() use ($request, $method, $uriPath) { 112 | return $this->toSymfonyRequest( 113 | $request, 114 | $method, 115 | $uriPath 116 | ); 117 | }) 118 | ->then(function(Request $symfonyRequest) use ($kernel) { 119 | 120 | return Promise\all( 121 | [ 122 | new FulfilledPromise($symfonyRequest), 123 | $kernel->handleAsync($symfonyRequest) 124 | ] 125 | ); 126 | }) 127 | ->then(function(array $parts) use ($kernel) { 128 | 129 | list($symfonyRequest, $symfonyResponse) = $parts; 130 | $kernel->terminate($symfonyRequest, $symfonyResponse); 131 | 132 | return $parts; 133 | }) 134 | ->then(function(array $parts) use ($request, $from) { 135 | 136 | list($symfonyRequest, $symfonyResponse) = $parts; 137 | return $this->toServerResponse( 138 | $symfonyRequest, 139 | $symfonyResponse, 140 | $from 141 | ); 142 | 143 | }, function(\Throwable $exception) use ($from, $method, $uriPath) { 144 | return $this->createExceptionServerResponse( 145 | $exception, 146 | $from, 147 | $method, 148 | $uriPath 149 | ); 150 | }); 151 | } 152 | 153 | /** 154 | * Handle static resource 155 | * 156 | * @param LoopInterface $loop 157 | * @param FilesystemInterface $filesystem 158 | * @param string $rootPath 159 | * @param string $resourcePath 160 | * 161 | * @return PromiseInterface 162 | */ 163 | public function handleStaticResource( 164 | LoopInterface $loop, 165 | FilesystemInterface $filesystem, 166 | string $rootPath, 167 | string $resourcePath 168 | ) : PromiseInterface 169 | { 170 | $from = microtime(true); 171 | 172 | $contents = $filesystem->getContents($rootPath . $resourcePath); 173 | $mimeType = \Mmoreram\React\mime_content_type($rootPath . $resourcePath, $loop); 174 | 175 | return Promise\all([$contents, $mimeType]) 176 | ->then(function ($results) use ($resourcePath, $from) { 177 | $to = microtime(true); 178 | 179 | return new ServerResponseWithMessage( 180 | new \React\Http\Response( 181 | 200, 182 | ['Content-Type' => $results[1]], 183 | $results[0] 184 | ), 185 | new ConsoleStaticMessage( 186 | $resourcePath, 187 | \intval(($to - $from) * 1000) 188 | ) 189 | ); 190 | }, function(Throwable $exception) use ($resourcePath, $from) { 191 | $to = microtime(true); 192 | 193 | return new ServerResponseWithMessage( 194 | new \React\Http\Response( 195 | 404, 196 | [], 197 | '' 198 | ), 199 | new ConsoleException( 200 | $exception, 201 | $resourcePath, 202 | 'GET', 203 | \intval(($to - $from) * 1000) 204 | ) 205 | ); 206 | }); 207 | } 208 | 209 | /** 210 | * Http request to symfony request 211 | * 212 | * @param ServerRequestInterface $request 213 | * @param string $method 214 | * @param string $uriPath 215 | * 216 | * @return Request 217 | */ 218 | private function toSymfonyRequest( 219 | ServerRequestInterface $request, 220 | string $method, 221 | string $uriPath 222 | ) : Request 223 | { 224 | $body = $request->getBody()->getContents(); 225 | $headers = $request->getHeaders(); 226 | 227 | $symfonyRequest = new Request( 228 | $request->getQueryParams(), 229 | $request->getParsedBody() ?? [], 230 | $request->getAttributes(), 231 | $request->getCookieParams(), 232 | $request->getUploadedFiles(), 233 | [], // Server is partially filled a few lines below 234 | $body 235 | ); 236 | 237 | $symfonyRequest->setMethod($method); 238 | $symfonyRequest->headers->replace($headers); 239 | $symfonyRequest->server->set('REQUEST_URI', $uriPath); 240 | 241 | if (isset($headers['Host'])) { 242 | $symfonyRequest->server->set('SERVER_NAME', explode(':', $headers['Host'][0])); 243 | } 244 | 245 | return $symfonyRequest; 246 | } 247 | 248 | /** 249 | * Symfony Response to http response 250 | * 251 | * @param Request $symfonyRequest 252 | * @param Response $symfonyResponse 253 | * @param float $from 254 | * 255 | * @return ServerResponseWithMessage 256 | */ 257 | private function toServerResponse( 258 | Request $symfonyRequest, 259 | Response $symfonyResponse, 260 | float $from 261 | ) : ServerResponseWithMessage 262 | { 263 | $to = microtime(true); 264 | 265 | $this->applyResponseEncoding( 266 | $symfonyRequest, 267 | $symfonyResponse 268 | ); 269 | 270 | $serverResponse = 271 | new ServerResponseWithMessage( 272 | new \React\Http\Response( 273 | $symfonyResponse->getStatusCode(), 274 | $symfonyResponse->headers->all(), 275 | $symfonyResponse->getContent() 276 | ), 277 | new ConsoleMessage( 278 | $symfonyRequest->getBaseUrl(), 279 | $symfonyRequest->getMethod(), 280 | $symfonyResponse->getStatusCode(), 281 | $symfonyResponse->getContent(), 282 | \intval(($to - $from) * 1000) 283 | ) 284 | ); 285 | 286 | $symfonyRequest = null; 287 | $symfonyResponse = null; 288 | 289 | return $serverResponse; 290 | } 291 | 292 | /** 293 | * Create exception Server response 294 | * 295 | * @param Throwable $exception 296 | * @param float $from 297 | * @param string $method 298 | * @param string $uriPath 299 | * 300 | * @return ServerResponseWithMessage 301 | */ 302 | private function createExceptionServerResponse( 303 | Throwable $exception, 304 | float $from, 305 | string $method, 306 | string $uriPath 307 | ) : ServerResponseWithMessage { 308 | $to = microtime(true); 309 | 310 | $serverResponse = 311 | new ServerResponseWithMessage( 312 | new \React\Http\Response( 313 | 400, 314 | ['Content-Type' => 'text/plain'], 315 | $exception->getMessage() 316 | ), 317 | new ConsoleException( 318 | $exception, 319 | $uriPath, 320 | $method, 321 | \intval(($to - $from) * 1000) 322 | ) 323 | ); 324 | 325 | $symfonyRequest = null; 326 | $symfonyResponse = null; 327 | 328 | return $serverResponse; 329 | } 330 | 331 | /** 332 | * Apply response encoding 333 | * 334 | * @param Request $request 335 | * @param Response $response 336 | */ 337 | private function applyResponseEncoding( 338 | Request $request, 339 | Response $response 340 | ) 341 | { 342 | $allowedCompressionAsString = $request 343 | ->headers 344 | ->get('Accept-Encoding'); 345 | if (!$allowedCompressionAsString) { 346 | return; 347 | } 348 | $allowedCompression = explode(',', $allowedCompressionAsString); 349 | $allowedCompression = array_map('trim', $allowedCompression); 350 | if (in_array('gzip', $allowedCompression)) { 351 | $response->setContent(gzencode($response->getContent())); 352 | $response 353 | ->headers 354 | ->set('Content-Encoding', 'gzip'); 355 | return; 356 | } 357 | if (in_array('deflate', $allowedCompression)) { 358 | $response->setContent(gzdeflate($response->getContent())); 359 | $response 360 | ->headers 361 | ->set('Content-Encoding', 'deflate'); 362 | return; 363 | } 364 | } 365 | } 366 | -------------------------------------------------------------------------------- /src/ServerResponseWithMessage.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | 14 | declare(strict_types=1); 15 | 16 | namespace Apisearch\SymfonyReactServer; 17 | 18 | use React\Http\Response; 19 | 20 | /** 21 | * Class ServerResponseWithMessage 22 | */ 23 | class ServerResponseWithMessage 24 | { 25 | /** 26 | * @var Response 27 | * 28 | * Server response 29 | */ 30 | private $serverResponse; 31 | 32 | /** 33 | * @var Printable 34 | * 35 | * Message 36 | */ 37 | private $message; 38 | 39 | /** 40 | * ServerResponseWithMessage constructor. 41 | * 42 | * @param Response $serverResponse 43 | * @param Printable $message 44 | */ 45 | public function __construct( 46 | Response $serverResponse, 47 | Printable $message 48 | ) 49 | { 50 | $this->serverResponse = $serverResponse; 51 | $this->message = $message; 52 | } 53 | 54 | /** 55 | * Get ServerResponse 56 | * 57 | * @return Response 58 | */ 59 | public function getServerResponse(): Response 60 | { 61 | return $this->serverResponse; 62 | } 63 | 64 | /** 65 | * Print message 66 | */ 67 | public function printMessage() 68 | { 69 | $this 70 | ->message 71 | ->print(); 72 | } 73 | } -------------------------------------------------------------------------------- /tests/ApplicationStaticFolderTest.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | 14 | declare(strict_types=1); 15 | 16 | namespace Apisearch\SymfonyReactServer\Tests; 17 | 18 | use finfo; 19 | use PHPUnit\Framework\TestCase; 20 | use Symfony\Component\Process\Process; 21 | 22 | /** 23 | * Class ApplicationStaticFolderTest. 24 | */ 25 | class ApplicationStaticFolderTest extends TestCase 26 | { 27 | /** 28 | * Test default adapter static folder. 29 | */ 30 | public function testBlockingServer() 31 | { 32 | $process = new Process([ 33 | 'php', 34 | dirname(__FILE__).'/../bin/server', 35 | '0.0.0.0:9999', 36 | '--bootstrap=autoload', 37 | '--adapter='.FakeAdapter::class, 38 | '--dev', 39 | ]); 40 | 41 | $process->start(); 42 | usleep(300000); 43 | 44 | $this->assertTrue( 45 | strpos( 46 | $process->getOutput(), 47 | 'Static Folder: /tests/public' 48 | ) > 0 49 | ); 50 | 51 | $this->assertFileWasReceived('http://localhost:9999/tests/public/app.js', '// Some app', 'text/plain'); 52 | usleep(100000); 53 | 54 | $this->assertNotFalse( 55 | strpos( 56 | $process->getOutput(), 57 | '200 GET' 58 | ) 59 | ); 60 | 61 | $process->stop(); 62 | } 63 | 64 | /** 65 | * Test disable static folder. 66 | */ 67 | public function testDisabledStaticFolder() 68 | { 69 | $process = new Process([ 70 | 'php', 71 | dirname(__FILE__).'/../bin/server', 72 | '0.0.0.0:9999', 73 | '--bootstrap=autoload', 74 | '--adapter='.FakeAdapter::class, 75 | '--no-static-folder', 76 | '--dev', 77 | ]); 78 | 79 | $process->start(); 80 | usleep(300000); 81 | 82 | $this->assertTrue( 83 | strpos( 84 | $process->getOutput(), 85 | 'Static Folder: disabled' 86 | ) > 0 87 | ); 88 | 89 | $content = @file_get_contents('http://localhost:9999/tests/public/app.js'); 90 | $this->assertFalse($content); 91 | 92 | $process->stop(); 93 | } 94 | 95 | private function assertFileWasReceived(string $file, string $expectedContent, string $expectedMimeType): void 96 | { 97 | $content = file_get_contents($file); 98 | $this->assertEquals($expectedContent, $content); 99 | 100 | $fileInfo = new Finfo(FILEINFO_MIME_TYPE); 101 | $this->assertEquals($expectedMimeType, $fileInfo->buffer($content)); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /tests/ApplicationTest.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | 14 | declare(strict_types=1); 15 | 16 | namespace Apisearch\SymfonyReactServer\Tests; 17 | 18 | use PHPUnit\Framework\TestCase; 19 | use Symfony\Component\Process\Process; 20 | 21 | /** 22 | * Class ApplicationTest. 23 | */ 24 | class ApplicationTest extends TestCase 25 | { 26 | /** 27 | * Test blocking server. 28 | */ 29 | public function testBlockingServer() 30 | { 31 | $process = new Process([ 32 | 'php', 33 | dirname(__FILE__).'/../bin/server', 34 | '0.0.0.0:9999', 35 | '--bootstrap=autoload', 36 | '--adapter='.FakeAdapter::class, 37 | '--dev', 38 | ]); 39 | 40 | $process->start(); 41 | usleep(300000); 42 | $this->assertTrue( 43 | strpos( 44 | $process->getOutput(), 45 | 'Non Blocking: disabled' 46 | ) > 0 47 | ); 48 | file_get_contents('http://localhost:9999?code=200'); 49 | usleep(100000); 50 | 51 | $this->assertNotFalse( 52 | strpos( 53 | $process->getOutput(), 54 | '200 GET' 55 | ) 56 | ); 57 | 58 | $process->stop(); 59 | } 60 | 61 | /** 62 | * Test non blocking server. 63 | */ 64 | public function testNonBlockingServer() 65 | { 66 | $process = new Process([ 67 | 'php', 68 | dirname(__FILE__).'/../bin/server', 69 | '0.0.0.0:9999', 70 | '--bootstrap=autoload', 71 | '--adapter='.FakeAdapter::class, 72 | '--non-blocking', 73 | '--dev', 74 | ]); 75 | 76 | $process->start(); 77 | usleep(300000); 78 | 79 | $this->assertTrue( 80 | strpos( 81 | $process->getOutput(), 82 | 'Non Blocking: enabled' 83 | ) > 0 84 | ); 85 | 86 | file_get_contents('http://localhost:9999?code=200'); 87 | usleep(100000); 88 | $this->assertNotFalse( 89 | strpos( 90 | $process->getOutput(), 91 | '200 GET' 92 | ) 93 | ); 94 | 95 | $process->stop(); 96 | } 97 | 98 | /** 99 | * Test silent. 100 | */ 101 | public function testSilentServer() 102 | { 103 | $process = new Process([ 104 | 'php', 105 | dirname(__FILE__).'/../bin/server', 106 | '0.0.0.0:9999', 107 | '--bootstrap=autoload', 108 | '--adapter='.FakeAdapter::class, 109 | '--silent', 110 | '--dev', 111 | ]); 112 | 113 | $process->start(); 114 | usleep(300000); 115 | file_get_contents('http://localhost:9999?code=200'); 116 | usleep(100000); 117 | 118 | $this->assertEquals( 119 | '', 120 | $process->getOutput() 121 | ); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /tests/FakeAdapter.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | 14 | declare(strict_types=1); 15 | 16 | namespace Apisearch\SymfonyReactServer\Tests; 17 | 18 | use Apisearch\SymfonyReactServer\Adapter\KernelAdapter; 19 | use Symfony\Component\HttpKernel\Kernel; 20 | 21 | /** 22 | * Class FakeAdapter. 23 | */ 24 | class FakeAdapter implements KernelAdapter 25 | { 26 | /** 27 | * Build kernel. 28 | * 29 | * @param string $environment 30 | * @param bool $debug 31 | * 32 | * @return Kernel 33 | */ 34 | public static function buildKernel( 35 | string $environment, 36 | bool $debug 37 | ): Kernel { 38 | return new FakeKernel($environment, $debug); 39 | } 40 | 41 | /** 42 | * Get static folder by kernel. 43 | * 44 | * @param Kernel $kernel 45 | * 46 | * @return string|null 47 | */ 48 | public static function getStaticFolder(Kernel $kernel): ? string 49 | { 50 | return '/tests/public'; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /tests/FakeKernel.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | 14 | declare(strict_types=1); 15 | 16 | namespace Apisearch\SymfonyReactServer\Tests; 17 | 18 | use React\Promise\FulfilledPromise; 19 | use React\Promise\PromiseInterface; 20 | use Symfony\Bundle\FrameworkBundle\FrameworkBundle; 21 | use Symfony\Component\Config\Loader\LoaderInterface; 22 | use Symfony\Component\DependencyInjection\ContainerBuilder; 23 | use Symfony\Component\HttpFoundation\Request; 24 | use Symfony\Component\HttpFoundation\Response; 25 | use Symfony\Component\HttpKernel\AsyncKernel; 26 | use Symfony\Component\HttpKernel\Bundle\BundleInterface; 27 | use Symfony\Component\HttpKernel\HttpKernelInterface; 28 | 29 | /** 30 | * Class FakeKernel. 31 | */ 32 | class FakeKernel extends AsyncKernel 33 | { 34 | /** 35 | * Returns an array of bundles to register. 36 | * 37 | * @return iterable|BundleInterface[] 38 | */ 39 | public function registerBundles() 40 | { 41 | return [ 42 | new FrameworkBundle(), 43 | ]; 44 | } 45 | 46 | /** 47 | * Loads the container configuration. 48 | */ 49 | public function registerContainerConfiguration(LoaderInterface $loader) 50 | { 51 | } 52 | 53 | /** 54 | * You can modify the container here before it is dumped to PHP code. 55 | */ 56 | public function process(ContainerBuilder $container) 57 | { 58 | parent::process($container); 59 | 60 | $container->setParameter('kernel.secret', 'engonga'); 61 | } 62 | 63 | /** 64 | * Handles a Request to convert it to a Response. 65 | * 66 | * When $catch is true, the implementation must catch all exceptions 67 | * and do its best to convert them to a Response instance. 68 | * 69 | * @param Request $request A Request instance 70 | * @param int $type The type of the request 71 | * (one of HttpKernelInterface::MASTER_REQUEST or HttpKernelInterface::SUB_REQUEST) 72 | * @param bool $catch Whether to catch exceptions or not 73 | * 74 | * @return PromiseInterface 75 | * 76 | * @throws \Exception When an Exception occurs during processing 77 | */ 78 | public function handleAsync( 79 | Request $request, 80 | $type = self::MASTER_REQUEST, 81 | $catch = true 82 | ): PromiseInterface { 83 | return (new FulfilledPromise($request)) 84 | ->then(function (Request $request) { 85 | return $this->handle($request); 86 | }); 87 | } 88 | 89 | /** 90 | * {@inheritdoc} 91 | */ 92 | public function handle(Request $request, $type = HttpKernelInterface::MASTER_REQUEST, $catch = true) 93 | { 94 | return new Response(json_encode([ 95 | 'query' => $request->query, 96 | ]), \intval($request->query->get('code'))); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /tests/public/app.js: -------------------------------------------------------------------------------- 1 | // Some app --------------------------------------------------------------------------------