├── LICENSE ├── README.md ├── bin └── swoole ├── composer.json └── src ├── Accessor.php ├── Command └── ServerCommand.php └── Driver └── Symfony ├── Driver.php ├── Request.php └── SessionStorage.php /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016-present PHP.earth 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 all 13 | 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 THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Swoole Engine 2 | 3 | [![Build Status](https://img.shields.io/travis/php-earth/swoole-engine/master.svg?style=flat-square)](https://travis-ci.org/php-earth/swoole-engine) 4 | 5 | Event-driven PHP engine for running PHP Applications with [Swoole extension](http://swoole.com). 6 | 7 | ## Installation 8 | 9 |
10 | Before using this library, you'll need Swoole extension 11 | 12 | Installing using PECL: 13 | 14 | ```bash 15 | pecl install swoole 16 | ``` 17 | 18 | Add `extension=swoole` (or `extension=swoole.so` for PHP < 7.2) to your `php.ini` 19 | file for PHP CLI sapi: 20 | 21 | ```bash 22 | echo "extension=swoole" | sudo tee --append `php -r 'echo php_ini_loaded_file();'` 23 | ``` 24 | 25 | Check if Swoole extension is loaded 26 | ```bash 27 | php --ri swoole 28 | ``` 29 | 30 | You should see something like 31 | 32 | ```bash 33 | swoole 34 | 35 | swoole support => enabled 36 | Version => 2.0.10 37 | Author => tianfeng.han[email: mikan.tenny@gmail.com] 38 | epoll => enabled 39 | eventfd => enabled 40 | timerfd => enabled 41 | signalfd => enabled 42 | cpu affinity => enabled 43 | spinlock => enabled 44 | rwlock => enabled 45 | async http/websocket client => enabled 46 | Linux Native AIO => enabled 47 | pcre => enabled 48 | mutex_timedlock => enabled 49 | pthread_barrier => enabled 50 | futex => enabled 51 | 52 | Directive => Local Value => Master Value 53 | swoole.aio_thread_num => 2 => 2 54 | swoole.display_errors => On => On 55 | swoole.use_namespace => On => On 56 | swoole.fast_serialize => Off => Off 57 | swoole.unixsock_buffer_size => 8388608 => 8388608 58 | ``` 59 | 60 |
61 | 62 | Then proceed and install Swoole Engine library in your project with Composer: 63 | 64 | ```bash 65 | composer require php-earth/swoole-engine 66 | ``` 67 | 68 | ## Usage 69 | 70 | Currently supported frameworks: 71 | 72 | * Symfony: 73 | 74 | ```bash 75 | vendor/bin/swoole [--env=dev|prod|...] [--host=IP] [--no-debug] 76 | ``` 77 | 78 | ## Documentation 79 | 80 | For more information, read the [documentation](docs): 81 | 82 | * [Introduction](docs/intro.md) 83 | * [Sessions](docs/sessions.md) 84 | 85 | ## License 86 | 87 | [Contributions](docs/CONTRIBUTING.md) are most welcome. This repository is 88 | released under the [MIT license](LICENSE). 89 | -------------------------------------------------------------------------------- /bin/swoole: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | add($serverCommand); 27 | 28 | $application->setDefaultCommand($serverCommand->getName(), true); 29 | $application->run(); 30 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "php-earth/swoole-engine", 3 | "description": "Event-driven engine for running PHP with Swoole extension.", 4 | "type": "library", 5 | "license": "MIT", 6 | "require": { 7 | "symfony/console": "^4.0", 8 | "php": ">=7.1", 9 | "ext-swoole": "*" 10 | }, 11 | "require-dev": { 12 | "phpunit/phpunit": "^6.1" 13 | }, 14 | "authors": [ 15 | { 16 | "name": "PHP.earth Contributors", 17 | "homepage": "https://github.com/php-earth/swoole-engine/graphs/contributors" 18 | } 19 | ], 20 | "autoload": { 21 | "psr-4": {"PhpEarth\\Swoole\\": "src/"} 22 | }, 23 | "autoload-dev": { 24 | "psr-4": { "PhpEarth\\Swoole\\Tests\\": "tests/" } 25 | }, 26 | "bin": ["bin/swoole"] 27 | } 28 | -------------------------------------------------------------------------------- /src/Accessor.php: -------------------------------------------------------------------------------- 1 | $property = $value; 18 | }, null, $object); 19 | 20 | $thief($object); 21 | } 22 | 23 | /** 24 | * Get private or protected property of given object. 25 | * 26 | * @param object $object Object for which property needs to be accessed. 27 | * @param string $property Property name 28 | */ 29 | public static function get($object, $property) 30 | { 31 | return (function() use ($property) { return $this->$property; })->bindTo($object, $object)(); 32 | } 33 | 34 | /** 35 | * Binds callable for calling private and protected methods. 36 | * 37 | * @param callable $callable 38 | * @param mixed $newThis 39 | * @param array $args 40 | * @param mixed $bindClass 41 | * @return void 42 | */ 43 | public static function call(callable $callable, $newThis, $args = [], $bindClass = null) 44 | { 45 | $closure = \Closure::bind($callable, $newThis, $bindClass ?: get_class($newThis)); 46 | if ($args) { 47 | call_user_func_array($closure, $args); 48 | } else { 49 | // Calling it directly is faster 50 | $closure(); 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Command/ServerCommand.php: -------------------------------------------------------------------------------- 1 | setName('server:start') 24 | ->setDescription('Start Swoole HTTP Server.') 25 | ->addOption('host', null, InputOption::VALUE_OPTIONAL, 'Host for server', '127.0.0.1') 26 | ->addOption('port', null, InputOption::VALUE_OPTIONAL, 'Port for server', 9501) 27 | ->addOption('env', null, InputOption::VALUE_OPTIONAL, 'Environment', 'dev') 28 | ->addOption('no-debug', null, InputOption::VALUE_NONE, 'Switch debug mode on/off') 29 | ; 30 | } 31 | 32 | protected function execute(InputInterface $input, OutputInterface $output) 33 | { 34 | $http = new \swoole_http_server($input->getOption('host'), $input->getOption('port')); 35 | 36 | $this->driver = new Driver(); 37 | 38 | $debug = ($input->getOption('no-debug')) ? false : (($input->getOption('env') == 'prod') ? false : true); 39 | 40 | $this->driver->boot($input->getOption('env'), $debug); 41 | 42 | $http->on('request', function(\swoole_http_request $request, \swoole_http_response $response) use($output, $debug) { 43 | $this->driver->setSwooleRequest($request); 44 | $this->driver->setSwooleResponse($response); 45 | 46 | $this->driver->preHandle(); 47 | $this->driver->handle(); 48 | $this->driver->postHandle(); 49 | 50 | if ($debug) { 51 | $output->writeln($this->driver->symfonyRequest->getPathInfo()); 52 | } 53 | }); 54 | 55 | $output->writeln('Swoole HTTP Server started on '.$input->getOption('host').':'.$input->getOption('port')); 56 | 57 | $http->start(); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Driver/Symfony/Driver.php: -------------------------------------------------------------------------------- 1 | projectDir.'/vendor/autoload.php'; 35 | 36 | // The check is to ensure we don't use .env in production 37 | if (!isset($_SERVER['APP_ENV'])) { 38 | if (!class_exists(Dotenv::class)) { 39 | throw new \RuntimeException('APP_ENV environment variable is not defined. You need to define environment variables for configuration or add "symfony/dotenv" as a Composer dependency to load variables from a .env file.'); 40 | } 41 | (new Dotenv())->load($this->projectDir.'/.env'); 42 | } 43 | 44 | if ($_SERVER['APP_DEBUG'] ?? ('prod' !== ($_SERVER['APP_ENV'] ?? 'dev'))) { 45 | umask(0000); 46 | 47 | Debug::enable(); 48 | } 49 | 50 | $this->kernel = new Kernel($_SERVER['APP_ENV'] ?? 'dev', $_SERVER['APP_DEBUG'] ?? ('prod' !== ($_SERVER['APP_ENV'] ?? 'dev'))); 51 | $this->kernel->boot(); 52 | } 53 | 54 | /** 55 | * Set Swoole request. 56 | * 57 | * @param \swoole_http_request $request 58 | */ 59 | public function setSwooleRequest(\swoole_http_request $request) 60 | { 61 | $this->swooleRequest = $request; 62 | } 63 | 64 | /** 65 | * Set Swoole response. 66 | * 67 | * @param \swoole_http_response $response 68 | */ 69 | public function setSwooleResponse(\swoole_http_response $response) 70 | { 71 | $this->swooleResponse = $response; 72 | } 73 | 74 | /** 75 | * Happens before each request. We need to change session storage service in 76 | * the middle of Kernel booting process. 77 | * 78 | * @return void 79 | */ 80 | public function preHandle() 81 | { 82 | // Reset Kernel startTime, so Symfony can correctly calculate the execution time 83 | if (Accessor::get($this->kernel, 'debug')) { 84 | Accessor::set($this->kernel, 'startTime', microtime(true)); 85 | } 86 | 87 | $this->reloadSession(); 88 | 89 | Accessor::call(function() { 90 | $this->initializeBundles(); 91 | 92 | $this->initializeContainer(); 93 | }, $this->kernel); 94 | 95 | if ($this->kernel->getContainer()->has('session')) { 96 | // Inject custom SessionStorage of Symfony Driver 97 | $nativeStorage = new SessionStorage( 98 | $this->kernel->getContainer()->getParameter('session.storage.options'), 99 | $this->kernel->getContainer()->has('session.handler') ? $this->kernel->getContainer()->get('session.handler'): null, 100 | $this->kernel->getContainer()->get('session.storage')->getMetadataBag() 101 | ); 102 | $nativeStorage->swooleResponse = $this->swooleResponse; 103 | $this->kernel->getContainer()->set('session.storage.native', $nativeStorage); 104 | } 105 | 106 | Accessor::call(function() { 107 | foreach ($this->getBundles() as $bundle) { 108 | $bundle->setContainer($this->container); 109 | $bundle->boot(); 110 | } 111 | $this->booted = true; 112 | }, $this->kernel); 113 | } 114 | 115 | /** 116 | * Happens after each request. 117 | * 118 | * @return void 119 | */ 120 | public function postHandle() 121 | { 122 | // Close database connection. 123 | if ($this->kernel->getContainer()->has('doctrine.orm.entity_manager')) { 124 | $this->kernel->getContainer()->get('doctrine.orm.entity_manager')->clear(); 125 | $this->kernel->getContainer()->get('doctrine.orm.entity_manager')->close(); 126 | $this->kernel->getContainer()->get('doctrine.orm.entity_manager')->getConnection()->close(); 127 | } 128 | 129 | $this->kernel->terminate($this->symfonyRequest, $this->symfonyResponse); 130 | } 131 | 132 | /** 133 | * Transform Symfony request and response to Swoole compatible response. 134 | * 135 | * @return void 136 | */ 137 | public function handle() 138 | { 139 | $rq = new Request(); 140 | $this->symfonyRequest = $rq->createSymfonyRequest($this->swooleRequest); 141 | $this->symfonyResponse = $this->kernel->handle($this->symfonyRequest); 142 | 143 | // Manually create PHP session cookie. When running Swoole, PHP session_start() 144 | // function cannot set PHP session cookie since there is no traditional 145 | // header outputting. 146 | if (!isset($this->swooleRequest->cookie[session_name()]) && 147 | $this->symfonyRequest->hasSession() 148 | ) { 149 | $params = session_get_cookie_params(); 150 | $this->swooleResponse->rawcookie( 151 | $this->symfonyRequest->getSession()->getName(), 152 | $this->symfonyRequest->getSession()->getId(), 153 | $params['lifetime'] ? time() + $params['lifetime'] : null, 154 | $params['path'], 155 | $params['domain'], 156 | $params['secure'], 157 | $params['httponly'] 158 | ); 159 | } 160 | 161 | // HTTP status code for response 162 | $this->swooleResponse->status($this->symfonyResponse->getStatusCode()); 163 | 164 | // Cookies 165 | foreach ($this->symfonyResponse->headers->getCookies() as $cookie) { 166 | $this->swooleResponse->rawcookie( 167 | $cookie->getName(), 168 | urlencode($cookie->getValue()), 169 | $cookie->getExpiresTime(), 170 | $cookie->getPath(), 171 | $cookie->getDomain(), 172 | $cookie->isSecure(), 173 | $cookie->isHttpOnly() 174 | ); 175 | } 176 | 177 | // Headers 178 | foreach ($this->symfonyResponse->headers->allPreserveCase() as $name => $values) { 179 | //$name = implode('-', array_map('ucfirst', explode('-', $name))); 180 | foreach ($values as $value) { 181 | $this->swooleResponse->header($name, $value); 182 | } 183 | } 184 | 185 | $this->swooleResponse->end($this->symfonyResponse->getContent()); 186 | } 187 | 188 | /** 189 | * Fix for managing sessions with Swoole. On each request session_id needs to be 190 | * regenerated, because we're running PHP script in CLI and listening for requests 191 | * concurrently. 192 | * 193 | * @return void 194 | */ 195 | private function reloadSession() 196 | { 197 | if (isset($this->swooleRequest->cookie[session_name()])) { 198 | session_id($this->swooleRequest->cookie[session_name()]); 199 | } else { 200 | if (session_id()) { 201 | session_id(\bin2hex(\random_bytes(32))); 202 | } 203 | 204 | // Empty global session array otherwise it is filled with values from 205 | // previous session. 206 | $_SESSION = []; 207 | } 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /src/Driver/Symfony/Request.php: -------------------------------------------------------------------------------- 1 | setServer($request); 19 | 20 | // Other superglobals 21 | $_GET = $request->get ?? []; 22 | $_POST = $request->post ?? []; 23 | $_COOKIE = $request->cookie ?? []; 24 | $_FILES = $request->files ?? []; 25 | $content = $request->rawContent() ?: null; 26 | 27 | $symfonyRequest = new SymfonyRequest( 28 | $_GET, 29 | $_POST, 30 | [], 31 | $_COOKIE, 32 | $_FILES, 33 | $_SERVER, 34 | $content 35 | ); 36 | 37 | if (0 === strpos($symfonyRequest->headers->get('Content-Type'), 'application/json')) { 38 | $data = json_decode($request->rawContent(), true); 39 | $symfonyRequest->request->replace(is_array($data) ? $data : []); 40 | } 41 | 42 | if ($trustedProxies = $_SERVER['TRUSTED_PROXIES'] ?? false) { 43 | $symfonyRequest::setTrustedProxies(explode(',', $trustedProxies), SymfonyRequest::HEADER_X_FORWARDED_ALL ^ SymfonyRequest::HEADER_X_FORWARDED_HOST); 44 | } 45 | 46 | if ($trustedHosts = $_SERVER['TRUSTED_HOSTS'] ?? false) { 47 | $symfonyRequest::setTrustedHosts(explode(',', $trustedHosts)); 48 | } 49 | 50 | return $symfonyRequest; 51 | } 52 | 53 | /** 54 | * Create $_SERVER superglobal for traditional PHP applications. By default 55 | * Swoole request contains headers with lower case keys and dash separator 56 | * instead of underscores and upper case letters which PHP expects in the 57 | * $_SERVER superglobal. For example: 58 | * - host: localhost:9501 59 | * - connection: keep-alive 60 | * - accept-language: en-US,en;q=0.8,sl;q=0.6 61 | * 62 | * @param \swoole_http_request $request 63 | */ 64 | public function setServer($request) 65 | { 66 | $headers = []; 67 | 68 | foreach ($request->header as $key => $value) { 69 | if ($key == 'x-forwarded-proto' && $value == 'https') { 70 | $request->server['HTTPS'] = 'on'; 71 | } 72 | 73 | $headerKey = 'HTTP_' . strtoupper(str_replace('-', '_', $key)); 74 | $headers[$headerKey] = $value; 75 | } 76 | 77 | // Make swoole's server's keys uppercased and merge them into the $_SERVER superglobal 78 | $_SERVER = array_change_key_case(array_merge($request->server, $headers), CASE_UPPER); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/Driver/Symfony/SessionStorage.php: -------------------------------------------------------------------------------- 1 | swooleResponse->rawcookie( 28 | session_name(), 29 | session_id(), 30 | $params['lifetime'] ? time() + $params['lifetime'] : null, 31 | $params['path'], 32 | $params['domain'], 33 | $params['secure'], 34 | $params['httponly'] 35 | ); 36 | } 37 | ini_set('session.use_cookies', 1); 38 | 39 | return $isRegenerated; 40 | } 41 | } 42 | --------------------------------------------------------------------------------