diff --git a/.gitignore b/.gitignore index 8918ec05..b56fa873 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ .DS_Store phpunit.xml composer.lock +.scannerwork # Folders vendor diff --git a/.scrutinizer.yml b/.scrutinizer.yml new file mode 100644 index 00000000..90494730 --- /dev/null +++ b/.scrutinizer.yml @@ -0,0 +1,12 @@ +build: + environment: + php: 7.2 + +filter: + excluded_paths: + - "tests/*" + - "routes/*" + - "config/*" + - "stubs/*" + dependency_paths: + - "vendor/*" \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index 6709f198..a90e95ac 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,26 +4,24 @@ sudo: false matrix: include: - - php: 7.2 - env: FRAMEWORK_VERSION=laravel/framework:5.1.* - - php: 7.2 - env: FRAMEWORK_VERSION=laravel/framework:5.2.* - - php: 7.2 - env: FRAMEWORK_VERSION=laravel/framework:5.3.* - - php: 7.2 - env: FRAMEWORK_VERSION=laravel/framework:5.4.* - php: 7.2 env: FRAMEWORK_VERSION=laravel/framework:5.5.* - php: 7.2 - env: FRAMEWORK_VERSION=laravel/lumen-framework:5.1.* + env: FRAMEWORK_VERSION=laravel/framework:5.6.* - php: 7.2 - env: FRAMEWORK_VERSION=laravel/lumen-framework:5.2.* + env: FRAMEWORK_VERSION=laravel/framework:5.7.* - php: 7.2 - env: FRAMEWORK_VERSION=laravel/lumen-framework:5.3.* + env: FRAMEWORK_VERSION=laravel/framework:5.8.* - php: 7.2 - env: FRAMEWORK_VERSION=laravel/lumen-framework:5.4.* + env: FRAMEWORK_VERSION=laravel/framework:6.0.* - php: 7.2 env: FRAMEWORK_VERSION=laravel/lumen-framework:5.5.* + - php: 7.2 + env: FRAMEWORK_VERSION=laravel/lumen-framework:5.6.* + - php: 7.2 + env: FRAMEWORK_VERSION=laravel/lumen-framework:5.7.* + # - php: 7.2 + # env: FRAMEWORK_VERSION=laravel/lumen-framework:5.8.* before_install: - printf "\n" | pecl install swoole @@ -37,4 +35,4 @@ script: - vendor/bin/phpunit --coverage-clover build/logs/clover.xml after_success: - - vendor/bin/coveralls -v + - vendor/bin/php-coveralls -v diff --git a/README.md b/README.md index 5fef1611..1a244625 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Laravel-Swoole -![php-badge](https://img.shields.io/badge/php-%3E%3D%207.1-8892BF.svg) +![php-badge](https://img.shields.io/badge/php-%3E%3D%207.2-8892BF.svg) [![packagist-badge](https://img.shields.io/packagist/v/swooletw/laravel-swoole.svg)](https://packagist.org/packages/swooletw/laravel-swoole) [![Total Downloads](https://poser.pugx.org/swooletw/laravel-swoole/downloads)](https://packagist.org/packages/swooletw/laravel-swoole) [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/swooletw/laravel-swoole/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/swooletw/laravel-swoole/?branch=master) @@ -12,7 +12,7 @@ This package provides a high performance HTTP server to speed up your Laravel/Lu | PHP | Laravel | Lumen | Swoole | |:-------:|:-------:|:-----:|:-------:| -| >=7.1 | ~5.1 | ~5.1 | >=4.0.0 | +| >=7.2 | >=5.5 | >=5.5 | >=4.3.1 | ## Features diff --git a/composer.json b/composer.json index 85b7537c..931c8ecb 100644 --- a/composer.json +++ b/composer.json @@ -1,60 +1,73 @@ { - "name": "swooletw/laravel-swoole", - "description": "High performance HTTP server based on Swoole. Speed up your Laravel and Lumen applications.", - "keywords": ["swoole", "laravel", "lumen", "performance", "http", "server"], - "license": "MIT", - "authors": [ - { - "name": "Albert Chen", - "email": "albert@unisharp.com" - }, - { - "name": "Huang Yi", - "email": "coodeer@163.com" - } - ], - "require": { - "php": "^7.1", - "illuminate/console": "~5.1", - "illuminate/contracts": "~5.1", - "illuminate/http": "~5.1", - "illuminate/support": "~5.1", - "predis/predis": "^1.1" - }, - "require-dev": { - "laravel/lumen-framework": "~5.1", - "phpunit/phpunit": "^6.1", - "phpunit/php-code-coverage": "^5.2", - "satooshi/php-coveralls": "^1.0", - "mockery/mockery": "~1.0", - "codedungeon/phpunit-result-printer": "^0.14.0", - "php-mock/php-mock": "^2.0" - }, - "autoload": { - "files": [ - "src/Server/helpers.php" - ], - "psr-4": { - "SwooleTW\\Http\\": "src" - } - }, - "autoload-dev": { - "psr-4": { - "SwooleTW\\Http\\Tests\\": "tests", - "SwooleTW\\Http\\Tests\\Fixtures\\Laravel\\App\\": "tests/fixtures/laravel/app" - } + "name": "swooletw/laravel-swoole", + "description": "High performance HTTP server based on Swoole. Speed up your Laravel and Lumen applications.", + "keywords": [ + "swoole", + "laravel", + "lumen", + "performance", + "http", + "server" + ], + "license": "MIT", + "authors": [ + { + "name": "Albert Chen", + "email": "albert@unisharp.com" }, - "extra": { - "laravel": { - "providers": [ - "SwooleTW\\Http\\LaravelServiceProvider" - ], - "aliases": { - "Server": "SwooleTW\\Http\\Server\\Facades\\Server", - "Table": "SwooleTW\\Http\\Server\\Facades\\Table", - "Room": "SwooleTW\\Http\\Websocket\\Facades\\Room", - "Websocket": "SwooleTW\\Http\\Websocket\\Facades\\Websocket" - } - } + { + "name": "Huang Yi", + "email": "coodeer@163.com" + } + ], + "require": { + "php": "^7.2", + "illuminate/console": "~5.4|~6.0|~7.0|~8.0", + "illuminate/contracts": "~5.4|~6.0|~7.0|~8.0", + "illuminate/http": "~5.4|~6.0|~7.0|~8.0", + "illuminate/support": "~5.4|~6.0|~7.0|~8.0", + "predis/predis": "^1.1" + }, + "require-dev": { + "laravel/lumen-framework": "~5.4|~6.0|~7.0|~8.0", + "phpunit/phpunit": "^7.5", + "phpunit/php-code-coverage": "^6.1", + "php-coveralls/php-coveralls": "^2.1", + "mockery/mockery": "~1.0", + "codedungeon/phpunit-result-printer": "^0.14.0", + "php-mock/php-mock": "^2.0", + "swoole/ide-helper": "@dev" + }, + "autoload": { + "files": [ + "src/Server/helpers.php" + ], + "psr-4": { + "SwooleTW\\Http\\": "src" + } + }, + "autoload-dev": { + "psr-4": { + "SwooleTW\\Http\\Tests\\": "tests", + "SwooleTW\\Http\\Tests\\Fixtures\\Laravel\\App\\": "tests/fixtures/laravel/app" + } + }, + "extra": { + "laravel": { + "providers": [ + "SwooleTW\\Http\\LaravelServiceProvider" + ], + "aliases": { + "Server": "SwooleTW\\Http\\Server\\Facades\\Server", + "Table": "SwooleTW\\Http\\Server\\Facades\\Table", + "Room": "SwooleTW\\Http\\Websocket\\Facades\\Room", + "Websocket": "SwooleTW\\Http\\Websocket\\Facades\\Websocket" + } } + }, + "scripts": { + "post-autoload-dump": [ + "@php copy_versioned_files.php" + ] + } } diff --git a/config/swoole_http.php b/config/swoole_http.php index 2f70159c..150e680f 100644 --- a/config/swoole_http.php +++ b/config/swoole_http.php @@ -1,7 +1,5 @@ base_path('public'), // Determine if to use swoole to respond request for static files 'handle_static_files' => env('SWOOLE_HANDLE_STATIC', true), + 'access_log' => env('SWOOLE_HTTP_ACCESS_LOG', false), // You must add --enable-openssl while compiling Swoole // Put `SWOOLE_SOCK_TCP | SWOOLE_SSL` if you want to enable SSL 'socket_type' => SWOOLE_SOCK_TCP, + 'process_type' => SWOOLE_PROCESS, 'options' => [ 'pid_file' => env('SWOOLE_HTTP_PID_FILE', base_path('storage/logs/swoole_http.pid')), 'log_file' => env('SWOOLE_HTTP_LOG_FILE', base_path('storage/logs/swoole_http.log')), @@ -53,6 +53,19 @@ 'enabled' => env('SWOOLE_HTTP_WEBSOCKET', false), ], + /* + |-------------------------------------------------------------------------- + | Hot reload configuration + |-------------------------------------------------------------------------- + */ + 'hot_reload' => [ + 'enabled' => env('SWOOLE_HOT_RELOAD_ENABLE', false), + 'recursively' => env('SWOOLE_HOT_RELOAD_RECURSIVELY', true), + 'directory' => env('SWOOLE_HOT_RELOAD_DIRECTORY', base_path()), + 'log' => env('SWOOLE_HOT_RELOAD_LOG', true), + 'filter' => env('SWOOLE_HOT_RELOAD_FILTER', '.php'), + ], + /* |-------------------------------------------------------------------------- | Console output will be transferred to response content if enabled. @@ -68,7 +81,7 @@ 'pre_resolved' => [ 'view', 'files', 'session', 'session.store', 'routes', 'db', 'db.factory', 'cache', 'cache.store', 'config', 'cookie', - 'encrypter', 'hash', 'router', 'translator', 'url', 'log', + 'encrypter', 'hash', 'router', 'translator', 'url', 'log', 'auth', ], /* @@ -120,5 +133,5 @@ // ['name' => 'column_name', 'type' => Table::TYPE_STRING, 'size' => 1024], // ] // ], - ] + ], ]; diff --git a/config/swoole_websocket.php b/config/swoole_websocket.php index a9c770da..867b9df8 100644 --- a/config/swoole_websocket.php +++ b/config/swoole_websocket.php @@ -35,6 +35,16 @@ // SwooleTW\Http\Websocket\Middleware\Authenticate::class, ], + /* + |-------------------------------------------------------------------------- + | Websocket handler for customized onHandShake callback + |-------------------------------------------------------------------------- + */ + 'handshake' => [ + 'enabled' => false, + 'handler' => SwooleTW\Http\Websocket\HandShakeHandler::class, + ], + /* |-------------------------------------------------------------------------- | Default websocket driver @@ -77,7 +87,7 @@ 'room_rows' => 4096, 'room_size' => 2048, 'client_rows' => 8192, - 'client_size' => 2048 + 'client_size' => 2048, ], 'redis' => [ @@ -92,6 +102,6 @@ // ], 'prefix' => 'swoole:', - ] + ], ], ]; diff --git a/copy_versioned_files.php b/copy_versioned_files.php new file mode 100644 index 00000000..f4f0b718 --- /dev/null +++ b/copy_versioned_files.php @@ -0,0 +1,33 @@ + - - - ./tests - - ./src + + + ./tests + + diff --git a/routes/laravel_routes.php b/routes/laravel_routes.php index 9afb5f6e..53564c96 100644 --- a/routes/laravel_routes.php +++ b/routes/laravel_routes.php @@ -1,5 +1,6 @@ get('socket.io', [ - 'as' => 'io.get', 'uses' => 'SocketIOController@upgrade' -]); - -$app->post('socket.io', [ - 'as' => 'io.post', 'uses' => 'SocketIOController@reject' -]); +$router->get('socket.io', ['uses' => 'SocketIOController@upgrade']); +$router->post('socket.io', ['uses' => 'SocketIOController@reject']); diff --git a/routes/websocket.php b/routes/websocket.php index a956ac7d..ddebfe14 100644 --- a/routes/websocket.php +++ b/routes/websocket.php @@ -1,5 +1,6 @@ emit('message', $data); }); - -// Websocket::on('test', 'ExampleController@method'); \ No newline at end of file diff --git a/src/Commands/HttpServerCommand.php b/src/Commands/HttpServerCommand.php index 157c9b54..5ed1d959 100644 --- a/src/Commands/HttpServerCommand.php +++ b/src/Commands/HttpServerCommand.php @@ -4,7 +4,20 @@ use Throwable; use Swoole\Process; +use Illuminate\Support\Arr; +use SwooleTW\Http\Helpers\OS; use Illuminate\Console\Command; +use SwooleTW\Http\Server\Manager; +use Illuminate\Console\OutputStyle; +use SwooleTW\Http\HotReload\FSEvent; +use SwooleTW\Http\HotReload\FSOutput; +use SwooleTW\Http\HotReload\FSProcess; +use SwooleTW\Http\Server\AccessOutput; +use SwooleTW\Http\Server\PidManager; +use SwooleTW\Http\Middleware\AccessLog; +use SwooleTW\Http\Server\Facades\Server; +use Illuminate\Contracts\Container\Container; +use Symfony\Component\Console\Output\ConsoleOutput; /** * @codeCoverageIgnore @@ -38,14 +51,14 @@ class HttpServerCommand extends Command * * @var int */ - protected $pid; + protected $currentPid; /** * The configs for this package. * * @var array */ - protected $configs; + protected $config; /** * Execute the console command. @@ -65,7 +78,7 @@ public function handle() */ protected function loadConfigs() { - $this->configs = $this->laravel['config']->get('swoole_http'); + $this->config = $this->laravel->make('config')->get('swoole_http'); } /** @@ -81,22 +94,38 @@ protected function runAction() */ protected function start() { - if ($this->isRunning($this->getPid())) { + if ($this->isRunning()) { $this->error('Failed! swoole_http_server process is already running.'); - exit(1); + + return; } - $host = $this->configs['server']['host']; - $port = $this->configs['server']['port']; + $host = Arr::get($this->config, 'server.host'); + $port = Arr::get($this->config, 'server.port'); + $hotReloadEnabled = Arr::get($this->config, 'hot_reload.enabled'); + $accessLogEnabled = Arr::get($this->config, 'server.access_log'); $this->info('Starting swoole http server...'); $this->info("Swoole http server started: "); if ($this->isDaemon()) { - $this->info('> (You can run this command to ensure the ' . - 'swoole_http_server process is running: ps aux|grep "swoole")'); + $this->info( + '> (You can run this command to ensure the ' . + 'swoole_http_server process is running: ps aux|grep "swoole")' + ); + } + + $manager = $this->laravel->make(Manager::class); + $server = $this->laravel->make(Server::class); + + if ($accessLogEnabled) { + $this->registerAccessLog(); } - $this->laravel->make('swoole.manager')->run(); + if ($hotReloadEnabled) { + $manager->addProcess($this->getHotReloadProcess($server)); + } + + $manager->run(); } /** @@ -104,25 +133,25 @@ protected function start() */ protected function stop() { - $pid = $this->getPid(); - - if (! $this->isRunning($pid)) { + if (! $this->isRunning()) { $this->error("Failed! There is no swoole_http_server process running."); - exit(1); + + return; } $this->info('Stopping swoole http server...'); - $isRunning = $this->killProcess($pid, SIGTERM, 15); + $isRunning = $this->killProcess(SIGTERM, 15); if ($isRunning) { $this->error('Unable to stop the swoole_http_server process.'); - exit(1); + + return; } // I don't known why Swoole didn't trigger "onShutdown" after sending SIGTERM. // So we should manually remove the pid file. - $this->removePidFile(); + $this->laravel->make(PidManager::class)->delete(); $this->info('> success'); } @@ -132,9 +161,7 @@ protected function stop() */ protected function restart() { - $pid = $this->getPid(); - - if ($this->isRunning($pid)) { + if ($this->isRunning()) { $this->stop(); } @@ -146,20 +173,18 @@ protected function restart() */ protected function reload() { - $pid = $this->getPid(); - - if (! $this->isRunning($pid)) { + if (! $this->isRunning()) { $this->error("Failed! There is no swoole_http_server process running."); - exit(1); + + return; } $this->info('Reloading swoole_http_server...'); - $isRunning = $this->killProcess($pid, SIGUSR1); - - if (! $isRunning) { + if (! $this->killProcess(SIGUSR1)) { $this->error('> failure'); - exit(1); + + return; } $this->info('> success'); @@ -175,20 +200,21 @@ protected function infos() /** * Display PHP and Swoole miscs infos. - * - * @param bool $more */ protected function showInfos() { - $pid = $this->getPid(); - $isRunning = $this->isRunning($pid); - $host = $this->configs['server']['host']; - $port = $this->configs['server']['port']; - $reactorNum = $this->configs['server']['options']['reactor_num']; - $workerNum = $this->configs['server']['options']['worker_num']; - $taskWorkerNum = $this->configs['server']['options']['task_worker_num']; - $isWebsocket = $this->configs['websocket']['enabled']; - $logFile = $this->configs['server']['options']['log_file']; + $isRunning = $this->isRunning(); + $host = Arr::get($this->config, 'server.host'); + $port = Arr::get($this->config, 'server.port'); + $reactorNum = Arr::get($this->config, 'server.options.reactor_num'); + $workerNum = Arr::get($this->config, 'server.options.worker_num'); + $taskWorkerNum = Arr::get($this->config, 'server.options.task_worker_num'); + $isWebsocket = Arr::get($this->config, 'websocket.enabled'); + $hasTaskWorker = $isWebsocket || Arr::get($this->config, 'queue.default') === 'swoole'; + $logFile = Arr::get($this->config, 'server.options.log_file'); + $pids = $this->laravel->make(PidManager::class)->read(); + $masterPid = $pids['masterPid'] ?? null; + $managerPid = $pids['managerPid'] ?? null; $table = [ ['PHP Version', 'Version' => phpversion()], @@ -199,9 +225,10 @@ protected function showInfos() ['Server Status', $isRunning ? 'Online' : 'Offline'], ['Reactor Num', $reactorNum], ['Worker Num', $workerNum], - ['Task Worker Num', $isWebsocket ? $taskWorkerNum : 0], + ['Task Worker Num', $hasTaskWorker ? $taskWorkerNum : 0], ['Websocket Mode', $isWebsocket ? 'On' : 'Off'], - ['PID', $isRunning ? $pid : 'None'], + ['Master PID', $isRunning ? $masterPid : 'None'], + ['Manager PID', $isRunning && $managerPid ? $managerPid : 'None'], ['Log Path', $logFile], ]; @@ -215,48 +242,82 @@ protected function initAction() { $this->action = $this->argument('action'); - if (! in_array($this->action, ['start', 'stop', 'restart', 'reload', 'infos'])) { - $this->error("Invalid argument '{$this->action}'. Expected 'start', 'stop', 'restart', 'reload' or 'infos'."); - exit(1); + if (! in_array($this->action, ['start', 'stop', 'restart', 'reload', 'infos'], true)) { + $this->error( + "Invalid argument '{$this->action}'. Expected 'start', 'stop', 'restart', 'reload' or 'infos'." + ); + + return; } } + /** + * @param \SwooleTW\Http\Server\Facades\Server $server + * + * @return \Swoole\Process + */ + protected function getHotReloadProcess($server) + { + $recursively = Arr::get($this->config, 'hot_reload.recursively'); + $directory = Arr::get($this->config, 'hot_reload.directory'); + $filter = Arr::get($this->config, 'hot_reload.filter'); + $log = Arr::get($this->config, 'hot_reload.log'); + + $cb = function (FSEvent $event) use ($server, $log) { + $log ? $this->info(FSOutput::format($event)) : null; + $server->reload(); + }; + + return (new FSProcess($filter, $recursively, $directory))->make($cb); + } + /** * If Swoole process is running. * * @param int $pid + * * @return bool */ - protected function isRunning($pid) + public function isRunning() { - if (! $pid) { + $pids = $this->laravel->make(PidManager::class)->read(); + + if (! count($pids)) { return false; } - try { - return Process::kill($pid, 0); - } catch (Throwable $e) { - return false; + $masterPid = $pids['masterPid'] ?? null; + $managerPid = $pids['managerPid'] ?? null; + + if ($managerPid) { + // Swoole process mode + return $masterPid && $managerPid && Process::kill((int) $managerPid, 0); } + + // Swoole base mode, no manager process + return $masterPid && Process::kill((int) $masterPid, 0); } /** * Kill process. * - * @param int $pid * @param int $sig * @param int $wait + * * @return bool */ - protected function killProcess($pid, $sig, $wait = 0) + protected function killProcess($sig, $wait = 0) { - Process::kill($pid, $sig); + Process::kill( + Arr::first($this->laravel->make(PidManager::class)->read()), + $sig + ); if ($wait) { $start = time(); do { - if (! $this->isRunning($pid)) { + if (! $this->isRunning()) { break; } @@ -264,78 +325,56 @@ protected function killProcess($pid, $sig, $wait = 0) } while (time() < $start + $wait); } - return $this->isRunning($pid); + return $this->isRunning(); } /** - * Get pid. - * - * @return int|null + * Return daemonize config. */ - protected function getPid() + protected function isDaemon(): bool { - if ($this->pid) { - return $this->pid; - } - - $pid = null; - $path = $this->getPidPath(); - - if (file_exists($path)) { - $pid = (int) file_get_contents($path); - - if (! $pid) { - $this->removePidFile(); - } else { - $this->pid = $pid; - } - } - - return $this->pid; + return Arr::get($this->config, 'server.options.daemonize', false); } /** - * Get Pid file path. - * - * @return string + * Check running enironment. */ - protected function getPidPath() + protected function checkEnvironment() { - return $this->configs['server']['options']['pid_file']; - } + if (OS::is(OS::WIN)) { + $this->error('Swoole extension doesn\'t support Windows OS.'); - /** - * Remove Pid file. - */ - protected function removePidFile() - { - if (file_exists($this->getPidPath())) { - unlink($this->getPidPath()); + exit(1); } - } - /** - * Return daemonize config. - */ - protected function isDaemon() - { - return $this->configs['server']['options']['daemonize']; + if (! extension_loaded('swoole')) { + $this->error('Can\'t detect Swoole extension installed.'); + + exit(1); + } + + if (! version_compare(swoole_version(), '4.3.1', 'ge')) { + $this->error('Your Swoole version must be higher than `4.3.1`.'); + + exit(1); + } } /** - * Check running enironment. + * Register access log services. */ - protected function checkEnvironment() + protected function registerAccessLog() { - if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') { - $this->error("Swoole extension doesn't support Windows OS yet."); - exit; - } elseif (! extension_loaded('swoole')) { - $this->error("Can't detect Swoole extension installed."); - exit; - } elseif (! version_compare(swoole_version(), '4.0.0', 'ge')) { - $this->error("Your Swoole version must be higher than 4.0 to use coroutine."); - exit; - } + $this->laravel->singleton(OutputStyle::class, function () { + return new OutputStyle($this->input, $this->output); + }); + + $this->laravel->singleton(AccessOutput::class, function () { + return new AccessOutput(new ConsoleOutput); + }); + + $this->laravel->singleton(AccessLog::class, function (Container $container) { + return new AccessLog($container->make(AccessOutput::class)); + }); } } diff --git a/src/Concerns/InteractsWithSwooleQueue.php b/src/Concerns/InteractsWithSwooleQueue.php new file mode 100644 index 00000000..efed9c26 --- /dev/null +++ b/src/Concerns/InteractsWithSwooleQueue.php @@ -0,0 +1,25 @@ +table = new SwooleTable; + $this->currentTable = new SwooleTable; $this->registerTables(); } @@ -26,7 +33,7 @@ protected function createTables() */ protected function registerTables() { - $tables = $this->container['config']->get('swoole_http.tables', []); + $tables = $this->container->make('config')->get('swoole_http.tables', []); foreach ($tables as $key => $value) { $table = new Table($value['size']); @@ -40,7 +47,7 @@ protected function registerTables() } $table->create(); - $this->table->add($key, $table); + $this->currentTable->add($key, $table); } } @@ -49,9 +56,12 @@ protected function registerTables() */ protected function bindSwooleTable() { - $this->app->singleton(SwooleTable::class, function () { - return $this->table; - }); - $this->app->alias(SwooleTable::class, 'swoole.table'); + if (! $this->app instanceof ConsoleApp) { + $this->app->singleton(SwooleTable::class, function () { + return $this->currentTable; + }); + + $this->app->alias(SwooleTable::class, 'swoole.table'); + } } } diff --git a/src/Concerns/InteractsWithWebsocket.php b/src/Concerns/InteractsWithWebsocket.php index c22e1cfc..6872529e 100644 --- a/src/Concerns/InteractsWithWebsocket.php +++ b/src/Concerns/InteractsWithWebsocket.php @@ -3,32 +3,47 @@ namespace SwooleTW\Http\Concerns; use Throwable; -use Swoole\Websocket\Frame; -use Swoole\Websocket\Server; use Illuminate\Pipeline\Pipeline; +use SwooleTW\Http\Server\Sandbox; use SwooleTW\Http\Websocket\Parser; -use Illuminate\Support\Facades\Facade; +use SwooleTW\Http\Websocket\Pusher; use SwooleTW\Http\Websocket\Websocket; use SwooleTW\Http\Transformers\Request; +use SwooleTW\Http\Server\Facades\Server; use SwooleTW\Http\Websocket\HandlerContract; +use Illuminate\Contracts\Container\Container; +use Swoole\WebSocket\Server as WebsocketServer; use SwooleTW\Http\Websocket\Rooms\RoomContract; - +use SwooleTW\Http\Exceptions\WebsocketNotSetInConfigException; + +/** + * Trait InteractsWithWebsocket + * + * @property \Illuminate\Contracts\Container\Container $container + * @property \Illuminate\Contracts\Container\Container $app + * @property array $types + */ trait InteractsWithWebsocket { /** * @var boolean */ - protected $isWebsocket = false; + protected $isServerWebsocket = false; /** - * @var SwooleTW\Http\Websocket\HandlerContract + * @var \SwooleTW\Http\Websocket\HandlerContract */ protected $websocketHandler; /** - * @var SwooleTW\Http\Websocket\Parser + * @var \SwooleTW\Http\Websocket\Parser */ - protected $parser; + protected $payloadParser; + + /** + * @var \SwooleTW\Http\Websocket\Rooms\RoomContract + */ + protected $websocketRoom; /** * Websocket server events. @@ -37,37 +52,60 @@ trait InteractsWithWebsocket */ protected $wsEvents = ['open', 'message', 'close']; + /** + * "onHandShake" listener. + * @param \Swoole\Http\Request $swooleRequest + * @param \Swoole\Http\Response $response + */ + public function onHandShake($swooleRequest, $response) + { + $this->onOpen( + $this->app->make(Server::class), + $swooleRequest, + $response + ); + } + /** * "onOpen" listener. * * @param \Swoole\Websocket\Server $server * @param \Swoole\Http\Request $swooleRequest + * @param \Swoole\Http\Response $response (optional) */ - public function onOpen($server, $swooleRequest) + public function onOpen($server, $swooleRequest, $response = null) { $illuminateRequest = Request::make($swooleRequest)->toIlluminate(); + $websocket = $this->app->make(Websocket::class); + $sandbox = $this->app->make(Sandbox::class); + $handshakeHandler = $this->app->make('config') + ->get('swoole_websocket.handshake.handler'); try { - $this->app['swoole.websocket']->reset(true)->setSender($swooleRequest->fd); + $websocket->reset(true)->setSender($swooleRequest->fd); // set currnt request to sandbox - $this->app['swoole.sandbox']->setRequest($illuminateRequest); + $sandbox->setRequest($illuminateRequest); // enable sandbox - $this->app['swoole.sandbox']->enable(); + $sandbox->enable(); + // call customized handshake handler + if ($response && ! $this->app->make($handshakeHandler)->handle($swooleRequest, $response)) { + return; + } // check if socket.io connection established if (! $this->websocketHandler->onOpen($swooleRequest->fd, $illuminateRequest)) { return; } // trigger 'connect' websocket event - if ($this->app['swoole.websocket']->eventExists('connect')) { + if ($websocket->eventExists('connect')) { // set sandbox container to websocket pipeline - $this->app['swoole.websocket']->setContainer($this->app['swoole.sandbox']->getApplication()); - $this->app['swoole.websocket']->call('connect', $illuminateRequest); + $websocket->setContainer($sandbox->getApplication()); + $websocket->call('connect', $illuminateRequest); } } catch (Throwable $e) { $this->logServerError($e); } finally { // disable and recycle sandbox resource - $this->app['swoole.sandbox']->disable(); + $sandbox->disable(); } } @@ -79,31 +117,33 @@ public function onOpen($server, $swooleRequest) */ public function onMessage($server, $frame) { - try { - // execute parser strategies and skip non-message packet - if ($this->parser->execute($server, $frame)) { - return; - } + // execute parser strategies and skip non-message packet + if ($this->payloadParser->execute($server, $frame)) { + return; + } + $websocket = $this->app->make(Websocket::class); + $sandbox = $this->app->make(Sandbox::class); + + try { // decode raw message via parser - $payload = $this->parser->decode($frame); + $payload = $this->payloadParser->decode($frame); - $this->app['swoole.websocket']->reset(true)->setSender($frame->fd); + $websocket->reset(true)->setSender($frame->fd); // enable sandbox - $this->app['swoole.sandbox']->enable(); + $sandbox->enable(); // dispatch message to registered event callback - if ($this->app['swoole.websocket']->eventExists($payload['event'])) { - $this->app['swoole.websocket']->call($payload['event'], $payload['data']); - } else { - $this->websocketHandler->onMessage($frame); - } + ['event' => $event, 'data' => $data] = $payload; + $websocket->eventExists($event) + ? $websocket->call($event, $data) + : $this->websocketHandler->onMessage($frame); } catch (Throwable $e) { $this->logServerError($e); } finally { // disable and recycle sandbox resource - $this->app['swoole.sandbox']->disable(); + $sandbox->disable(); } } @@ -116,25 +156,46 @@ public function onMessage($server, $frame) */ public function onClose($server, $fd, $reactorId) { - if (! $this->isWebsocket($fd)) { + if (! $this->isServerWebsocket($fd) || ! $server instanceof WebsocketServer) { return; } + $websocket = $this->app->make(Websocket::class); + try { - $this->app['swoole.websocket']->reset(true)->setSender($fd); + $websocket->reset(true)->setSender($fd); // trigger 'disconnect' websocket event - if ($this->app['swoole.websocket']->eventExists('disconnect')) { - $this->app['swoole.websocket']->call('disconnect'); + if ($websocket->eventExists('disconnect')) { + $websocket->call('disconnect'); } else { $this->websocketHandler->onClose($fd, $reactorId); } // leave all rooms - $this->app['swoole.websocket']->leave(); + $websocket->leave(); } catch (Throwable $e) { $this->logServerError($e); } } + /** + * Indicates if a packet is websocket push action. + * + * @param mixed + * + * @return bool + */ + protected function isWebsocketPushPacket($packet) + { + if (! is_array($packet)) { + return false; + } + + return $this->isServerWebsocket + && array_key_exists('action', $packet) + && $packet['action'] === Websocket::PUSH_ACTION; + } + + /** * Push websocket message to clients. * @@ -143,40 +204,23 @@ public function onClose($server, $fd, $reactorId) */ public function pushMessage($server, array $data) { - [$opcode, $sender, $fds, $broadcast, $assigned, $event, $message] = $this->normalizePushData($data); - $message = $this->parser->encode($event, $message); - - // attach sender if not broadcast - if (! $broadcast && $sender && ! in_array($sender, $fds)) { - $fds[] = $sender; - } - - // check if to broadcast all clients - if ($broadcast && empty($fds) && ! $assigned) { - foreach ($server->connections as $fd) { - if ($this->isWebsocket($fd)) { - $fds[] = $fd; - } - } - } - - // push message to designated fds - foreach ($fds as $fd) { - if (($broadcast && $sender === (integer) $fd) || ! $server->exist($fd)) { - continue; - } - $server->push($fd, $message, $opcode); - } + $pusher = Pusher::make($data, $server); + $pusher->push($this->payloadParser->encode( + $pusher->getEvent(), + $pusher->getMessage() + )); } /** * Set frame parser for websocket. * - * @param \SwooleTW\Http\Websocket\Parser $parser + * @param \SwooleTW\Http\Websocket\Parser $payloadParser + * + * @return \SwooleTW\Http\Concerns\InteractsWithWebsocket */ - public function setParser(Parser $parser) + public function setPayloadParser(Parser $payloadParser) { - $this->parser = $parser; + $this->payloadParser = $payloadParser; return $this; } @@ -184,9 +228,9 @@ public function setParser(Parser $parser) /** * Get frame parser for websocket. */ - public function getParser() + public function getPayloadParser() { - return $this->parser; + return $this->payloadParser; } /** @@ -194,42 +238,74 @@ public function getParser() */ protected function prepareWebsocket() { - $isWebsocket = $this->container['config']->get('swoole_http.websocket.enabled'); - $parser = $this->container['config']->get('swoole_websocket.parser'); + $config = $this->container->make('config'); + $parser = $config->get('swoole_websocket.parser'); - if ($isWebsocket) { - array_push($this->events, ...$this->wsEvents); - $this->isWebsocket = true; - $this->setParser(new $parser); + if (! $this->isServerWebsocket = $config->get('swoole_http.websocket.enabled')) { + return; } + + if ($config->get('swoole_websocket.handshake.enabled')) { + $this->wsEvents = array_merge($this->wsEvents, ['handshake']); + } + + $this->events = array_merge($this->events ?? [], $this->wsEvents); + $this->prepareWebsocketRoom(); + $this->setPayloadParser(new $parser); } /** * Check if it's a websocket fd. + * + * @param int $fd + * + * @return bool */ - protected function isWebsocket(int $fd) + protected function isServerWebsocket(int $fd): bool { - $info = $this->container['swoole.server']->connection_info($fd); - - return array_key_exists('websocket_status', $info) && $info['websocket_status']; + return array_key_exists( + 'websocket_status', + $this->container->make(Server::class) + ->connection_info($fd) + ); } /** * Prepare websocket handler for onOpen and onClose callback. + * + * @throws \Exception */ protected function prepareWebsocketHandler() { - $handlerClass = $this->container['config']->get('swoole_websocket.handler'); + $handlerClass = $this->container->make('config')->get('swoole_websocket.handler'); if (! $handlerClass) { - throw new Exception('Websocket handler is not set in swoole_websocket config'); + throw new WebsocketNotSetInConfigException; } $this->setWebsocketHandler($this->app->make($handlerClass)); } + /** + * Prepare websocket room. + */ + protected function prepareWebsocketRoom() + { + $config = $this->container->make('config'); + $driver = $config->get('swoole_websocket.default'); + $websocketConfig = $config->get("swoole_websocket.settings.{$driver}"); + $className = $config->get("swoole_websocket.drivers.{$driver}"); + + $this->websocketRoom = new $className($websocketConfig); + $this->websocketRoom->prepare(); + } + /** * Set websocket handler. + * + * @param \SwooleTW\Http\Websocket\HandlerContract $handler + * + * @return \SwooleTW\Http\Concerns\InteractsWithWebsocket */ public function setWebsocketHandler(HandlerContract $handler) { @@ -240,35 +316,34 @@ public function setWebsocketHandler(HandlerContract $handler) /** * Get websocket handler. + * + * @return \SwooleTW\Http\Websocket\HandlerContract */ - public function getWebsocketHandler() + public function getWebsocketHandler(): HandlerContract { return $this->websocketHandler; } /** - * Get websocket handler for onOpen and onClose callback. + * @param string $class + * @param array $settings + * + * @return \SwooleTW\Http\Websocket\Rooms\RoomContract */ - protected function getWebsocketRoom() + protected function createRoom(string $class, array $settings): RoomContract { - $driver = $this->container['config']->get('swoole_websocket.default'); - $configs = $this->container['config']->get("swoole_websocket.settings.{$driver}"); - $className = $this->container['config']->get("swoole_websocket.drivers.{$driver}"); - - $websocketRoom = new $className($configs); - $websocketRoom->prepare(); - - return $websocketRoom; + return new $class($settings); } /** * Bind room instance to Laravel app container. */ - protected function bindRoom() + protected function bindRoom(): void { - $this->app->singleton(RoomContract::class, function ($app) { - return $this->getWebsocketRoom(); + $this->app->singleton(RoomContract::class, function () { + return $this->websocketRoom; }); + $this->app->alias(RoomContract::class, 'swoole.room'); } @@ -277,9 +352,10 @@ protected function bindRoom() */ protected function bindWebsocket() { - $this->app->singleton(Websocket::class, function ($app) { - return new Websocket($app['swoole.room'], new Pipeline($app)); + $this->app->singleton(Websocket::class, function (Container $app) { + return new Websocket($app->make(RoomContract::class), new Pipeline($app)); }); + $this->app->alias(Websocket::class, 'swoole.websocket'); } @@ -288,7 +364,8 @@ protected function bindWebsocket() */ protected function loadWebsocketRoutes() { - $routePath = $this->container['config']->get('swoole_websocket.route_file'); + $routePath = $this->container->make('config') + ->get('swoole_websocket.route_file'); if (! file_exists($routePath)) { $routePath = __DIR__ . '/../../routes/websocket.php'; @@ -298,18 +375,20 @@ protected function loadWebsocketRoutes() } /** - * Normalize data for message push. + * Indicates if the payload is websocket push. + * + * @param mixed $payload + * + * @return boolean */ - public function normalizePushData(array $data) + public function isWebsocketPushPayload($payload): bool { - $opcode = $data['opcode'] ?? 1; - $sender = $data['sender'] ?? 0; - $fds = $data['fds'] ?? []; - $broadcast = $data['broadcast'] ?? false; - $assigned = $data['assigned'] ?? false; - $event = $data['event'] ?? null; - $message = $data['message'] ?? null; - - return [$opcode, $sender, $fds, $broadcast, $assigned, $event, $message]; + if (! is_array($payload)) { + return false; + } + + return $this->isServerWebsocket + && ($payload['action'] ?? null) === Websocket::PUSH_ACTION + && array_key_exists('data', $payload); } } diff --git a/src/Concerns/ResetApplication.php b/src/Concerns/ResetApplication.php index 6f86569f..d6a49593 100644 --- a/src/Concerns/ResetApplication.php +++ b/src/Concerns/ResetApplication.php @@ -2,6 +2,7 @@ namespace SwooleTW\Http\Concerns; +use Illuminate\Contracts\Config\Repository; use Illuminate\Contracts\Container\Container; use SwooleTW\Http\Exceptions\SandboxException; use SwooleTW\Http\Server\Resetters\ResetterContract; @@ -19,7 +20,7 @@ trait ResetApplication protected $providers = []; /** - * @var array + * @var \SwooleTW\Http\Server\Resetters\ResetterContract[]|array */ protected $resetters = []; @@ -28,7 +29,7 @@ trait ResetApplication */ protected function setInitialConfig() { - $this->config = clone $this->getBaseApp()->make('config'); + $this->config = clone $this->getBaseApp()->make(Repository::class); } /** @@ -90,6 +91,8 @@ public function getResetters() /** * Reset Laravel/Lumen Application. + * + * @param \Illuminate\Contracts\Container\Container $app */ public function resetApp(Container $app) { diff --git a/src/Concerns/WithApplication.php b/src/Concerns/WithApplication.php index d34385dc..8a09a1fb 100644 --- a/src/Concerns/WithApplication.php +++ b/src/Concerns/WithApplication.php @@ -2,21 +2,23 @@ namespace SwooleTW\Http\Concerns; -use Illuminate\Http\Request; use Illuminate\Contracts\Http\Kernel; use Illuminate\Support\Facades\Facade; use Illuminate\Contracts\Container\Container; -use Laravel\Lumen\Application as LumenApplication; -use Symfony\Component\HttpFoundation\StreamedResponse; -use Symfony\Component\HttpFoundation\BinaryFileResponse; -use Symfony\Component\HttpFoundation\Response as SymfonyResponse; - +use SwooleTW\Http\Exceptions\FrameworkNotSupportException; + +/** + * Trait WithApplication + * + * @property Container $container + * @property string $framework + */ trait WithApplication { /** * Laravel|Lumen Application. * - * @var \Illuminate\Contracts\Container\Container + * @var \Illuminate\Contracts\Container\Container|mixed */ protected $app; @@ -28,7 +30,12 @@ protected function bootstrap() if ($this->framework === 'laravel') { $bootstrappers = $this->getBootstrappers(); $this->app->bootstrapWith($bootstrappers); - } elseif (is_null(Facade::getFacadeApplication())) { + } else { + // for Lumen 5.7 + // https://github.com/laravel/lumen-framework/commit/42cbc998375718b1a8a11883e033617024e57260#diff-c9248b3167fc44af085b81db2e292837 + if (method_exists($this->app, 'boot')) { + $this->app->boot(); + } $this->app->withFacades(); } @@ -42,11 +49,12 @@ protected function bootstrap() */ protected function loadApplication() { - return require $this->basePath . '/bootstrap/app.php'; + return require "{$this->basePath}/bootstrap/app.php"; } /** - * @return \Illuminate\Contracts\Container\Container + * @return \Illuminate\Contracts\Container\Container|mixed + * @throws \ReflectionException */ public function getApplication() { @@ -60,37 +68,19 @@ public function getApplication() /** * Set laravel application. + * + * @param \Illuminate\Contracts\Container\Container $app */ public function setApplication(Container $app) { $this->app = $app; } - /** - * Get bootstrappers. - * - * @return array - */ - protected function getBootstrappers() - { - $kernel = $this->getApplication()->make(Kernel::class); - - $reflection = new \ReflectionObject($kernel); - - $bootstrappersMethod = $reflection->getMethod('bootstrappers'); - $bootstrappersMethod->setAccessible(true); - - $bootstrappers = $bootstrappersMethod->invoke($kernel); - - array_splice($bootstrappers, -2, 0, ['Illuminate\Foundation\Bootstrap\SetRequestForConsole']); - - return $bootstrappers; - } - /** * Set framework. * * @param string $framework + * * @throws \Exception */ protected function setFramework($framework) @@ -98,7 +88,7 @@ protected function setFramework($framework) $framework = strtolower($framework); if (! in_array($framework, ['laravel', 'lumen'])) { - throw new \Exception(sprintf('Not support framework "%s".', $framework)); + throw new FrameworkNotSupportException($framework); } $this->framework = $framework; @@ -132,10 +122,13 @@ public function getBasePath() /** * Reslove some instances before request. + * + * @throws \ReflectionException */ protected function preResolveInstances() { - $resolves = $this->container['config']->get('swoole_http.pre_resolved', []); + $resolves = $this->container->make('config') + ->get('swoole_http.pre_resolved', []); foreach ($resolves as $abstract) { if ($this->getApplication()->offsetExists($abstract)) { @@ -143,4 +136,24 @@ protected function preResolveInstances() } } } + + /** + * Get bootstrappers. + * + * @return array + * @throws \ReflectionException + */ + protected function getBootstrappers() + { + $kernel = $this->getApplication()->make(Kernel::class); + + $reflection = new \ReflectionObject($kernel); + $bootstrappersMethod = $reflection->getMethod('bootstrappers'); + $bootstrappersMethod->setAccessible(true); + $bootstrappers = $bootstrappersMethod->invoke($kernel); + + array_splice($bootstrappers, -2, 0, ['Illuminate\Foundation\Bootstrap\SetRequestForConsole']); + + return $bootstrappers; + } } diff --git a/src/Controllers/SocketIOController.php b/src/Controllers/SocketIOController.php index be16a5ac..159adfd8 100644 --- a/src/Controllers/SocketIOController.php +++ b/src/Controllers/SocketIOController.php @@ -12,31 +12,39 @@ class SocketIOController public function upgrade(Request $request) { if (! in_array($request->input('transport'), $this->transports)) { - return response()->json([ - 'code' => 0, - 'message' => 'Transport unknown' - ], 400); + return response()->json( + [ + 'code' => 0, + 'message' => 'Transport unknown', + ], + 400 + ); } if ($request->has('sid')) { return '1:6'; } - $payload = json_encode([ - 'sid' => base64_encode(uniqid()), - 'upgrades' => ['websocket'], - 'pingInterval' => Config::get('swoole_websocket.ping_interval'), - 'pingTimeout' => Config::get('swoole_websocket.ping_timeout') - ]); + $payload = json_encode( + [ + 'sid' => base64_encode(uniqid()), + 'upgrades' => ['websocket'], + 'pingInterval' => Config::get('swoole_websocket.ping_interval'), + 'pingTimeout' => Config::get('swoole_websocket.ping_timeout'), + ] + ); return '97:0' . $payload . '2:40'; } - public function reject(Request $request) + public function reject() { - return response()->json([ - 'code' => 3, - 'message' => 'Bad request' - ], 400); + return response()->json( + [ + 'code' => 3, + 'message' => 'Bad request', + ], + 400 + ); } } diff --git a/src/Coroutine/Connectors/ConnectorFactory.php b/src/Coroutine/Connectors/ConnectorFactory.php new file mode 100644 index 00000000..fd402338 --- /dev/null +++ b/src/Coroutine/Connectors/ConnectorFactory.php @@ -0,0 +1,105 @@ +getDocComment(), $result)) { + $fileVersion = Arr::first($result); + } + } + + return version_compare($fileVersion, $version, '>='); + } catch (\Exception $e) { + return false; + } + } + + /** + * @param string $version + * + * @return bool + */ + protected static function hasBreakingChanges(string $version): bool + { + return version_compare($version, self::CHANGE_VERSION, '>='); + } +} diff --git a/src/Coroutine/Connectors/MySqlConnector.php b/src/Coroutine/Connectors/MySqlConnector.php index 627aadd8..2efa720a 100644 --- a/src/Coroutine/Connectors/MySqlConnector.php +++ b/src/Coroutine/Connectors/MySqlConnector.php @@ -2,21 +2,26 @@ namespace SwooleTW\Http\Coroutine\Connectors; -use Exception; +use Illuminate\Database\Connectors\MySqlConnector as BaseConnector; use Illuminate\Support\Str; use SwooleTW\Http\Coroutine\PDO as SwoolePDO; -use Illuminate\Database\Connectors\MySqlConnector as BaseConnector; +use Throwable; +/** + * Class MySqlConnector (5.6) + */ class MySqlConnector extends BaseConnector { /** * Create a new PDO connection instance. * - * @param string $dsn - * @param string $username - * @param string $password - * @param array $options + * @param string $dsn + * @param string $username + * @param string $password + * @param array $options + * * @return \PDO + * @throws \SwooleTW\Http\Coroutine\ConnectionException */ protected function createPdoConnection($dsn, $username, $password, $options) { @@ -26,16 +31,16 @@ protected function createPdoConnection($dsn, $username, $password, $options) /** * Handle an exception that occurred during connect execution. * - * @param \Exception $e - * @param string $dsn - * @param string $username - * @param string $password - * @param array $options - * @return \PDO + * @param \Throwable $e + * @param string $dsn + * @param string $username + * @param string $password + * @param array $options * - * @throws \Exception + * @return \PDO + * @throws \Throwable */ - protected function tryAgainIfCausedByLostConnection(Exception $e, $dsn, $username, $password, $options) + protected function tryAgainIfCausedByLostConnection(Throwable $e, $dsn, $username, $password, $options) { // https://github.com/swoole/swoole-src/blob/a414e5e8fec580abb3dbd772d483e12976da708f/swoole_mysql_coro.c#L196 if ($this->causedByLostConnection($e) || Str::contains($e->getMessage(), 'is closed')) { diff --git a/src/Coroutine/Context.php b/src/Coroutine/Context.php index a2f49a34..2ea8a20c 100644 --- a/src/Coroutine/Context.php +++ b/src/Coroutine/Context.php @@ -2,8 +2,8 @@ namespace SwooleTW\Http\Coroutine; -use Swoole\Coroutine; use Illuminate\Contracts\Container\Container; +use Swoole\Coroutine; class Context { @@ -31,6 +31,8 @@ public static function getApp() /** * Set app container by current coroutine id. + * + * @param \Illuminate\Contracts\Container\Container $app */ public static function setApp(Container $app) { @@ -39,6 +41,10 @@ public static function setApp(Container $app) /** * Get data by current coroutine id. + * + * @param string $key + * + * @return mixed|null */ public static function getData(string $key) { @@ -47,6 +53,9 @@ public static function getData(string $key) /** * Set data by current coroutine id. + * + * @param string $key + * @param $value */ public static function setData(string $key, $value) { @@ -55,6 +64,8 @@ public static function setData(string $key, $value) /** * Remove data by current coroutine id. + * + * @param string $key */ public static function removeData(string $key) { @@ -62,7 +73,7 @@ public static function removeData(string $key) } /** - * Get data by current coroutine id. + * Get data keys by current coroutine id. */ public static function getDataKeys() { @@ -70,7 +81,7 @@ public static function getDataKeys() } /** - * Get data by current coroutine id. + * Clear data by current coroutine id. */ public static function clear() { diff --git a/src/Coroutine/MySqlConnection.php b/src/Coroutine/MySqlConnection.php index be73c294..631ad6bb 100644 --- a/src/Coroutine/MySqlConnection.php +++ b/src/Coroutine/MySqlConnection.php @@ -3,19 +3,20 @@ namespace SwooleTW\Http\Coroutine; use Closure; -use Illuminate\Support\Str; -use Illuminate\Database\QueryException; use Illuminate\Database\MySqlConnection as BaseConnection; +use Illuminate\Database\QueryException; +use Illuminate\Support\Str; class MySqlConnection extends BaseConnection { /** * Handle a query exception that occurred during query execution. * - * @param \Illuminate\Database\QueryException $e - * @param string $query - * @param array $bindings - * @param \Closure $callback + * @param \Illuminate\Database\QueryException $e + * @param string $query + * @param array $bindings + * @param \Closure $callback + * * @return mixed * * @throws \Illuminate\Database\QueryException @@ -23,7 +24,10 @@ class MySqlConnection extends BaseConnection protected function tryAgainIfCausedByLostConnection(QueryException $e, $query, $bindings, Closure $callback) { // https://github.com/swoole/swoole-src/blob/a414e5e8fec580abb3dbd772d483e12976da708f/swoole_mysql_coro.c#L1140 - if ($this->causedByLostConnection($e->getPrevious()) || Str::contains($e->getMessage(), ['is closed', 'is not established'])) { + if ($this->causedByLostConnection($e->getPrevious()) || Str::contains( + $e->getMessage(), + ['is closed', 'is not established'] + )) { $this->reconnect(); return $this->runQueryCallback($query, $bindings, $callback); diff --git a/src/Coroutine/PDO.php b/src/Coroutine/PDO.php index 5e4a7aeb..2ee646f2 100644 --- a/src/Coroutine/PDO.php +++ b/src/Coroutine/PDO.php @@ -10,26 +10,28 @@ namespace SwooleTW\Http\Coroutine; use Exception; -use PDO as BasePDO; use Illuminate\Database\QueryException; -use SwooleTW\Http\Coroutine\PDOStatement; -use SwooleTW\Http\Coroutine\StatementException; -use SwooleTW\Http\Coroutine\ConnectionException; +use Illuminate\Support\Arr; +use PDO as BasePDO; +/** + * Class PDO + */ class PDO extends BasePDO { public static $keyMap = [ - 'dbname' => 'database' + 'dbname' => 'database', ]; - private static $defaultOptions = [ + private static $options = [ 'host' => '', 'port' => 3306, 'user' => '', 'password' => '', 'database' => '', 'charset' => 'utf8mb4', - 'strict_type' => true + 'strict_type' => true, + 'timeout' => -1, ]; /** @var \Swoole\Coroutine\Mysql */ @@ -37,21 +39,37 @@ class PDO extends BasePDO public $inTransaction = false; - public function __construct( - string $dsn, - string $username = '', - string $password = '', - array $driverOptions = [] - ) { + /** + * PDO constructor. + * + * @param string $dsn + * @param string $username + * @param string $password + * @param array $options + * + * @throws \SwooleTW\Http\Coroutine\ConnectionException + */ + public function __construct(string $dsn, string $username = '', string $password = '', array $options = []) + { + parent::__construct($dsn, $username, $password, $options); $this->setClient(); $this->connect($this->getOptions(...func_get_args())); } + /** + * @param mixed $client + */ protected function setClient($client = null) { - $this->client = $client ?: new \Swoole\Coroutine\Mysql(); + $this->client = $client ?: new \Swoole\Coroutine\Mysql; } + /** + * @param array $options + * + * @return $this + * @throws \SwooleTW\Http\Coroutine\ConnectionException + */ protected function connect(array $options = []) { $this->client->connect($options); @@ -66,19 +84,27 @@ protected function connect(array $options = []) return $this; } + /** + * @param $dsn + * @param $username + * @param $password + * @param $driverOptions + * + * @return array + */ protected function getOptions($dsn, $username, $password, $driverOptions) { $dsn = explode(':', $dsn); $driver = ucwords(array_shift($dsn)); $dsn = explode(';', implode(':', $dsn)); - $options = []; + $configuredOptions = []; static::checkDriver($driver); foreach ($dsn as $kv) { $kv = explode('=', $kv); - if ($kv) { - $options[$kv[0]] = $kv[1] ?? ''; + if (count($kv)) { + $configuredOptions[$kv[0]] = $kv[1] ?? ''; } } @@ -87,18 +113,21 @@ protected function getOptions($dsn, $username, $password, $driverOptions) 'password' => $password, ]; - $options = $driverOptions + $authorization + $options; + $configuredOptions = $driverOptions + $authorization + $configuredOptions; foreach (static::$keyMap as $pdoKey => $swpdoKey) { - if (isset($options[$pdoKey])) { - $options[$swpdoKey] = $options[$pdoKey]; - unset($options[$pdoKey]); + if (isset($configuredOptions[$pdoKey])) { + $configuredOptions[$swpdoKey] = $configuredOptions[$pdoKey]; + unset($configuredOptions[$pdoKey]); } } - return $options + static::$defaultOptions; + return array_merge(static::$options, $configuredOptions); } + /** + * @param string $driver + */ public static function checkDriver(string $driver) { if (! in_array($driver, static::getAvailableDrivers())) { @@ -106,44 +135,70 @@ public static function checkDriver(string $driver) } } + /** + * @return array + */ public static function getAvailableDrivers() { return ['Mysql']; } + /** + * @return bool|void + */ public function beginTransaction() { $this->client->begin(); $this->inTransaction = true; } + /** + * @return bool|void + */ public function rollBack() { $this->client->rollback(); $this->inTransaction = false; } + /** + * @return bool|void + */ public function commit() { $this->client->commit(); $this->inTransaction = true; } + /** + * @return bool + */ public function inTransaction() { return $this->inTransaction; } + /** + * @param null $seqname + * + * @return int|string + */ public function lastInsertId($seqname = null) { return $this->client->insert_id; } + /** + * @return mixed|void + */ public function errorCode() { $this->client->errno; } + /** + * @return array + */ public function errorInfo() { return [ @@ -153,6 +208,11 @@ public function errorInfo() ]; } + /** + * @param string $statement + * + * @return int + */ public function exec($statement): int { $this->query($statement); @@ -160,9 +220,17 @@ public function exec($statement): int return $this->client->affected_rows; } - public function query(string $statement, float $timeout = -1) + /** + * @param string $statement + * @param int $mode + * @param mixed $arg3 + * @param array $ctorargs + * + * @return array|bool|false|\PDOStatement + */ + public function query($statement, $mode = PDO::ATTR_DEFAULT_FETCH_MODE, $arg3 = null, array $ctorargs = []) { - $result = $this->client->query($statement, $timeout); + $result = $this->client->query($statement, Arr::get(self::$options, 'timeout')); if ($result === false) { $exception = new Exception($this->client->error, $this->client->errno); @@ -172,39 +240,42 @@ public function query(string $statement, float $timeout = -1) return $result; } - private function rewriteToPosition(string $statement) + /** + * @param string $statement + * @param array $options + * + * @return bool|\PDOStatement|\SwooleTW\Http\Coroutine\PDOStatement + */ + public function prepare($statement, $options = null) { - // - } - - public function prepare($statement, $driverOptions = null) - { - $driverOptions = is_null($driverOptions) ? [] : $driverOptions; + $options = is_null($options) ? [] : $options; if (strpos($statement, ':') !== false) { $i = 0; $bindKeyMap = []; - $statement = preg_replace_callback( - '/:(\w+)\b/', - function ($matches) use (&$i, &$bindKeyMap) { - $bindKeyMap[$matches[1]] = $i++; - - return '?'; - }, - $statement - ); + $statement = preg_replace_callback('/:([a-zA-Z_]\w*?)\b/', function ($matches) use (&$i, &$bindKeyMap) { + $bindKeyMap[$matches[1]] = $i++; + + return '?'; + }, $statement); } $stmtObj = $this->client->prepare($statement); if ($stmtObj) { $stmtObj->bindKeyMap = $bindKeyMap ?? []; - return new PDOStatement($this, $stmtObj, $driverOptions); + + return new PDOStatement($this, $stmtObj, $options); } else { $statementException = new StatementException($this->client->error, $this->client->errno); throw new QueryException($statement, [], $statementException); } } + /** + * @param int $attribute + * + * @return bool|mixed|string + */ public function getAttribute($attribute) { switch ($attribute) { @@ -221,7 +292,7 @@ public function getAttribute($attribute) case \PDO::ATTR_PERSISTENT: case \PDO::ATTR_PREFETCH: case \PDO::ATTR_SERVER_INFO: - return $this->serverInfo['timeout'] ?? static::$defaultOptions['timeout']; + return self::$options['timeout']; case \PDO::ATTR_SERVER_VERSION: return 'Swoole Mysql'; case \PDO::ATTR_TIMEOUT: @@ -230,9 +301,16 @@ public function getAttribute($attribute) } } + /** + * @param string $string + * @param null $paramtype + * + * @return string|void + */ public function quote($string, $paramtype = null) { - throw new \BadMethodCallException(<<client->close(); diff --git a/src/Coroutine/PDOStatement.php b/src/Coroutine/PDOStatement.php index bec3fa1a..fcd605ab 100644 --- a/src/Coroutine/PDOStatement.php +++ b/src/Coroutine/PDOStatement.php @@ -10,18 +10,24 @@ namespace SwooleTW\Http\Coroutine; use PDOStatement as BaseStatement; -use SwooleTW\Http\Coroutine\PDO; use Swoole\Coroutine\MySQL\Statement; class PDOStatement extends BaseStatement { private $parent; + public $statement; + public $timeout; + public $bindMap = []; + public $cursor = -1; + public $cursorOrientation = PDO::FETCH_ORI_NEXT; + public $resultSet = []; + public $fetchStyle = PDO::FETCH_BOTH; public function __construct(PDO $parent, Statement $statement, array $driverOptions = []) @@ -95,7 +101,9 @@ public function execute($inputParameters = null) $inputParameters = []; if (! empty($this->statement->bindKeyMap)) { foreach ($this->statement->bindKeyMap as $nameKey => $numKey) { - $inputParameters[$numKey] = $this->bindMap[$nameKey]; + if (isset($this->bindMap[$nameKey])) { + $inputParameters[$numKey] = $this->bindMap[$nameKey]; + } } } else { $inputParameters = $this->bindMap; @@ -105,6 +113,10 @@ public function execute($inputParameters = null) $this->resultSet = ($ok = $result !== false) ? $result : []; $this->afterExecute(); + if ($result === false) { + throw new \PDOException($this->errorInfo(), $this->errorCode()); + } + return $ok; } @@ -142,7 +154,8 @@ private function transStyle( $fetchStyle = null, $fetchArgument = null, $ctorArgs = null - ) { + ) + { if (! is_array($rawData)) { return false; } @@ -187,7 +200,8 @@ public function fetch( $cursorOrientation = null, $cursorOffset = null, $fetchArgument = null - ) { + ) + { $this->__executeWhenStringQueryEmpty(); $cursorOrientation = is_null($cursorOrientation) ? PDO::FETCH_ORI_NEXT : $cursorOrientation; @@ -222,7 +236,8 @@ public function fetch( /** * Returns a single column from the next row of a result set or FALSE if there are no more rows. * - * @param int $column_number + * @param int|null $columnNumber + * * 0-indexed number of the column you wish to retrieve from the row. * If no value is supplied, PDOStatement::fetchColumn() fetches the first column. * @@ -232,6 +247,7 @@ public function fetchColumn($columnNumber = null) { $columnNumber = is_null($columnNumber) ? 0 : $columnNumber; $this->__executeWhenStringQueryEmpty(); + return $this->fetch(PDO::FETCH_COLUMN, PDO::FETCH_ORI_NEXT, 0, $columnNumber); } diff --git a/src/Exceptions/FrameworkNotSupportException.php b/src/Exceptions/FrameworkNotSupportException.php new file mode 100644 index 00000000..f393279c --- /dev/null +++ b/src/Exceptions/FrameworkNotSupportException.php @@ -0,0 +1,21 @@ +dump( + static::$cloner->cloneVar($arg) + ); + } + + return true; + } + + public static function getDumper() + { + $dumper = defined('IN_PHPUNIT') || ! config('swoole_http.ob_output') + ? CliDumper::class + : HtmlDumper::class; + + return new $dumper; + } +} diff --git a/src/Helpers/FW.php b/src/Helpers/FW.php new file mode 100644 index 00000000..818e5189 --- /dev/null +++ b/src/Helpers/FW.php @@ -0,0 +1,102 @@ +version(), $expression)) { + return $version; + } + + throw new LogicException('No any version found.'); + } + + /** + * Extracts lumen version from $app->version() + * + * @param string $version + * @param string $expression + * + * @return string|null + */ + protected static function extractVersion(string $version, string $expression): ?string + { + if (preg_match($expression, $version, $result)) { + return Arr::first($result); + } + + return null; + } +} \ No newline at end of file diff --git a/src/Helpers/MimeType.php b/src/Helpers/MimeType.php new file mode 100644 index 00000000..7132b629 --- /dev/null +++ b/src/Helpers/MimeType.php @@ -0,0 +1,821 @@ + 'application/andrew-inset', + 'aw' => 'application/applixware', + 'atom' => 'application/atom+xml', + 'atomcat' => 'application/atomcat+xml', + 'atomsvc' => 'application/atomsvc+xml', + 'ccxml' => 'application/ccxml+xml', + 'cdmia' => 'application/cdmi-capability', + 'cdmic' => 'application/cdmi-container', + 'cdmid' => 'application/cdmi-domain', + 'cdmio' => 'application/cdmi-object', + 'cdmiq' => 'application/cdmi-queue', + 'cu' => 'application/cu-seeme', + 'davmount' => 'application/davmount+xml', + 'dbk' => 'application/docbook+xml', + 'dssc' => 'application/dssc+der', + 'xdssc' => 'application/dssc+xml', + 'ecma' => 'application/ecmascript', + 'emma' => 'application/emma+xml', + 'epub' => 'application/epub+zip', + 'exi' => 'application/exi', + 'pfr' => 'application/font-tdpfr', + 'gml' => 'application/gml+xml', + 'gpx' => 'application/gpx+xml', + 'gxf' => 'application/gxf', + 'stk' => 'application/hyperstudio', + 'ink' => 'application/inkml+xml', + 'ipfix' => 'application/ipfix', + 'jar' => 'application/java-archive', + 'ser' => 'application/java-serialized-object', + 'class' => 'application/java-vm', + 'js' => 'application/javascript', + 'json' => 'application/json', + 'jsonml' => 'application/jsonml+json', + 'lostxml' => 'application/lost+xml', + 'hqx' => 'application/mac-binhex40', + 'cpt' => 'application/mac-compactpro', + 'mads' => 'application/mads+xml', + 'mrc' => 'application/marc', + 'mrcx' => 'application/marcxml+xml', + 'ma' => 'application/mathematica', + 'mathml' => 'application/mathml+xml', + 'mbox' => 'application/mbox', + 'mscml' => 'application/mediaservercontrol+xml', + 'metalink' => 'application/metalink+xml', + 'meta4' => 'application/metalink4+xml', + 'mets' => 'application/mets+xml', + 'mods' => 'application/mods+xml', + 'm21' => 'application/mp21', + 'mp4s' => 'application/mp4', + 'doc' => 'application/msword', + 'mxf' => 'application/mxf', + 'bin' => 'application/octet-stream', + 'oda' => 'application/oda', + 'opf' => 'application/oebps-package+xml', + 'ogx' => 'application/ogg', + 'omdoc' => 'application/omdoc+xml', + 'onetoc' => 'application/onenote', + 'oxps' => 'application/oxps', + 'xer' => 'application/patch-ops-error+xml', + 'pdf' => 'application/pdf', + 'pgp' => 'application/pgp-encrypted', + 'asc' => 'application/pgp-signature', + 'prf' => 'application/pics-rules', + 'p10' => 'application/pkcs10', + 'p7m' => 'application/pkcs7-mime', + 'p7s' => 'application/pkcs7-signature', + 'p8' => 'application/pkcs8', + 'ac' => 'application/pkix-attr-cert', + 'cer' => 'application/pkix-cert', + 'crl' => 'application/pkix-crl', + 'pkipath' => 'application/pkix-pkipath', + 'pki' => 'application/pkixcmp', + 'pls' => 'application/pls+xml', + 'ai' => 'application/postscript', + 'cww' => 'application/prs.cww', + 'pskcxml' => 'application/pskc+xml', + 'rdf' => 'application/rdf+xml', + 'rif' => 'application/reginfo+xml', + 'rnc' => 'application/relax-ng-compact-syntax', + 'rl' => 'application/resource-lists+xml', + 'rld' => 'application/resource-lists-diff+xml', + 'rs' => 'application/rls-services+xml', + 'gbr' => 'application/rpki-ghostbusters', + 'mft' => 'application/rpki-manifest', + 'roa' => 'application/rpki-roa', + 'rsd' => 'application/rsd+xml', + 'rss' => 'application/rss+xml', + 'sbml' => 'application/sbml+xml', + 'scq' => 'application/scvp-cv-request', + 'scs' => 'application/scvp-cv-response', + 'spq' => 'application/scvp-vp-request', + 'spp' => 'application/scvp-vp-response', + 'sdp' => 'application/sdp', + 'setpay' => 'application/set-payment-initiation', + 'setreg' => 'application/set-registration-initiation', + 'shf' => 'application/shf+xml', + 'smi' => 'application/smil+xml', + 'rq' => 'application/sparql-query', + 'srx' => 'application/sparql-results+xml', + 'gram' => 'application/srgs', + 'grxml' => 'application/srgs+xml', + 'sru' => 'application/sru+xml', + 'ssdl' => 'application/ssdl+xml', + 'ssml' => 'application/ssml+xml', + 'tei' => 'application/tei+xml', + 'tfi' => 'application/thraud+xml', + 'tsd' => 'application/timestamped-data', + 'plb' => 'application/vnd.3gpp.pic-bw-large', + 'psb' => 'application/vnd.3gpp.pic-bw-small', + 'pvb' => 'application/vnd.3gpp.pic-bw-var', + 'tcap' => 'application/vnd.3gpp2.tcap', + 'pwn' => 'application/vnd.3m.post-it-notes', + 'aso' => 'application/vnd.accpac.simply.aso', + 'imp' => 'application/vnd.accpac.simply.imp', + 'acu' => 'application/vnd.acucobol', + 'atc' => 'application/vnd.acucorp', + 'air' => 'application/vnd.adobe.air-application-installer-package+zip', + 'fcdt' => 'application/vnd.adobe.formscentral.fcdt', + 'fxp' => 'application/vnd.adobe.fxp', + 'xdp' => 'application/vnd.adobe.xdp+xml', + 'xfdf' => 'application/vnd.adobe.xfdf', + 'ahead' => 'application/vnd.ahead.space', + 'azf' => 'application/vnd.airzip.filesecure.azf', + 'azs' => 'application/vnd.airzip.filesecure.azs', + 'azw' => 'application/vnd.amazon.ebook', + 'acc' => 'application/vnd.americandynamics.acc', + 'ami' => 'application/vnd.amiga.ami', + 'apk' => 'application/vnd.android.package-archive', + 'cii' => 'application/vnd.anser-web-certificate-issue-initiation', + 'fti' => 'application/vnd.anser-web-funds-transfer-initiation', + 'atx' => 'application/vnd.antix.game-component', + 'mpkg' => 'application/vnd.apple.installer+xml', + 'm3u8' => 'application/vnd.apple.mpegurl', + 'swi' => 'application/vnd.aristanetworks.swi', + 'iota' => 'application/vnd.astraea-software.iota', + 'aep' => 'application/vnd.audiograph', + 'mpm' => 'application/vnd.blueice.multipass', + 'bmi' => 'application/vnd.bmi', + 'rep' => 'application/vnd.businessobjects', + 'cdxml' => 'application/vnd.chemdraw+xml', + 'mmd' => 'application/vnd.chipnuts.karaoke-mmd', + 'cdy' => 'application/vnd.cinderella', + 'cla' => 'application/vnd.claymore', + 'rp9' => 'application/vnd.cloanto.rp9', + 'c4g' => 'application/vnd.clonk.c4group', + 'c11amc' => 'application/vnd.cluetrust.cartomobile-config', + 'c11amz' => 'application/vnd.cluetrust.cartomobile-config-pkg', + 'csp' => 'application/vnd.commonspace', + 'cdbcmsg' => 'application/vnd.contact.cmsg', + 'cmc' => 'application/vnd.cosmocaller', + 'clkx' => 'application/vnd.crick.clicker', + 'clkk' => 'application/vnd.crick.clicker.keyboard', + 'clkp' => 'application/vnd.crick.clicker.palette', + 'clkt' => 'application/vnd.crick.clicker.template', + 'clkw' => 'application/vnd.crick.clicker.wordbank', + 'wbs' => 'application/vnd.criticaltools.wbs+xml', + 'pml' => 'application/vnd.ctc-posml', + 'ppd' => 'application/vnd.cups-ppd', + 'car' => 'application/vnd.curl.car', + 'pcurl' => 'application/vnd.curl.pcurl', + 'dart' => 'application/vnd.dart', + 'rdz' => 'application/vnd.data-vision.rdz', + 'uvf' => 'application/vnd.dece.data', + 'uvt' => 'application/vnd.dece.ttml+xml', + 'uvx' => 'application/vnd.dece.unspecified', + 'uvz' => 'application/vnd.dece.zip', + 'fe_launch' => 'application/vnd.denovo.fcselayout-link', + 'dna' => 'application/vnd.dna', + 'mlp' => 'application/vnd.dolby.mlp', + 'dpg' => 'application/vnd.dpgraph', + 'dfac' => 'application/vnd.dreamfactory', + 'kpxx' => 'application/vnd.ds-keypoint', + 'ait' => 'application/vnd.dvb.ait', + 'svc' => 'application/vnd.dvb.service', + 'geo' => 'application/vnd.dynageo', + 'mag' => 'application/vnd.ecowin.chart', + 'nml' => 'application/vnd.enliven', + 'esf' => 'application/vnd.epson.esf', + 'msf' => 'application/vnd.epson.msf', + 'qam' => 'application/vnd.epson.quickanime', + 'slt' => 'application/vnd.epson.salt', + 'ssf' => 'application/vnd.epson.ssf', + 'es3' => 'application/vnd.eszigno3+xml', + 'ez2' => 'application/vnd.ezpix-album', + 'ez3' => 'application/vnd.ezpix-package', + 'fdf' => 'application/vnd.fdf', + 'mseed' => 'application/vnd.fdsn.mseed', + 'seed' => 'application/vnd.fdsn.seed', + 'gph' => 'application/vnd.flographit', + 'ftc' => 'application/vnd.fluxtime.clip', + 'fm' => 'application/vnd.framemaker', + 'fnc' => 'application/vnd.frogans.fnc', + 'ltf' => 'application/vnd.frogans.ltf', + 'fsc' => 'application/vnd.fsc.weblaunch', + 'oas' => 'application/vnd.fujitsu.oasys', + 'oa2' => 'application/vnd.fujitsu.oasys2', + 'oa3' => 'application/vnd.fujitsu.oasys3', + 'fg5' => 'application/vnd.fujitsu.oasysgp', + 'bh2' => 'application/vnd.fujitsu.oasysprs', + 'ddd' => 'application/vnd.fujixerox.ddd', + 'xdw' => 'application/vnd.fujixerox.docuworks', + 'xbd' => 'application/vnd.fujixerox.docuworks.binder', + 'fzs' => 'application/vnd.fuzzysheet', + 'txd' => 'application/vnd.genomatix.tuxedo', + 'ggb' => 'application/vnd.geogebra.file', + 'ggt' => 'application/vnd.geogebra.tool', + 'gex' => 'application/vnd.geometry-explorer', + 'gxt' => 'application/vnd.geonext', + 'g2w' => 'application/vnd.geoplan', + 'g3w' => 'application/vnd.geospace', + 'gmx' => 'application/vnd.gmx', + 'kml' => 'application/vnd.google-earth.kml+xml', + 'kmz' => 'application/vnd.google-earth.kmz', + 'gqf' => 'application/vnd.grafeq', + 'gac' => 'application/vnd.groove-account', + 'ghf' => 'application/vnd.groove-help', + 'gim' => 'application/vnd.groove-identity-message', + 'grv' => 'application/vnd.groove-injector', + 'gtm' => 'application/vnd.groove-tool-message', + 'tpl' => 'application/vnd.groove-tool-template', + 'vcg' => 'application/vnd.groove-vcard', + 'hal' => 'application/vnd.hal+xml', + 'zmm' => 'application/vnd.handheld-entertainment+xml', + 'hbci' => 'application/vnd.hbci', + 'les' => 'application/vnd.hhe.lesson-player', + 'hpgl' => 'application/vnd.hp-hpgl', + 'hpid' => 'application/vnd.hp-hpid', + 'hps' => 'application/vnd.hp-hps', + 'jlt' => 'application/vnd.hp-jlyt', + 'pcl' => 'application/vnd.hp-pcl', + 'pclxl' => 'application/vnd.hp-pclxl', + 'sfd-hdstx' => 'application/vnd.hydrostatix.sof-data', + 'mpy' => 'application/vnd.ibm.minipay', + 'afp' => 'application/vnd.ibm.modcap', + 'irm' => 'application/vnd.ibm.rights-management', + 'sc' => 'application/vnd.ibm.secure-container', + 'icc' => 'application/vnd.iccprofile', + 'igl' => 'application/vnd.igloader', + 'ivp' => 'application/vnd.immervision-ivp', + 'ivu' => 'application/vnd.immervision-ivu', + 'igm' => 'application/vnd.insors.igm', + 'xpw' => 'application/vnd.intercon.formnet', + 'i2g' => 'application/vnd.intergeo', + 'qbo' => 'application/vnd.intu.qbo', + 'qfx' => 'application/vnd.intu.qfx', + 'rcprofile' => 'application/vnd.ipunplugged.rcprofile', + 'irp' => 'application/vnd.irepository.package+xml', + 'xpr' => 'application/vnd.is-xpr', + 'fcs' => 'application/vnd.isac.fcs', + 'jam' => 'application/vnd.jam', + 'rms' => 'application/vnd.jcp.javame.midlet-rms', + 'jisp' => 'application/vnd.jisp', + 'joda' => 'application/vnd.joost.joda-archive', + 'ktz' => 'application/vnd.kahootz', + 'karbon' => 'application/vnd.kde.karbon', + 'chrt' => 'application/vnd.kde.kchart', + 'kfo' => 'application/vnd.kde.kformula', + 'flw' => 'application/vnd.kde.kivio', + 'kon' => 'application/vnd.kde.kontour', + 'kpr' => 'application/vnd.kde.kpresenter', + 'ksp' => 'application/vnd.kde.kspread', + 'kwd' => 'application/vnd.kde.kword', + 'htke' => 'application/vnd.kenameaapp', + 'kia' => 'application/vnd.kidspiration', + 'kne' => 'application/vnd.kinar', + 'skp' => 'application/vnd.koan', + 'sse' => 'application/vnd.kodak-descriptor', + 'lasxml' => 'application/vnd.las.las+xml', + 'lbd' => 'application/vnd.llamagraphics.life-balance.desktop', + 'lbe' => 'application/vnd.llamagraphics.life-balance.exchange+xml', + '123' => 'application/vnd.lotus-1-2-3', + 'apr' => 'application/vnd.lotus-approach', + 'pre' => 'application/vnd.lotus-freelance', + 'nsf' => 'application/vnd.lotus-notes', + 'org' => 'application/vnd.lotus-organizer', + 'scm' => 'application/vnd.lotus-screencam', + 'lwp' => 'application/vnd.lotus-wordpro', + 'portpkg' => 'application/vnd.macports.portpkg', + 'mcd' => 'application/vnd.mcd', + 'mc1' => 'application/vnd.medcalcdata', + 'cdkey' => 'application/vnd.mediastation.cdkey', + 'mwf' => 'application/vnd.mfer', + 'mfm' => 'application/vnd.mfmp', + 'flo' => 'application/vnd.micrografx.flo', + 'igx' => 'application/vnd.micrografx.igx', + 'mif' => 'application/vnd.mif', + 'daf' => 'application/vnd.mobius.daf', + 'dis' => 'application/vnd.mobius.dis', + 'mbk' => 'application/vnd.mobius.mbk', + 'mqy' => 'application/vnd.mobius.mqy', + 'msl' => 'application/vnd.mobius.msl', + 'plc' => 'application/vnd.mobius.plc', + 'txf' => 'application/vnd.mobius.txf', + 'mpn' => 'application/vnd.mophun.application', + 'mpc' => 'application/vnd.mophun.certificate', + 'xul' => 'application/vnd.mozilla.xul+xml', + 'cil' => 'application/vnd.ms-artgalry', + 'cab' => 'application/vnd.ms-cab-compressed', + 'xls' => 'application/vnd.ms-excel', + 'xlam' => 'application/vnd.ms-excel.addin.macroenabled.12', + 'xlsb' => 'application/vnd.ms-excel.sheet.binary.macroenabled.12', + 'xlsm' => 'application/vnd.ms-excel.sheet.macroenabled.12', + 'xltm' => 'application/vnd.ms-excel.template.macroenabled.12', + 'eot' => 'application/vnd.ms-fontobject', + 'chm' => 'application/vnd.ms-htmlhelp', + 'ims' => 'application/vnd.ms-ims', + 'lrm' => 'application/vnd.ms-lrm', + 'thmx' => 'application/vnd.ms-officetheme', + 'cat' => 'application/vnd.ms-pki.seccat', + 'stl' => 'application/vnd.ms-pki.stl', + 'ppt' => 'application/vnd.ms-powerpoint', + 'ppam' => 'application/vnd.ms-powerpoint.addin.macroenabled.12', + 'pptm' => 'application/vnd.ms-powerpoint.presentation.macroenabled.12', + 'sldm' => 'application/vnd.ms-powerpoint.slide.macroenabled.12', + 'ppsm' => 'application/vnd.ms-powerpoint.slideshow.macroenabled.12', + 'potm' => 'application/vnd.ms-powerpoint.template.macroenabled.12', + 'mpp' => 'application/vnd.ms-project', + 'docm' => 'application/vnd.ms-word.document.macroenabled.12', + 'dotm' => 'application/vnd.ms-word.template.macroenabled.12', + 'wps' => 'application/vnd.ms-works', + 'wpl' => 'application/vnd.ms-wpl', + 'xps' => 'application/vnd.ms-xpsdocument', + 'mseq' => 'application/vnd.mseq', + 'mus' => 'application/vnd.musician', + 'msty' => 'application/vnd.muvee.style', + 'taglet' => 'application/vnd.mynfc', + 'nlu' => 'application/vnd.neurolanguage.nlu', + 'ntf' => 'application/vnd.nitf', + 'nnd' => 'application/vnd.noblenet-directory', + 'nns' => 'application/vnd.noblenet-sealer', + 'nnw' => 'application/vnd.noblenet-web', + 'ngdat' => 'application/vnd.nokia.n-gage.data', + 'n-gage' => 'application/vnd.nokia.n-gage.symbian.install', + 'rpst' => 'application/vnd.nokia.radio-preset', + 'rpss' => 'application/vnd.nokia.radio-presets', + 'edm' => 'application/vnd.novadigm.edm', + 'edx' => 'application/vnd.novadigm.edx', + 'ext' => 'application/vnd.novadigm.ext', + 'odc' => 'application/vnd.oasis.opendocument.chart', + 'otc' => 'application/vnd.oasis.opendocument.chart-template', + 'odb' => 'application/vnd.oasis.opendocument.database', + 'odf' => 'application/vnd.oasis.opendocument.formula', + 'odft' => 'application/vnd.oasis.opendocument.formula-template', + 'odg' => 'application/vnd.oasis.opendocument.graphics', + 'otg' => 'application/vnd.oasis.opendocument.graphics-template', + 'odi' => 'application/vnd.oasis.opendocument.image', + 'oti' => 'application/vnd.oasis.opendocument.image-template', + 'odp' => 'application/vnd.oasis.opendocument.presentation', + 'otp' => 'application/vnd.oasis.opendocument.presentation-template', + 'ods' => 'application/vnd.oasis.opendocument.spreadsheet', + 'ots' => 'application/vnd.oasis.opendocument.spreadsheet-template', + 'odt' => 'application/vnd.oasis.opendocument.text', + 'odm' => 'application/vnd.oasis.opendocument.text-master', + 'ott' => 'application/vnd.oasis.opendocument.text-template', + 'oth' => 'application/vnd.oasis.opendocument.text-web', + 'xo' => 'application/vnd.olpc-sugar', + 'dd2' => 'application/vnd.oma.dd2+xml', + 'oxt' => 'application/vnd.openofficeorg.extension', + 'pptx' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation', + 'sldx' => 'application/vnd.openxmlformats-officedocument.presentationml.slide', + 'ppsx' => 'application/vnd.openxmlformats-officedocument.presentationml.slideshow', + 'potx' => 'application/vnd.openxmlformats-officedocument.presentationml.template', + 'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + 'xltx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.template', + 'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + 'dotx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.template', + 'mgp' => 'application/vnd.osgeo.mapguide.package', + 'dp' => 'application/vnd.osgi.dp', + 'esa' => 'application/vnd.osgi.subsystem', + 'pdb' => 'application/vnd.palm', + 'paw' => 'application/vnd.pawaafile', + 'str' => 'application/vnd.pg.format', + 'ei6' => 'application/vnd.pg.osasli', + 'efif' => 'application/vnd.picsel', + 'wg' => 'application/vnd.pmi.widget', + 'plf' => 'application/vnd.pocketlearn', + 'pbd' => 'application/vnd.powerbuilder6', + 'box' => 'application/vnd.previewsystems.box', + 'mgz' => 'application/vnd.proteus.magazine', + 'qps' => 'application/vnd.publishare-delta-tree', + 'ptid' => 'application/vnd.pvi.ptid1', + 'qxd' => 'application/vnd.quark.quarkxpress', + 'bed' => 'application/vnd.realvnc.bed', + 'mxl' => 'application/vnd.recordare.musicxml', + 'musicxml' => 'application/vnd.recordare.musicxml+xml', + 'cryptonote' => 'application/vnd.rig.cryptonote', + 'cod' => 'application/vnd.rim.cod', + 'rm' => 'application/vnd.rn-realmedia', + 'rmvb' => 'application/vnd.rn-realmedia-vbr', + 'link66' => 'application/vnd.route66.link66+xml', + 'st' => 'application/vnd.sailingtracker.track', + 'see' => 'application/vnd.seemail', + 'sema' => 'application/vnd.sema', + 'semd' => 'application/vnd.semd', + 'semf' => 'application/vnd.semf', + 'ifm' => 'application/vnd.shana.informed.formdata', + 'itp' => 'application/vnd.shana.informed.formtemplate', + 'iif' => 'application/vnd.shana.informed.interchange', + 'ipk' => 'application/vnd.shana.informed.package', + 'twd' => 'application/vnd.simtech-mindmapper', + 'mmf' => 'application/vnd.smaf', + 'teacher' => 'application/vnd.smart.teacher', + 'sdkm' => 'application/vnd.solent.sdkm+xml', + 'dxp' => 'application/vnd.spotfire.dxp', + 'sfs' => 'application/vnd.spotfire.sfs', + 'sdc' => 'application/vnd.stardivision.calc', + 'sda' => 'application/vnd.stardivision.draw', + 'sdd' => 'application/vnd.stardivision.impress', + 'smf' => 'application/vnd.stardivision.math', + 'sdw' => 'application/vnd.stardivision.writer', + 'sgl' => 'application/vnd.stardivision.writer-global', + 'smzip' => 'application/vnd.stepmania.package', + 'sm' => 'application/vnd.stepmania.stepchart', + 'sxc' => 'application/vnd.sun.xml.calc', + 'stc' => 'application/vnd.sun.xml.calc.template', + 'sxd' => 'application/vnd.sun.xml.draw', + 'std' => 'application/vnd.sun.xml.draw.template', + 'sxi' => 'application/vnd.sun.xml.impress', + 'sti' => 'application/vnd.sun.xml.impress.template', + 'sxm' => 'application/vnd.sun.xml.math', + 'sxw' => 'application/vnd.sun.xml.writer', + 'sxg' => 'application/vnd.sun.xml.writer.global', + 'stw' => 'application/vnd.sun.xml.writer.template', + 'sus' => 'application/vnd.sus-calendar', + 'svd' => 'application/vnd.svd', + 'sis' => 'application/vnd.symbian.install', + 'xsm' => 'application/vnd.syncml+xml', + 'bdm' => 'application/vnd.syncml.dm+wbxml', + 'xdm' => 'application/vnd.syncml.dm+xml', + 'tao' => 'application/vnd.tao.intent-module-archive', + 'pcap' => 'application/vnd.tcpdump.pcap', + 'tmo' => 'application/vnd.tmobile-livetv', + 'tpt' => 'application/vnd.trid.tpt', + 'mxs' => 'application/vnd.triscape.mxs', + 'tra' => 'application/vnd.trueapp', + 'ufd' => 'application/vnd.ufdl', + 'utz' => 'application/vnd.uiq.theme', + 'umj' => 'application/vnd.umajin', + 'unityweb' => 'application/vnd.unity', + 'uoml' => 'application/vnd.uoml+xml', + 'vcx' => 'application/vnd.vcx', + 'vsd' => 'application/vnd.visio', + 'vis' => 'application/vnd.visionary', + 'vsf' => 'application/vnd.vsf', + 'wbxml' => 'application/vnd.wap.wbxml', + 'wmlc' => 'application/vnd.wap.wmlc', + 'wmlsc' => 'application/vnd.wap.wmlscriptc', + 'wtb' => 'application/vnd.webturbo', + 'nbp' => 'application/vnd.wolfram.player', + 'wpd' => 'application/vnd.wordperfect', + 'wqd' => 'application/vnd.wqd', + 'stf' => 'application/vnd.wt.stf', + 'xar' => 'application/vnd.xara', + 'xfdl' => 'application/vnd.xfdl', + 'hvd' => 'application/vnd.yamaha.hv-dic', + 'hvs' => 'application/vnd.yamaha.hv-script', + 'hvp' => 'application/vnd.yamaha.hv-voice', + 'osf' => 'application/vnd.yamaha.openscoreformat', + 'osfpvg' => 'application/vnd.yamaha.openscoreformat.osfpvg+xml', + 'saf' => 'application/vnd.yamaha.smaf-audio', + 'spf' => 'application/vnd.yamaha.smaf-phrase', + 'cmp' => 'application/vnd.yellowriver-custom-menu', + 'zir' => 'application/vnd.zul', + 'zaz' => 'application/vnd.zzazz.deck+xml', + 'vxml' => 'application/voicexml+xml', + 'wgt' => 'application/widget', + 'hlp' => 'application/winhlp', + 'wsdl' => 'application/wsdl+xml', + 'wspolicy' => 'application/wspolicy+xml', + '7z' => 'application/x-7z-compressed', + 'abw' => 'application/x-abiword', + 'ace' => 'application/x-ace-compressed', + 'dmg' => 'application/x-apple-diskimage', + 'aab' => 'application/x-authorware-bin', + 'aam' => 'application/x-authorware-map', + 'aas' => 'application/x-authorware-seg', + 'bcpio' => 'application/x-bcpio', + 'torrent' => 'application/x-bittorrent', + 'blb' => 'application/x-blorb', + 'bz' => 'application/x-bzip', + 'bz2' => 'application/x-bzip2', + 'cbr' => 'application/x-cbr', + 'vcd' => 'application/x-cdlink', + 'cfs' => 'application/x-cfs-compressed', + 'chat' => 'application/x-chat', + 'pgn' => 'application/x-chess-pgn', + 'nsc' => 'application/x-conference', + 'cpio' => 'application/x-cpio', + 'csh' => 'application/x-csh', + 'deb' => 'application/x-debian-package', + 'dgc' => 'application/x-dgc-compressed', + 'dir' => 'application/x-director', + 'wad' => 'application/x-doom', + 'ncx' => 'application/x-dtbncx+xml', + 'dtb' => 'application/x-dtbook+xml', + 'res' => 'application/x-dtbresource+xml', + 'dvi' => 'application/x-dvi', + 'evy' => 'application/x-envoy', + 'eva' => 'application/x-eva', + 'bdf' => 'application/x-font-bdf', + 'gsf' => 'application/x-font-ghostscript', + 'psf' => 'application/x-font-linux-psf', + 'otf' => 'application/x-font-otf', + 'pcf' => 'application/x-font-pcf', + 'snf' => 'application/x-font-snf', + 'ttf' => 'application/x-font-ttf', + 'pfa' => 'application/x-font-type1', + 'woff' => 'application/x-font-woff', + 'arc' => 'application/x-freearc', + 'spl' => 'application/x-futuresplash', + 'gca' => 'application/x-gca-compressed', + 'ulx' => 'application/x-glulx', + 'gnumeric' => 'application/x-gnumeric', + 'gramps' => 'application/x-gramps-xml', + 'gtar' => 'application/x-gtar', + 'hdf' => 'application/x-hdf', + 'install' => 'application/x-install-instructions', + 'iso' => 'application/x-iso9660-image', + 'jnlp' => 'application/x-java-jnlp-file', + 'latex' => 'application/x-latex', + 'lzh' => 'application/x-lzh-compressed', + 'mie' => 'application/x-mie', + 'prc' => 'application/x-mobipocket-ebook', + 'application' => 'application/x-ms-application', + 'lnk' => 'application/x-ms-shortcut', + 'wmd' => 'application/x-ms-wmd', + 'wmz' => 'application/x-ms-wmz', + 'xbap' => 'application/x-ms-xbap', + 'mdb' => 'application/x-msaccess', + 'obd' => 'application/x-msbinder', + 'crd' => 'application/x-mscardfile', + 'clp' => 'application/x-msclip', + 'exe' => 'application/x-msdownload', + 'mvb' => 'application/x-msmediaview', + 'wmf' => 'application/x-msmetafile', + 'mny' => 'application/x-msmoney', + 'pub' => 'application/x-mspublisher', + 'scd' => 'application/x-msschedule', + 'trm' => 'application/x-msterminal', + 'wri' => 'application/x-mswrite', + 'nc' => 'application/x-netcdf', + 'nzb' => 'application/x-nzb', + 'p12' => 'application/x-pkcs12', + 'p7b' => 'application/x-pkcs7-certificates', + 'p7r' => 'application/x-pkcs7-certreqresp', + 'rar' => 'application/x-rar', + 'ris' => 'application/x-research-info-systems', + 'sh' => 'application/x-sh', + 'shar' => 'application/x-shar', + 'swf' => 'application/x-shockwave-flash', + 'xap' => 'application/x-silverlight-app', + 'sql' => 'application/x-sql', + 'sit' => 'application/x-stuffit', + 'sitx' => 'application/x-stuffitx', + 'srt' => 'application/x-subrip', + 'sv4cpio' => 'application/x-sv4cpio', + 'sv4crc' => 'application/x-sv4crc', + 't3' => 'application/x-t3vm-image', + 'gam' => 'application/x-tads', + 'tar' => 'application/x-tar', + 'tcl' => 'application/x-tcl', + 'tex' => 'application/x-tex', + 'tfm' => 'application/x-tex-tfm', + 'texinfo' => 'application/x-texinfo', + 'obj' => 'application/x-tgif', + 'ustar' => 'application/x-ustar', + 'src' => 'application/x-wais-source', + 'der' => 'application/x-x509-ca-cert', + 'fig' => 'application/x-xfig', + 'xlf' => 'application/x-xliff+xml', + 'xpi' => 'application/x-xpinstall', + 'xz' => 'application/x-xz', + 'z1' => 'application/x-zmachine', + 'xaml' => 'application/xaml+xml', + 'xdf' => 'application/xcap-diff+xml', + 'xenc' => 'application/xenc+xml', + 'xhtml' => 'application/xhtml+xml', + 'xml' => 'application/xml', + 'dtd' => 'application/xml-dtd', + 'xop' => 'application/xop+xml', + 'xpl' => 'application/xproc+xml', + 'xslt' => 'application/xslt+xml', + 'xspf' => 'application/xspf+xml', + 'mxml' => 'application/xv+xml', + 'yang' => 'application/yang', + 'yin' => 'application/yin+xml', + 'zip' => 'application/zip', + 'adp' => 'audio/adpcm', + 'au' => 'audio/basic', + 'mid' => 'audio/midi', + 'mp3' => 'audio/mpeg', + 'mp4a' => 'audio/mp4', + 'mpga' => 'audio/mpeg', + 'oga' => 'audio/ogg', + 's3m' => 'audio/s3m', + 'sil' => 'audio/silk', + 'uva' => 'audio/vnd.dece.audio', + 'eol' => 'audio/vnd.digital-winds', + 'dra' => 'audio/vnd.dra', + 'dts' => 'audio/vnd.dts', + 'dtshd' => 'audio/vnd.dts.hd', + 'lvp' => 'audio/vnd.lucent.voice', + 'pya' => 'audio/vnd.ms-playready.media.pya', + 'ecelp4800' => 'audio/vnd.nuera.ecelp4800', + 'ecelp7470' => 'audio/vnd.nuera.ecelp7470', + 'ecelp9600' => 'audio/vnd.nuera.ecelp9600', + 'rip' => 'audio/vnd.rip', + 'weba' => 'audio/webm', + 'aac' => 'audio/x-aac', + 'aif' => 'audio/x-aiff', + 'caf' => 'audio/x-caf', + 'flac' => 'audio/x-flac', + 'mka' => 'audio/x-matroska', + 'm3u' => 'audio/x-mpegurl', + 'wax' => 'audio/x-ms-wax', + 'wma' => 'audio/x-ms-wma', + 'ram' => 'audio/x-pn-realaudio', + 'rmp' => 'audio/x-pn-realaudio-plugin', + 'wav' => 'audio/x-wav', + 'xm' => 'audio/xm', + 'cdx' => 'chemical/x-cdx', + 'cif' => 'chemical/x-cif', + 'cmdf' => 'chemical/x-cmdf', + 'cml' => 'chemical/x-cml', + 'csml' => 'chemical/x-csml', + 'xyz' => 'chemical/x-xyz', + 'bmp' => 'image/bmp', + 'cgm' => 'image/cgm', + 'g3' => 'image/g3fax', + 'gif' => 'image/gif', + 'ief' => 'image/ief', + 'jpg' => 'image/jpeg', + 'jpeg' => 'image/jpeg', + 'ktx' => 'image/ktx', + 'png' => 'image/png', + 'btif' => 'image/prs.btif', + 'sgi' => 'image/sgi', + 'svg' => 'image/svg+xml', + 'tiff' => 'image/tiff', + 'psd' => 'image/vnd.adobe.photoshop', + 'uvi' => 'image/vnd.dece.graphic', + 'djvu' => 'image/vnd.djvu', + 'dwg' => 'image/vnd.dwg', + 'dxf' => 'image/vnd.dxf', + 'fbs' => 'image/vnd.fastbidsheet', + 'fpx' => 'image/vnd.fpx', + 'fst' => 'image/vnd.fst', + 'mmr' => 'image/vnd.fujixerox.edmics-mmr', + 'rlc' => 'image/vnd.fujixerox.edmics-rlc', + 'mdi' => 'image/vnd.ms-modi', + 'wdp' => 'image/vnd.ms-photo', + 'npx' => 'image/vnd.net-fpx', + 'wbmp' => 'image/vnd.wap.wbmp', + 'xif' => 'image/vnd.xiff', + 'webp' => 'image/webp', + '3ds' => 'image/x-3ds', + 'ras' => 'image/x-cmu-raster', + 'cmx' => 'image/x-cmx', + 'fh' => 'image/x-freehand', + 'ico' => 'image/x-icon', + 'sid' => 'image/x-mrsid-image', + 'pcx' => 'image/x-pcx', + 'pic' => 'image/x-pict', + 'pnm' => 'image/x-portable-anymap', + 'pbm' => 'image/x-portable-bitmap', + 'pgm' => 'image/x-portable-graymap', + 'ppm' => 'image/x-portable-pixmap', + 'rgb' => 'image/x-rgb', + 'tga' => 'image/x-tga', + 'xbm' => 'image/x-xbitmap', + 'xpm' => 'image/x-xpixmap', + 'xwd' => 'image/x-xwindowdump', + 'eml' => 'message/rfc822', + 'igs' => 'model/iges', + 'msh' => 'model/mesh', + 'dae' => 'model/vnd.collada+xml', + 'dwf' => 'model/vnd.dwf', + 'gdl' => 'model/vnd.gdl', + 'gtw' => 'model/vnd.gtw', + 'mts' => 'model/vnd.mts', + 'vtu' => 'model/vnd.vtu', + 'wrl' => 'model/vrml', + 'x3db' => 'model/x3d+binary', + 'x3dv' => 'model/x3d+vrml', + 'x3d' => 'model/x3d+xml', + 'appcache' => 'text/cache-manifest', + 'ics' => 'text/calendar', + 'css' => 'text/css', + 'csv' => 'text/csv', + 'html' => 'text/html', + 'n3' => 'text/n3', + 'txt' => 'text/plain', + 'dsc' => 'text/prs.lines.tag', + 'rtx' => 'text/richtext', + 'rtf' => 'text/rtf', + 'sgml' => 'text/sgml', + 'tsv' => 'text/tab-separated-values', + 't' => 'text/troff', + 'ttl' => 'text/turtle', + 'uri' => 'text/uri-list', + 'vcard' => 'text/vcard', + 'curl' => 'text/vnd.curl', + 'dcurl' => 'text/vnd.curl.dcurl', + 'scurl' => 'text/vnd.curl.scurl', + 'mcurl' => 'text/vnd.curl.mcurl', + 'sub' => 'text/vnd.dvb.subtitle', + 'fly' => 'text/vnd.fly', + 'flx' => 'text/vnd.fmi.flexstor', + 'gv' => 'text/vnd.graphviz', + '3dml' => 'text/vnd.in3d.3dml', + 'spot' => 'text/vnd.in3d.spot', + 'jad' => 'text/vnd.sun.j2me.app-descriptor', + 'wml' => 'text/vnd.wap.wml', + 'wmls' => 'text/vnd.wap.wmlscript', + 's' => 'text/x-asm', + 'c' => 'text/x-c', + 'f' => 'text/x-fortran', + 'p' => 'text/x-pascal', + 'java' => 'text/x-java-source', + 'opml' => 'text/x-opml', + 'nfo' => 'text/x-nfo', + 'etx' => 'text/x-setext', + 'sfv' => 'text/x-sfv', + 'uu' => 'text/x-uuencode', + 'vcs' => 'text/x-vcalendar', + 'vcf' => 'text/x-vcard', + '3gp' => 'video/3gpp', + '3g2' => 'video/3gpp2', + 'h261' => 'video/h261', + 'h263' => 'video/h263', + 'h264' => 'video/h264', + 'jpgv' => 'video/jpeg', + 'jpm' => 'video/jpm', + 'mj2' => 'video/mj2', + 'mp4' => 'video/mp4', + 'mpeg' => 'video/mpeg', + 'ogv' => 'video/ogg', + 'mov' => 'video/quicktime', + 'qt' => 'video/quicktime', + 'uvh' => 'video/vnd.dece.hd', + 'uvm' => 'video/vnd.dece.mobile', + 'uvp' => 'video/vnd.dece.pd', + 'uvs' => 'video/vnd.dece.sd', + 'uvv' => 'video/vnd.dece.video', + 'dvb' => 'video/vnd.dvb.file', + 'fvt' => 'video/vnd.fvt', + 'mxu' => 'video/vnd.mpegurl', + 'pyv' => 'video/vnd.ms-playready.media.pyv', + 'uvu' => 'video/vnd.uvvu.mp4', + 'viv' => 'video/vnd.vivo', + 'webm' => 'video/webm', + 'f4v' => 'video/x-f4v', + 'fli' => 'video/x-fli', + 'flv' => 'video/x-flv', + 'm4v' => 'video/x-m4v', + 'mkv' => 'video/x-matroska', + 'mng' => 'video/x-mng', + 'asf' => 'video/x-ms-asf', + 'vob' => 'video/x-ms-vob', + 'wm' => 'video/x-ms-wm', + 'wmv' => 'video/x-ms-wmv', + 'wmx' => 'video/x-ms-wmx', + 'wvx' => 'video/x-ms-wvx', + 'avi' => 'video/x-msvideo', + 'movie' => 'video/x-sgi-movie', + 'smv' => 'video/x-smv', + 'ice' => 'x-conference/x-cooltalk', + ]; + + /** + * Get the MIME type for a file based on the file's extension. + * + * @param string $filename + * @return string + */ + public static function from($filename) + { + $extension = strtok(pathinfo($filename, PATHINFO_EXTENSION), '?'); + + return self::getMimeTypeFromExtension($extension); + } + + /** + * Get the MIME type for a given extension or return all mimes. + * + * @param string $extension + * @return string|array + */ + public static function get($extension = null) + { + return isset($extension) ? self::getMimeTypeFromExtension($extension) : self::$mimes; + } + + /** + * Get the MIME type for a given extension. + * + * @param string $extension + * @return string + */ + protected static function getMimeTypeFromExtension($extension) + { + return self::$mimes[$extension] ?? 'application/octet-stream'; + } +} diff --git a/src/Helpers/OS.php b/src/Helpers/OS.php new file mode 100644 index 00000000..7a22a6fb --- /dev/null +++ b/src/Helpers/OS.php @@ -0,0 +1,61 @@ +when = $when; + $this->path = $path; + $this->types = $types; + } + + /** + * @return \Carbon\Carbon + */ + public function getWhen(): Carbon + { + return $this->when; + } + + /** + * @return string + */ + public function getPath(): string + { + return $this->path; + } + + /** + * @return array + */ + public function getTypes(): array + { + return $this->types; + } + + /** + * @return string + */ + public function getType(): string + { + return Arr::first($this->types); + } + + /** + * Checks if event types has needed type(s). + * + * @param string ...$types + * + * @return bool + */ + public function isType(string ...$types): bool + { + return count(array_intersect($this->types, $types)) > 0; + } + + /** + * Get possible event types. + * + * @return array + */ + public static function getPossibleTypes(): array + { + return self::$possibleTypes; + } +} \ No newline at end of file diff --git a/src/HotReload/FSEventParser.php b/src/HotReload/FSEventParser.php new file mode 100644 index 00000000..b37fbc8c --- /dev/null +++ b/src/HotReload/FSEventParser.php @@ -0,0 +1,37 @@ +getPath()) ? 'Directory' : 'File'; + $events = implode(', ', $event->getTypes()); + $time = $event->getWhen()->format('Y.m.d H:i:s'); + + return sprintf('%s: %s %s at %s', $item, $event->getPath(), $events, $time); + } +} \ No newline at end of file diff --git a/src/HotReload/FSProcess.php b/src/HotReload/FSProcess.php new file mode 100644 index 00000000..4747d07b --- /dev/null +++ b/src/HotReload/FSProcess.php @@ -0,0 +1,96 @@ +filter = $filter; + $this->recursively = $recursively; + $this->directory = $directory; + $this->locked = false; + } + + /** + * Make swoole process. + * + * @param callable|null $callback + * + * @return \Swoole\Process + */ + public function make(?callable $callback = null) + { + $mcb = function ($type, $buffer) use ($callback) { + if (! $this->locked && AppProcess::OUT === $type && $event = FSEventParser::toEvent($buffer)) { + $this->locked = true; + ($callback) ? $callback($event) : null; + $this->locked = false; + unset($event); + } + }; + + return new SwooleProcess(function () use ($mcb) { + (new AppProcess($this->configure()))->setTimeout(0)->run($mcb); + }, false, false); + } + + /** + * Configure process. + * + * @return array + */ + protected function configure(): array + { + return [ + 'fswatch', + $this->recursively ? '-rtx' : '-tx', + '-e', + '.*', + '-i', + "\\{$this->filter}$", + $this->directory, + ]; + } +} diff --git a/src/HttpServiceProvider.php b/src/HttpServiceProvider.php index 6f6c0b2a..fdc7460a 100644 --- a/src/HttpServiceProvider.php +++ b/src/HttpServiceProvider.php @@ -2,14 +2,18 @@ namespace SwooleTW\Http; +use SwooleTW\Http\Helpers\FW; +use Illuminate\Queue\QueueManager; +use SwooleTW\Http\Server\PidManager; use Swoole\Http\Server as HttpServer; use Illuminate\Support\ServiceProvider; +use Illuminate\Database\DatabaseManager; use SwooleTW\Http\Server\Facades\Server; use SwooleTW\Http\Coroutine\MySqlConnection; use SwooleTW\Http\Commands\HttpServerCommand; use Swoole\Websocket\Server as WebsocketServer; -use SwooleTW\Http\Coroutine\Connectors\MySqlConnector; use SwooleTW\Http\Task\Connectors\SwooleTaskConnector; +use SwooleTW\Http\Coroutine\Connectors\ConnectorFactory; /** * @codeCoverageIgnore @@ -34,17 +38,39 @@ abstract class HttpServiceProvider extends ServiceProvider protected static $server; /** - * Register the service provider. + * Boot the service provider. * * @return void */ - public function register() + public function boot() { + $this->publishFiles(); + $this->loadConfigs(); $this->mergeConfigs(); $this->setIsWebsocket(); + + $config = $this->app->make('config'); + + if ($config->get('swoole_http.websocket.enabled')) { + $this->bootWebsocketRoutes(); + } + + if ($config->get('swoole_http.server.access_log')) { + $this->pushAccessLogMiddleware(); + } + } + + /** + * Register the service provider. + * + * @return void + */ + public function register() + { $this->registerServer(); $this->registerManager(); $this->registerCommands(); + $this->registerPidManager(); $this->registerDatabaseDriver(); $this->registerSwooleQueueDriver(); } @@ -57,28 +83,37 @@ public function register() abstract protected function registerManager(); /** - * Boot routes. + * Boot websocket routes. * * @return void */ - abstract protected function bootRoutes(); + abstract protected function bootWebsocketRoutes(); /** - * Boot the service provider. + * Register access log middleware to container. * * @return void */ - public function boot() + abstract protected function pushAccessLogMiddleware(); + + /** + * Publish files of this package. + */ + protected function publishFiles() { $this->publishes([ __DIR__ . '/../config/swoole_http.php' => base_path('config/swoole_http.php'), __DIR__ . '/../config/swoole_websocket.php' => base_path('config/swoole_websocket.php'), - __DIR__ . '/../routes/websocket.php' => base_path('routes/websocket.php') + __DIR__ . '/../routes/websocket.php' => base_path('routes/websocket.php'), ], 'laravel-swoole'); + } - if ($this->app['config']->get('swoole_http.websocket.enabled')) { - $this->bootRoutes(); - } + /** + * Load configurations. + */ + protected function loadConfigs() + { + // do nothing } /** @@ -90,12 +125,27 @@ protected function mergeConfigs() $this->mergeConfigFrom(__DIR__ . '/../config/swoole_websocket.php', 'swoole_websocket'); } + /** + * Register pid manager. + * + * @return void + */ + protected function registerPidManager(): void + { + $this->app->singleton(PidManager::class, function() { + return new PidManager( + $this->app->make('config')->get('swoole_http.server.options.pid_file') + ); + }); + } + /** * Set isWebsocket. */ protected function setIsWebsocket() { - $this->isWebsocket = $this->app['config']->get('swoole_http.websocket.enabled'); + $this->isWebsocket = $this->app->make('config') + ->get('swoole_http.websocket.enabled'); } /** @@ -114,11 +164,13 @@ protected function registerCommands() protected function createSwooleServer() { $server = $this->isWebsocket ? WebsocketServer::class : HttpServer::class; - $host = $this->app['config']->get('swoole_http.server.host'); - $port = $this->app['config']->get('swoole_http.server.port'); - $socketType = $this->app['config']->get('swoole_http.server.socket_type', SWOOLE_SOCK_TCP); + $config = $this->app->make('config'); + $host = $config->get('swoole_http.server.host'); + $port = $config->get('swoole_http.server.port'); + $socketType = $config->get('swoole_http.server.socket_type', SWOOLE_SOCK_TCP); + $processType = $config->get('swoole_http.server.process_type', SWOOLE_PROCESS); - static::$server = new $server($host, $port, SWOOLE_PROCESS, $socketType); + static::$server = new $server($host, $port, $processType, $socketType); } /** @@ -126,12 +178,12 @@ protected function createSwooleServer() */ protected function configureSwooleServer() { - $config = $this->app['config']; + $config = $this->app->make('config'); $options = $config->get('swoole_http.server.options'); // only enable task worker in websocket mode and for queue driver if ($config->get('queue.default') !== 'swoole' && ! $this->isWebsocket) { - unset($config['task_worker_num']); + unset($options['task_worker_num']); } static::$server->set($options); @@ -149,6 +201,7 @@ protected function registerServer() $this->createSwooleServer(); $this->configureSwooleServer(); } + return static::$server; }); $this->app->alias(Server::class, 'swoole.server'); @@ -159,29 +212,51 @@ protected function registerServer() */ protected function registerDatabaseDriver() { - $this->app->resolving('db', function ($db) { + $this->app->extend(DatabaseManager::class, function (DatabaseManager $db) { $db->extend('mysql-coroutine', function ($config, $name) { $config['name'] = $name; - $connection = function () use ($config) { - return (new MySqlConnector())->connect($config); - }; - return new MySqlConnection( - $connection, + $connection = new MySqlConnection( + $this->getNewMySqlConnection($config, 'write'), $config['database'], $config['prefix'], $config ); + + if (isset($config['read'])) { + $connection->setReadPdo($this->getNewMySqlConnection($config, 'read')); + } + + return $connection; }); + + return $db; }); } + /** + * Get a new mysql connection. + * + * @param array $config + * @param string $connection + * + * @return \PDO + */ + protected function getNewMySqlConnection(array $config, string $connection = null) + { + if ($connection && isset($config[$connection])) { + $config = array_merge($config, $config[$connection]); + } + + return ConnectorFactory::make(FW::version())->connect($config); + } + /** * Register queue driver for swoole async task. */ protected function registerSwooleQueueDriver() { - $this->app->afterResolving('queue', function ($manager) { + $this->app->afterResolving('queue', function (QueueManager $manager) { $manager->addConnector('swoole', function () { return new SwooleTaskConnector($this->app->make(Server::class)); }); diff --git a/src/LaravelServiceProvider.php b/src/LaravelServiceProvider.php index 5cf86a30..3f58e2ca 100644 --- a/src/LaravelServiceProvider.php +++ b/src/LaravelServiceProvider.php @@ -3,6 +3,8 @@ namespace SwooleTW\Http; use SwooleTW\Http\Server\Manager; +use Illuminate\Contracts\Http\Kernel; +use SwooleTW\Http\Middleware\AccessLog; /** * @codeCoverageIgnore @@ -16,18 +18,30 @@ class LaravelServiceProvider extends HttpServiceProvider */ protected function registerManager() { - $this->app->singleton('swoole.manager', function ($app) { + $this->app->singleton(Manager::class, function ($app) { return new Manager($app, 'laravel'); }); + + $this->app->alias(Manager::class, 'swoole.manager'); + } + + /** + * Boot websocket routes. + * + * @return void + */ + protected function bootWebsocketRoutes() + { + require __DIR__ . '/../routes/laravel_routes.php'; } /** - * Boot routes. + * Register access log middleware to container. * * @return void */ - protected function bootRoutes() + protected function pushAccessLogMiddleware() { - require __DIR__.'/../routes/laravel_routes.php'; + $this->app->make(Kernel::class)->pushMiddleware(AccessLog::class); } } diff --git a/src/LumenServiceProvider.php b/src/LumenServiceProvider.php index f7ae8414..3270ded9 100644 --- a/src/LumenServiceProvider.php +++ b/src/LumenServiceProvider.php @@ -3,12 +3,22 @@ namespace SwooleTW\Http; use SwooleTW\Http\Server\Manager; +use SwooleTW\Http\Middleware\AccessLog; /** * @codeCoverageIgnore */ class LumenServiceProvider extends HttpServiceProvider { + /** + * Load configurations. + */ + protected function loadConfigs() + { + $this->app->configure('swoole_http'); + $this->app->configure('swoole_websocket'); + } + /** * Register manager. * @@ -16,28 +26,33 @@ class LumenServiceProvider extends HttpServiceProvider */ protected function registerManager() { - $this->app->singleton('swoole.manager', function ($app) { + $this->app->singleton(Manager::class, function ($app) { return new Manager($app, 'lumen'); }); + + $this->app->alias(Manager::class, 'swoole.manager'); } /** - * Boot routes. + * Boot websocket routes. * * @return void */ - protected function bootRoutes() + protected function bootWebsocketRoutes() { - $app = $this->app; - - if (property_exists($app, 'router')) { - $app->router->group(['namespace' => 'SwooleTW\Http\Controllers'], function ($app) { - require __DIR__ . '/../routes/lumen_routes.php'; - }); - } else { - $app->group(['namespace' => 'App\Http\Controllers'], function ($app) { + $this->app->router + ->group(['namespace' => 'SwooleTW\Http\Controllers'], function ($router) { require __DIR__ . '/../routes/lumen_routes.php'; }); - } + } + + /** + * Register access log middleware to container. + * + * @return void + */ + protected function pushAccessLogMiddleware() + { + $this->app->middleware(AccessLog::class); } } diff --git a/src/Middleware/AccessLog.php b/src/Middleware/AccessLog.php new file mode 100644 index 00000000..2041b8cf --- /dev/null +++ b/src/Middleware/AccessLog.php @@ -0,0 +1,55 @@ +output = $output; + } + + /** + * Handle the incoming request. + * + * @param \Illuminate\Http\Request $request + * @param \Closure $next + * + * @return mixed + */ + public function handle($request, Closure $next) + { + return $next($request); + } + + /** + * Handle the outgoing request and response. + * + * @param \Illuminate\Http\Request $request + * @param \Symfony\Component\HttpFoundation\Response $response + */ + public function terminate(Request $request, Response $response) + { + $this->output->log($request, $response); + } +} \ No newline at end of file diff --git a/src/Server/AccessOutput.php b/src/Server/AccessOutput.php new file mode 100644 index 00000000..08de5046 --- /dev/null +++ b/src/Server/AccessOutput.php @@ -0,0 +1,71 @@ +output = $output; + } + + /** + * Access log. + * + * @param \Illuminate\Http\Request $request + * @param \Symfony\Component\HttpFoundation\Response $response + */ + public function log(Request $request, Response $response): void + { + $host = $request->url(); + $method = $request->method(); + $agent = $request->userAgent(); + $date = $this->date($response->getDate()); + $status = $response->getStatusCode(); + $style = $this->style($status); + + $this->output->writeln( + sprintf("%s %s %s <$style>%d %s", $host, $date, $method, $status, $agent) + ); + } + + /** + * @param \DateTimeInterface $date + * + * @return string + */ + protected function date(DateTimeInterface $date): string + { + return $date->format('Y-m-d H:i:s'); + } + + /** + * @param int $status + * + * @return string + */ + protected function style(int $status): string + { + return $status !== Response::HTTP_OK ? 'error' : 'info'; + } +} diff --git a/src/Server/Facades/Sandbox.php b/src/Server/Facades/Sandbox.php index 8c468254..6dd2bd4f 100644 --- a/src/Server/Facades/Sandbox.php +++ b/src/Server/Facades/Sandbox.php @@ -4,6 +4,26 @@ use Illuminate\Support\Facades\Facade; +/** + * @method static this setFramework($framework) + * @method static string getFramework() + * @method static this setBaseApp($app) + * @method static \Illuminate\Container\Container getBaseApp() + * @method static \Illuminate\Container\Container getApplication() + * @method static this setRequest($request) + * @method static \Illuminate\Http\Request getRequest($request) + * @method static \Illuminate\Http\Response run() + * @method static this setSnapshot($snapshot) + * @method static \Illuminate\Container\Container getSnapshot() + * @method static this initialize() + * @method static boolean isLaravel() + * @method static void terminate($request, $response) + * @method static void enable() + * @method static void disable() + * @method static void setInstance($app) + * + * @see \SwooleTW\Http\Server\Sandbox + */ class Sandbox extends Facade { /** diff --git a/src/Server/Facades/Server.php b/src/Server/Facades/Server.php index 7a34102a..eb422aa3 100644 --- a/src/Server/Facades/Server.php +++ b/src/Server/Facades/Server.php @@ -4,6 +4,11 @@ use Illuminate\Support\Facades\Facade; +/** + * Class Server + * + * @mixin \Swoole\Http\Server + */ class Server extends Facade { /** diff --git a/src/Server/Manager.php b/src/Server/Manager.php index b5715d81..0495aba1 100644 --- a/src/Server/Manager.php +++ b/src/Server/Manager.php @@ -2,24 +2,37 @@ namespace SwooleTW\Http\Server; +use Exception; use Throwable; -use Swoole\Http\Server; +use Swoole\Process; +use Swoole\Server\Task; +use Illuminate\Support\Str; +use SwooleTW\Http\Helpers\OS; use SwooleTW\Http\Server\Sandbox; +use SwooleTW\Http\Server\PidManager; use SwooleTW\Http\Task\SwooleTaskJob; use Illuminate\Support\Facades\Facade; use SwooleTW\Http\Websocket\Websocket; use SwooleTW\Http\Transformers\Request; +use SwooleTW\Http\Server\Facades\Server; use SwooleTW\Http\Transformers\Response; use SwooleTW\Http\Concerns\WithApplication; use Illuminate\Contracts\Container\Container; use Illuminate\Contracts\Debug\ExceptionHandler; use SwooleTW\Http\Concerns\InteractsWithWebsocket; +use Symfony\Component\Console\Output\ConsoleOutput; +use SwooleTW\Http\Concerns\InteractsWithSwooleQueue; use SwooleTW\Http\Concerns\InteractsWithSwooleTable; +use Symfony\Component\Debug\Exception\FatalThrowableError; +/** + * Class Manager + */ class Manager { use InteractsWithWebsocket, InteractsWithSwooleTable, + InteractsWithSwooleQueue, WithApplication; /** @@ -45,18 +58,30 @@ class Manager * @var array */ protected $events = [ - 'start', 'shutDown', 'workerStart', 'workerStop', 'packet', - 'bufferFull', 'bufferEmpty', 'task', 'finish', 'pipeMessage', - 'workerError', 'managerStart', 'managerStop', 'request', + 'start', + 'shutDown', + 'workerStart', + 'workerStop', + 'packet', + 'bufferFull', + 'bufferEmpty', + 'task', + 'finish', + 'pipeMessage', + 'workerError', + 'managerStart', + 'managerStop', + 'request', ]; /** * HTTP server manager constructor. * - * @param \Swoole\Http\Server $server * @param \Illuminate\Contracts\Container\Container $container * @param string $framework * @param string $basePath + * + * @throws \Exception */ public function __construct(Container $container, $framework, $basePath = null) { @@ -71,7 +96,7 @@ public function __construct(Container $container, $framework, $basePath = null) */ public function run() { - $this->container['swoole.server']->start(); + $this->container->make(Server::class)->start(); } /** @@ -79,7 +104,7 @@ public function run() */ public function stop() { - $this->container['swoole.server']->shutdown(); + $this->container->make(Server::class)->shutdown(); } /** @@ -89,7 +114,10 @@ protected function initialize() { $this->createTables(); $this->prepareWebsocket(); - $this->setSwooleServerListeners(); + + if (! $this->container->make(Server::class)->taskworker) { + $this->setSwooleServerListeners(); + } } /** @@ -97,18 +125,14 @@ protected function initialize() */ protected function setSwooleServerListeners() { + $server = $this->container->make(Server::class); foreach ($this->events as $event) { - $listener = 'on' . ucfirst($event); - - if (method_exists($this, $listener)) { - $this->container['swoole.server']->on($event, [$this, $listener]); - } else { - $this->container['swoole.server']->on($event, function () use ($event) { - $event = sprintf('swoole.%s', $event); + $listener = Str::camel("on_$event"); + $callback = method_exists($this, $listener) ? [$this, $listener] : function () use ($event) { + $this->container->make('events')->dispatch("swoole.$event", func_get_args()); + }; - $this->container['events']->fire($event, func_get_args()); - }); - } + $server->on($event, $callback); } } @@ -118,9 +142,11 @@ protected function setSwooleServerListeners() public function onStart() { $this->setProcessName('master process'); - $this->createPidFile(); - $this->container['events']->fire('swoole.start', func_get_args()); + $server = $this->container->make(Server::class); + $this->container->make(PidManager::class)->write($server->master_pid, $server->manager_pid ?? 0); + + $this->container->make('events')->dispatch('swoole.start', func_get_args()); } /** @@ -131,23 +157,24 @@ public function onStart() public function onManagerStart() { $this->setProcessName('manager process'); - $this->container['events']->fire('swoole.managerStart', func_get_args()); + + $this->container->make('events')->dispatch('swoole.managerStart', func_get_args()); } /** * "onWorkerStart" listener. + * + * @param \Swoole\Http\Server|mixed $server + * + * @throws \Exception */ public function onWorkerStart($server) { $this->clearCache(); - $this->setProcessName('worker process'); - $this->container['events']->fire('swoole.workerStart', func_get_args()); + $this->container->make('events')->dispatch('swoole.workerStart', func_get_args()); - // don't init laravel app in task workers - if ($server->taskworker) { - return; - } + $this->setProcessName($server->taskworker ? 'task process' : 'worker process'); // clear events instance in case of repeated listeners in worker process Facade::clearResolvedInstance('events'); @@ -159,7 +186,7 @@ public function onWorkerStart($server) $this->bindToLaravelApp(); // prepare websocket handler and routes - if ($this->isWebsocket) { + if ($this->isServerWebsocket) { $this->prepareWebsocketHandler(); $this->loadWebsocketRoutes(); } @@ -173,11 +200,12 @@ public function onWorkerStart($server) */ public function onRequest($swooleRequest, $swooleResponse) { - $this->app['events']->fire('swoole.request'); + $this->app->make('events')->dispatch('swoole.request'); $this->resetOnRequest(); - $handleStatic = $this->container['config']->get('swoole_http.handle_static_files', true); - $publicPath = $this->container['config']->get('swoole_http.server.public_path', base_path('public')); + $sandbox = $this->app->make(Sandbox::class); + $handleStatic = $this->container->make('config')->get('swoole_http.server.handle_static_files', true); + $publicPath = $this->container->make('config')->get('swoole_http.server.public_path', base_path('public')); try { // handle static file request first @@ -188,25 +216,31 @@ public function onRequest($swooleRequest, $swooleResponse) $illuminateRequest = Request::make($swooleRequest)->toIlluminate(); // set current request to sandbox - $this->app['swoole.sandbox']->setRequest($illuminateRequest); + $sandbox->setRequest($illuminateRequest); + // enable sandbox - $this->app['swoole.sandbox']->enable(); + $sandbox->enable(); // handle request via laravel/lumen's dispatcher - $illuminateResponse = $this->app['swoole.sandbox']->run($illuminateRequest); - $response = Response::make($illuminateResponse, $swooleResponse); - $response->send(); + $illuminateResponse = $sandbox->run($illuminateRequest); + + // send response + Response::make($illuminateResponse, $swooleResponse)->send(); } catch (Throwable $e) { try { - $exceptionResponse = $this->app[ExceptionHandler::class]->render($illuminateRequest, $e); - $response = Response::make($exceptionResponse, $swooleResponse); - $response->send(); + $exceptionResponse = $this->app + ->make(ExceptionHandler::class) + ->render( + $illuminateRequest, + $this->normalizeException($e) + ); + Response::make($exceptionResponse, $swooleResponse)->send(); } catch (Throwable $e) { $this->logServerError($e); } } finally { // disable and recycle sandbox resource - $this->app['swoole.sandbox']->disable(); + $sandbox->disable(); } } @@ -216,33 +250,30 @@ public function onRequest($swooleRequest, $swooleResponse) protected function resetOnRequest() { // Reset websocket data - if ($this->isWebsocket) { - $this->app['swoole.websocket']->reset(true); + if ($this->isServerWebsocket) { + $this->app->make(Websocket::class)->reset(true); } } /** * Set onTask listener. + * + * @param mixed $server + * @param string|\Swoole\Server\Task $taskId or $task + * @param string $srcWorkerId + * @param mixed $data */ public function onTask($server, $taskId, $srcWorkerId, $data) { - $this->container['events']->fire('swoole.task', func_get_args()); + $this->container->make('events')->dispatch('swoole.task', func_get_args()); try { // push websocket message - if (is_array($data)) { - if ($this->isWebsocket - && array_key_exists('action', $data) - && $data['action'] === Websocket::PUSH_ACTION) { - $this->pushMessage($server, $data['data'] ?? []); - } + if ($this->isWebsocketPushPayload($data)) { + $this->pushMessage($server, $data['data']); // push async task to queue - } elseif (is_string($data)) { - $decoded = json_decode($data, true); - - if (JSON_ERROR_NONE === json_last_error() && isset($decoded['job'])) { - (new SwooleTaskJob($this->container, $server, $data, $taskId, $srcWorkerId))->fire(); - } + } elseif ($this->isAsyncTaskPayload($data)) { + (new SwooleTaskJob($this->container, $server, $data, $taskId, $srcWorkerId))->fire(); } } catch (Throwable $e) { $this->logServerError($e); @@ -251,10 +282,16 @@ public function onTask($server, $taskId, $srcWorkerId, $data) /** * Set onFinish listener. + * + * @param mixed $server + * @param string $taskId + * @param mixed $data */ public function onFinish($server, $taskId, $data) { // task worker callback + $this->container->make('events')->dispatch('swoole.finish', func_get_args()); + return; } @@ -263,7 +300,7 @@ public function onFinish($server, $taskId, $data) */ public function onShutdown() { - $this->removePidFile(); + $this->container->make(PidManager::class)->delete(); } /** @@ -274,7 +311,7 @@ protected function bindToLaravelApp() $this->bindSandbox(); $this->bindSwooleTable(); - if ($this->isWebsocket) { + if ($this->isServerWebsocket) { $this->bindRoom(); $this->bindWebsocket(); } @@ -288,40 +325,8 @@ protected function bindSandbox() $this->app->singleton(Sandbox::class, function ($app) { return new Sandbox($app, $this->framework); }); - $this->app->alias(Sandbox::class, 'swoole.sandbox'); - } - - /** - * Gets pid file path. - * - * @return string - */ - protected function getPidFile() - { - return $this->container['config']->get('swoole_http.server.options.pid_file'); - } - - /** - * Create pid file. - */ - protected function createPidFile() - { - $pidFile = $this->getPidFile(); - $pid = $this->container['swoole.server']->master_pid; - - file_put_contents($pidFile, $pid); - } - /** - * Remove pid file. - */ - protected function removePidFile() - { - $pidFile = $this->getPidFile(); - - if (file_exists($pidFile)) { - unlink($pidFile); - } + $this->app->alias(Sandbox::class, 'swoole.sandbox'); } /** @@ -329,11 +334,11 @@ protected function removePidFile() */ protected function clearCache() { - if (function_exists('apc_clear_cache')) { + if (extension_loaded('apc')) { apc_clear_cache(); } - if (function_exists('opcache_reset')) { + if (extension_loaded('Zend OPcache')) { opcache_reset(); } } @@ -342,16 +347,17 @@ protected function clearCache() * Set process name. * * @codeCoverageIgnore + * * @param $process */ protected function setProcessName($process) { // MacOS doesn't support modifying process name. - if ($this->isMacOS() || $this->isInTesting()) { + if (OS::is(OS::MAC_OS, OS::CYGWIN) || $this->isInTesting()) { return; } $serverName = 'swoole_http_server'; - $appName = $this->container['config']->get('app.name', 'Laravel'); + $appName = $this->container->make('config')->get('app.name', 'Laravel'); $name = sprintf('%s: %s for %s', $serverName, $process, $appName); @@ -359,13 +365,13 @@ protected function setProcessName($process) } /** - * Indicates if the process is running in macOS. + * Add process to http server * - * @return bool + * @param \Swoole\Process $process */ - protected function isMacOS() + public function addProcess(Process $process): void { - return PHP_OS === 'Darwin'; + $this->container->make(Server::class)->addProcess($process); } /** @@ -381,10 +387,51 @@ protected function isInTesting() /** * Log server error. * - * @param Throwable + * @param \Throwable|\Exception $e */ public function logServerError(Throwable $e) { - $this->container[ExceptionHandler::class]->report($e); + if ($this->isInTesting()) { + return; + } + + $exception = $this->normalizeException($e); + $this->container->make(ConsoleOutput::class) + ->writeln(sprintf("%s", $exception)); + + $this->container->make(ExceptionHandler::class) + ->report($exception); + } + + /** + * Normalize a throwable/exception to exception. + * + * @param \Throwable|\Exception $e + */ + protected function normalizeException(Throwable $e) + { + if (! $e instanceof Exception) { + $e = new FatalThrowableError($e); + } + + return $e; + } + + /** + * Indicates if the payload is async task. + * + * @param mixed $payload + * + * @return boolean + */ + protected function isAsyncTaskPayload($payload): bool + { + $data = json_decode($payload, true); + + if (JSON_ERROR_NONE !== json_last_error()) { + return false; + } + + return isset($data['job']); } } diff --git a/src/Server/PidManager.php b/src/Server/PidManager.php new file mode 100644 index 00000000..1e17babd --- /dev/null +++ b/src/Server/PidManager.php @@ -0,0 +1,88 @@ +setPidFile( + $pidFile ?: sys_get_temp_dir() . '/swoole.pid' + ); + } + + /** + * Set pid file path + */ + public function setPidFile(string $pidFile): self + { + $this->pidFile = $pidFile; + + return $this; + } + + /** + * Write master pid and manager pid to pid file + * + * @throws \RuntimeException when $pidFile is not writable + */ + public function write(int $masterPid, int $managerPid): void + { + if (! is_writable($this->pidFile) + && ! is_writable(dirname($this->pidFile)) + ) { + throw new \RuntimeException( + sprintf('Pid file "%s" is not writable', $this->pidFile) + ); + } + + file_put_contents($this->pidFile, $masterPid . ',' . $managerPid); + } + + /** + * Read master pid and manager pid from pid file + * + * @return string[] { + * @var string $masterPid + * @var string $managerPid + * } + */ + public function read(): array + { + $pids = []; + + if (is_readable($this->pidFile)) { + $content = file_get_contents($this->pidFile); + $pids = explode(',', $content); + } + + return [ + 'masterPid' => $pids[0] ?? null, + 'managerPid' => $pids[1] ?? null + ]; + } + + /** + * Gets pid file path. + * + * @return string + */ + public function file() + { + return $this->pidFile; + } + + /** + * Delete pid file + */ + public function delete(): bool + { + if (is_writable($this->pidFile)) { + return unlink($this->pidFile); + } + + return false; + } +} diff --git a/src/Server/Resetters/BindRequest.php b/src/Server/Resetters/BindRequest.php index 35df56ce..b0787829 100644 --- a/src/Server/Resetters/BindRequest.php +++ b/src/Server/Resetters/BindRequest.php @@ -2,11 +2,13 @@ namespace SwooleTW\Http\Server\Resetters; +use Illuminate\Contracts\Container\Container; use Illuminate\Http\Request; use SwooleTW\Http\Server\Sandbox; -use Illuminate\Contracts\Container\Container; -use SwooleTW\Http\Server\Resetters\ResetterContract; +/** + * Class BindRequest + */ class BindRequest implements ResetterContract { /** @@ -14,6 +16,8 @@ class BindRequest implements ResetterContract * * @param \Illuminate\Contracts\Container\Container $app * @param \SwooleTW\Http\Server\Sandbox $sandbox + * + * @return \Illuminate\Contracts\Container\Container */ public function handle(Container $app, Sandbox $sandbox) { diff --git a/src/Server/Resetters/ClearInstances.php b/src/Server/Resetters/ClearInstances.php index 24fc6963..865516f1 100644 --- a/src/Server/Resetters/ClearInstances.php +++ b/src/Server/Resetters/ClearInstances.php @@ -2,9 +2,8 @@ namespace SwooleTW\Http\Server\Resetters; -use SwooleTW\Http\Server\Sandbox; use Illuminate\Contracts\Container\Container; -use SwooleTW\Http\Server\Resetters\ResetterContract; +use SwooleTW\Http\Server\Sandbox; class ClearInstances implements ResetterContract { @@ -13,6 +12,8 @@ class ClearInstances implements ResetterContract * * @param \Illuminate\Contracts\Container\Container $app * @param \SwooleTW\Http\Server\Sandbox $sandbox + * + * @return \Illuminate\Contracts\Container\Container */ public function handle(Container $app, Sandbox $sandbox) { diff --git a/src/Server/Resetters/RebindKernelContainer.php b/src/Server/Resetters/RebindKernelContainer.php index f97a07ac..f2d8b9b4 100644 --- a/src/Server/Resetters/RebindKernelContainer.php +++ b/src/Server/Resetters/RebindKernelContainer.php @@ -2,18 +2,24 @@ namespace SwooleTW\Http\Server\Resetters; -use SwooleTW\Http\Server\Sandbox; -use Illuminate\Contracts\Http\Kernel; use Illuminate\Contracts\Container\Container; -use SwooleTW\Http\Server\Resetters\ResetterContract; +use Illuminate\Contracts\Http\Kernel; +use SwooleTW\Http\Server\Sandbox; class RebindKernelContainer implements ResetterContract { + /** + * @var \Illuminate\Contracts\Container\Container + */ + protected $app; + /** * "handle" function for resetting app. * * @param \Illuminate\Contracts\Container\Container $app * @param \SwooleTW\Http\Server\Sandbox $sandbox + * + * @return \Illuminate\Contracts\Container\Container */ public function handle(Container $app, Sandbox $sandbox) { diff --git a/src/Server/Resetters/RebindRouterContainer.php b/src/Server/Resetters/RebindRouterContainer.php index c51398ce..15274195 100644 --- a/src/Server/Resetters/RebindRouterContainer.php +++ b/src/Server/Resetters/RebindRouterContainer.php @@ -2,18 +2,29 @@ namespace SwooleTW\Http\Server\Resetters; -use SwooleTW\Http\Server\Sandbox; use Illuminate\Contracts\Container\Container; -use SwooleTW\Http\Server\Resetters\ResetterContract; +use SwooleTW\Http\Server\Sandbox; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; class RebindRouterContainer implements ResetterContract { + /** + * @var \Illuminate\Contracts\Container\Container + */ + protected $container; + + /** + * @var mixed + */ + protected $routes; + /** * "handle" function for resetting app. * * @param \Illuminate\Contracts\Container\Container $app * @param \SwooleTW\Http\Server\Sandbox $sandbox + * + * @return \Illuminate\Contracts\Container\Container */ public function handle(Container $app, Sandbox $sandbox) { @@ -26,6 +37,7 @@ public function handle(Container $app, Sandbox $sandbox) return; } try { + /** @var mixed $route */ $route = $this->routes->match($request); // clear resolved controller if (property_exists($route, 'container')) { diff --git a/src/Server/Resetters/RebindViewContainer.php b/src/Server/Resetters/RebindViewContainer.php index 52bd5ab1..525f9daa 100644 --- a/src/Server/Resetters/RebindViewContainer.php +++ b/src/Server/Resetters/RebindViewContainer.php @@ -2,17 +2,28 @@ namespace SwooleTW\Http\Server\Resetters; -use SwooleTW\Http\Server\Sandbox; use Illuminate\Contracts\Container\Container; -use SwooleTW\Http\Server\Resetters\ResetterContract; +use SwooleTW\Http\Server\Sandbox; class RebindViewContainer implements ResetterContract { + /** + * @var Container + */ + protected $container; + + /** + * @var array + */ + protected $shared; + /** * "handle" function for resetting app. * * @param \Illuminate\Contracts\Container\Container $app * @param \SwooleTW\Http\Server\Sandbox $sandbox + * + * @return \Illuminate\Contracts\Container\Container */ public function handle(Container $app, Sandbox $sandbox) { diff --git a/src/Server/Resetters/ResetConfig.php b/src/Server/Resetters/ResetConfig.php index 5fbfd042..beb44b7d 100644 --- a/src/Server/Resetters/ResetConfig.php +++ b/src/Server/Resetters/ResetConfig.php @@ -4,7 +4,6 @@ use SwooleTW\Http\Server\Sandbox; use Illuminate\Contracts\Container\Container; -use SwooleTW\Http\Server\Resetters\ResetterContract; class ResetConfig implements ResetterContract { @@ -13,6 +12,8 @@ class ResetConfig implements ResetterContract * * @param \Illuminate\Contracts\Container\Container $app * @param \SwooleTW\Http\Server\Sandbox $sandbox + * + * @return \Illuminate\Contracts\Container\Container */ public function handle(Container $app, Sandbox $sandbox) { diff --git a/src/Server/Resetters/ResetCookie.php b/src/Server/Resetters/ResetCookie.php index c9b76da9..28d26662 100644 --- a/src/Server/Resetters/ResetCookie.php +++ b/src/Server/Resetters/ResetCookie.php @@ -2,9 +2,8 @@ namespace SwooleTW\Http\Server\Resetters; -use SwooleTW\Http\Server\Sandbox; use Illuminate\Contracts\Container\Container; -use SwooleTW\Http\Server\Resetters\ResetterContract; +use SwooleTW\Http\Server\Sandbox; class ResetCookie implements ResetterContract { @@ -13,13 +12,15 @@ class ResetCookie implements ResetterContract * * @param \Illuminate\Contracts\Container\Container $app * @param \SwooleTW\Http\Server\Sandbox $sandbox + * + * @return \Illuminate\Contracts\Container\Container */ public function handle(Container $app, Sandbox $sandbox) { if (isset($app['cookie'])) { $cookies = $app->make('cookie'); foreach ($cookies->getQueuedCookies() as $key => $value) { - $cookies->unqueue($key); + $cookies->unqueue($value->getName()); } } diff --git a/src/Server/Resetters/ResetProviders.php b/src/Server/Resetters/ResetProviders.php index 0b11648e..0193106b 100644 --- a/src/Server/Resetters/ResetProviders.php +++ b/src/Server/Resetters/ResetProviders.php @@ -2,17 +2,23 @@ namespace SwooleTW\Http\Server\Resetters; -use SwooleTW\Http\Server\Sandbox; use Illuminate\Contracts\Container\Container; -use SwooleTW\Http\Server\Resetters\ResetterContract; +use SwooleTW\Http\Server\Sandbox; class ResetProviders implements ResetterContract { + /** + * @var \Illuminate\Contracts\Container\Container + */ + protected $app; + /** * "handle" function for resetting app. * * @param \Illuminate\Contracts\Container\Container $app * @param \SwooleTW\Http\Server\Sandbox $sandbox + * + * @return \Illuminate\Contracts\Container\Container */ public function handle(Container $app, Sandbox $sandbox) { @@ -31,6 +37,9 @@ public function handle(Container $app, Sandbox $sandbox) /** * Rebind service provider's container. + * + * @param $app + * @param $provider */ protected function rebindProviderContainer($app, $provider) { diff --git a/src/Server/Resetters/ResetSession.php b/src/Server/Resetters/ResetSession.php index 84db650c..9a97ac53 100644 --- a/src/Server/Resetters/ResetSession.php +++ b/src/Server/Resetters/ResetSession.php @@ -2,9 +2,8 @@ namespace SwooleTW\Http\Server\Resetters; -use SwooleTW\Http\Server\Sandbox; use Illuminate\Contracts\Container\Container; -use SwooleTW\Http\Server\Resetters\ResetterContract; +use SwooleTW\Http\Server\Sandbox; class ResetSession implements ResetterContract { @@ -13,6 +12,8 @@ class ResetSession implements ResetterContract * * @param \Illuminate\Contracts\Container\Container $app * @param \SwooleTW\Http\Server\Sandbox $sandbox + * + * @return \Illuminate\Contracts\Container\Container */ public function handle(Container $app, Sandbox $sandbox) { diff --git a/src/Server/Resetters/ResetterContract.php b/src/Server/Resetters/ResetterContract.php index 5f63611f..27a5d85c 100644 --- a/src/Server/Resetters/ResetterContract.php +++ b/src/Server/Resetters/ResetterContract.php @@ -2,8 +2,8 @@ namespace SwooleTW\Http\Server\Resetters; -use SwooleTW\Http\Server\Sandbox; use Illuminate\Contracts\Container\Container; +use SwooleTW\Http\Server\Sandbox; interface ResetterContract { diff --git a/src/Server/Sandbox.php b/src/Server/Sandbox.php index 0dbf367f..1fcad3bd 100644 --- a/src/Server/Sandbox.php +++ b/src/Server/Sandbox.php @@ -31,6 +31,11 @@ class Sandbox /** * Constructor + * + * @param null $app + * @param null $framework + * + * @throws \SwooleTW\Http\Exceptions\SandboxException */ public function __construct($app = null, $framework = null) { @@ -45,6 +50,10 @@ public function __construct($app = null, $framework = null) /** * Set framework type. + * + * @param string $framework + * + * @return \SwooleTW\Http\Server\Sandbox */ public function setFramework(string $framework) { @@ -65,6 +74,8 @@ public function getFramework() * Set a base application. * * @param \Illuminate\Container\Container + * + * @return \SwooleTW\Http\Server\Sandbox */ public function setBaseApp(Container $app) { @@ -77,6 +88,8 @@ public function setBaseApp(Container $app) * Set current request. * * @param \Illuminate\Http\Request + * + * @return \SwooleTW\Http\Server\Sandbox */ public function setRequest(Request $request) { @@ -89,6 +102,8 @@ public function setRequest(Request $request) * Set current snapshot. * * @param \Illuminate\Container\Container + * + * @return \SwooleTW\Http\Server\Sandbox */ public function setSnapshot(Container $snapshot) { @@ -99,6 +114,8 @@ public function setSnapshot(Container $snapshot) /** * Initialize based on base app. + * + * @throws \SwooleTW\Http\Exceptions\SandboxException */ public function initialize() { @@ -145,7 +162,10 @@ public function getApplication() * Run framework. * * @param \Illuminate\Http\Request $request + * * @return \Illuminate\Http\Response + * @throws \SwooleTW\Http\Exceptions\SandboxException + * @throws \ReflectionException */ public function run(Request $request) { @@ -166,7 +186,9 @@ public function run(Request $request) * Handle request for non-ob case. * * @param \Illuminate\Http\Request $request + * * @return \Illuminate\Http\Response + * @throws \ReflectionException */ protected function prepareResponse(Request $request) { @@ -183,7 +205,9 @@ protected function prepareResponse(Request $request) * Handle request for ob output. * * @param \Illuminate\Http\Request $request + * * @return \Illuminate\Http\Response + * @throws \ReflectionException */ protected function prepareObResponse(Request $request) { @@ -224,6 +248,7 @@ protected function prepareObResponse(Request $request) * Handle request through Laravel or Lumen. * * @param \Illuminate\Http\Request $request + * * @return \Illuminate\Http\Response */ protected function handleRequest(Request $request) @@ -254,29 +279,35 @@ public function isLaravel() /** * @param \Illuminate\Http\Request $request * @param \Illuminate\Http\Response $response + * + * @throws \ReflectionException */ public function terminate(Request $request, $response) { if ($this->isLaravel()) { $this->getKernel()->terminate($request, $response); - } else { - $app = $this->getApplication(); - $reflection = new \ReflectionObject($app); - $middleware = $reflection->getProperty('middleware'); - $middleware->setAccessible(true); + return; + } - $callTerminableMiddleware = $reflection->getMethod('callTerminableMiddleware'); - $callTerminableMiddleware->setAccessible(true); + $app = $this->getApplication(); + $reflection = new \ReflectionObject($app); - if (count($middleware->getValue($app)) > 0) { - $callTerminableMiddleware->invoke($app, $response); - } + $middleware = $reflection->getProperty('middleware'); + $middleware->setAccessible(true); + + $callTerminableMiddleware = $reflection->getMethod('callTerminableMiddleware'); + $callTerminableMiddleware->setAccessible(true); + + if (count($middleware->getValue($app)) > 0) { + $callTerminableMiddleware->invoke($app, $response); } } /** * Set laravel snapshot to container and facade. + * + * @throws \SwooleTW\Http\Exceptions\SandboxException */ public function enable() { @@ -293,12 +324,14 @@ public function enable() */ public function disable() { - Context::clear(); $this->setInstance($this->getBaseApp()); + Context::clear(); } /** * Replace app's self bindings. + * + * @param \Illuminate\Container\Container $app */ public function setInstance(Container $app) { @@ -323,14 +356,6 @@ public function getSnapshot() return Context::getApp(); } - /** - * Remove current request. - */ - protected function removeRequest() - { - return Context::removeData('_request'); - } - /** * Get current request. */ diff --git a/src/Server/helpers.php b/src/Server/helpers.php index 7fb15785..f9c43ba8 100644 --- a/src/Server/helpers.php +++ b/src/Server/helpers.php @@ -4,9 +4,9 @@ * This is only for `function not exists` in config/swoole_http.php. */ if (! function_exists('swoole_cpu_num')) { - function swoole_cpu_num() + function swoole_cpu_num(): int { - return; + return 1; } } @@ -16,3 +16,7 @@ function swoole_cpu_num() if (! defined('SWOOLE_SOCK_TCP')) { define('SWOOLE_SOCK_TCP', 1); } + +if (! defined('SWOOLE_PROCESS')) { + define('SWOOLE_PROCESS', 2); +} diff --git a/src/Table/Facades/SwooleTable.php b/src/Table/Facades/SwooleTable.php index f5441690..67510a2a 100644 --- a/src/Table/Facades/SwooleTable.php +++ b/src/Table/Facades/SwooleTable.php @@ -4,6 +4,13 @@ use Illuminate\Support\Facades\Facade; +/** + * @method static \SwooleTW\Http\Table\SwooleTable add($name, $table) + * @method static \Swoole\Table get($name) + * @method static array getAll() + * + * @see \SwooleTW\Http\Table\SwooleTable + */ class SwooleTable extends Facade { /** diff --git a/src/Table/SwooleTable.php b/src/Table/SwooleTable.php index 9dfe8947..3c154775 100644 --- a/src/Table/SwooleTable.php +++ b/src/Table/SwooleTable.php @@ -18,6 +18,7 @@ class SwooleTable * * @param string $name * @param \Swoole\Table $table + * * @return \SwooleTW\Http\Table\SwooleTable */ public function add(string $name, Table $table) @@ -31,6 +32,7 @@ public function add(string $name, Table $table) * Get a swoole table by its name from existing tables. * * @param string $name + * * @return \Swoole\Table $table */ public function get(string $name) @@ -51,7 +53,8 @@ public function getAll() /** * Dynamically access table. * - * @param string $key + * @param string $key + * * @return table */ public function __get($key) diff --git a/src/Task/Connectors/SwooleTaskConnector.php b/src/Task/Connectors/SwooleTaskConnector.php index 3b690aac..6e3c0664 100644 --- a/src/Task/Connectors/SwooleTaskConnector.php +++ b/src/Task/Connectors/SwooleTaskConnector.php @@ -2,9 +2,13 @@ namespace SwooleTW\Http\Task\Connectors; -use SwooleTW\Http\Task\SwooleTaskQueue; use Illuminate\Queue\Connectors\ConnectorInterface; +use SwooleTW\Http\Helpers\FW; +use SwooleTW\Http\Task\QueueFactory; +/** + * Class SwooleTaskConnector + */ class SwooleTaskConnector implements ConnectorInterface { /** @@ -14,10 +18,11 @@ class SwooleTaskConnector implements ConnectorInterface */ protected $swoole; - /** + /** * Create a new Swoole Async task connector instance. * * @param \Swoole\Http\Server $swoole + * * @return void */ public function __construct($swoole) @@ -28,11 +33,12 @@ public function __construct($swoole) /** * Establish a queue connection. * - * @param array $config + * @param array $config + * * @return \Illuminate\Contracts\Queue\Queue */ public function connect(array $config) { - return new SwooleTaskQueue($this->swoole); + return QueueFactory::make($this->swoole, FW::version()); } } diff --git a/src/Task/QueueFactory.php b/src/Task/QueueFactory.php new file mode 100644 index 00000000..99ddc73d --- /dev/null +++ b/src/Task/QueueFactory.php @@ -0,0 +1,107 @@ +getDocComment(), $result)) { + $fileVersion = Arr::first($result); + } + } + + return version_compare($fileVersion, $version, '>='); + } catch (\Exception $e) { + return false; + } + } + + /** + * @param string $version + * + * @return bool + */ + protected static function hasBreakingChanges(string $version): bool + { + return version_compare($version, self::CHANGE_VERSION, '>='); + } +} diff --git a/src/Task/SwooleTaskJob.php b/src/Task/SwooleTaskJob.php index 5a70b7a9..5aa0fc64 100644 --- a/src/Task/SwooleTaskJob.php +++ b/src/Task/SwooleTaskJob.php @@ -6,6 +6,9 @@ use Illuminate\Contracts\Container\Container; use Illuminate\Contracts\Queue\Job as JobContract; +/** + * Class SwooleTaskJob + */ class SwooleTaskJob extends Job implements JobContract { /** @@ -36,15 +39,19 @@ class SwooleTaskJob extends Job implements JobContract */ protected $srcWrokerId; + /** + * @var int + */ + protected $srcWorkderId; + /** * Create a new job instance. * - * @param \Illuminate\Container\Container $container - * @param \Swoole\Http\Server $swoole - * @param string $job - * @param int $taskId - * @param int $srcWorkerId - * @return void + * @param \Illuminate\Contracts\Container\Container $container + * @param \Swoole\Http\Server $swoole + * @param string $job + * @param int $taskId + * @param $srcWrokerId */ public function __construct(Container $container, $swoole, $job, $taskId, $srcWrokerId) { @@ -71,6 +78,7 @@ public function fire() /** * Get the number of times the job has been attempted. + * * @return int */ public function attempts() @@ -80,6 +88,7 @@ public function attempts() /** * Get the raw body string for the job. + * * @return string */ public function getRawBody() @@ -87,9 +96,9 @@ public function getRawBody() return $this->job; } - /** * Get the job identifier. + * * @return string */ public function getJobId() @@ -100,7 +109,7 @@ public function getJobId() /** * Get the service container instance. * - * @return \Illuminate\Container\Container + * @return \Illuminate\Contracts\Container\Container */ public function getContainer() { diff --git a/src/Task/SwooleTaskQueue.php b/src/Task/SwooleTaskQueue.php index 734fbc4a..5eb0e73d 100644 --- a/src/Task/SwooleTaskQueue.php +++ b/src/Task/SwooleTaskQueue.php @@ -3,10 +3,13 @@ namespace SwooleTW\Http\Task; use Exception; -use Swoole\Timer; -use Illuminate\Queue\Queue; use Illuminate\Contracts\Queue\Queue as QueueContract; +use Illuminate\Queue\Queue; +use Swoole\Timer; +/** + * Class SwooleTaskQueue (5.7) + */ class SwooleTaskQueue extends Queue implements QueueContract { /** @@ -19,7 +22,7 @@ class SwooleTaskQueue extends Queue implements QueueContract /** * Create Async Task instance. * - * @param \Swoole\Http\Server $swoole + * @param \Swoole\Http\Server $swoole */ public function __construct($swoole) { @@ -29,9 +32,10 @@ public function __construct($swoole) /** * Push a new job onto the queue. * - * @param string|object $job - * @param mixed $data - * @param string $queue + * @param string|object $job + * @param mixed $data + * @param string $queue + * * @return mixed */ public function push($job, $data = '', $queue = null) @@ -42,23 +46,25 @@ public function push($job, $data = '', $queue = null) /** * Push a raw payload onto the queue. * - * @param string $payload - * @param string $queue - * @param array $options + * @param string $payload + * @param string $queue + * @param array $options + * * @return mixed */ public function pushRaw($payload, $queue = null, array $options = []) { - return $this->swoole->task($payload, ! is_numeric($queue) ? 1 : (int) $queue); + return $this->swoole->task($payload, ! is_numeric($queue) ? -1 : (int)$queue); } /** * Push a new job onto the queue after a delay. * - * @param \DateTimeInterface|\DateInterval|int $delay - * @param string|object $job - * @param mixed $data - * @param string $queue + * @param \DateTimeInterface|\DateInterval|int $delay + * @param string|object $job + * @param mixed $data + * @param string $queue + * * @return mixed */ public function later($delay, $job, $data = '', $queue = null) @@ -71,12 +77,13 @@ public function later($delay, $job, $data = '', $queue = null) /** * Create a typical, string based queue payload array. * - * @param string $job - * @param mixed $data + * @param string $job + * @param string $queue + * @param mixed $data * - * @throws Expcetion + * @throws \Exception */ - protected function createStringPayload($job, $data) + protected function createStringPayload($job, $queue, $data) { throw new Exception('Unsupported empty data'); } @@ -84,7 +91,8 @@ protected function createStringPayload($job, $data) /** * Get the size of the queue. * - * @param string $queue + * @param string $queue + * * @return int */ public function size($queue = null) @@ -95,7 +103,8 @@ public function size($queue = null) /** * Pop the next job off of the queue. * - * @param string $queue + * @param string $queue + * * @return \Illuminate\Contracts\Queue\Job|null */ public function pop($queue = null) diff --git a/src/Transformers/Request.php b/src/Transformers/Request.php index 704ab911..1dbd41db 100644 --- a/src/Transformers/Request.php +++ b/src/Transformers/Request.php @@ -3,14 +3,29 @@ namespace SwooleTW\Http\Transformers; use Illuminate\Http\Request as IlluminateRequest; +use Illuminate\Http\Response as IlluminateResponse; use Swoole\Http\Request as SwooleRequest; -use Swoole\Http\Response as SwooleResponse; +use SwooleTW\Http\Helpers\FW; +use SwooleTW\Http\Helpers\MimeType; use Symfony\Component\HttpFoundation\ParameterBag; use Symfony\Component\HttpFoundation\Request as SymfonyRequest; +/** + * Class Request + */ class Request { - const BLACK_LIST = ['php', 'htaccess', 'config']; + /** + * Blacklisted extensions + * + * @const array + */ + protected const EXTENSION_BLACKLIST = ['php', 'htaccess', 'config']; + + /** + * Extension mime types + */ + protected const EXTENSION_MIMES = ['js' => 'text/javascript', 'css' => 'text/css']; /** * @var \Illuminate\Http\Request @@ -21,30 +36,22 @@ class Request * Make a request. * * @param \Swoole\Http\Request $swooleRequest - * @return \SwooleTW\Http\Server\Request + * + * @return \SwooleTW\Http\Transformers\Request */ public static function make(SwooleRequest $swooleRequest) { - list($get, $post, $cookie, $files, $server, $content) - = static::toIlluminateParameters($swooleRequest); - - return new static($get, $post, $cookie, $files, $server, $content); + return new static(...static::toIlluminateParameters($swooleRequest)); } /** * Request constructor. * - * @param array $get - * @param array $post - * @param array $cookie - * @param array $files - * @param array $server - * @param string $content - * @throws \LogicException + * @param array $params provides GET, POST, COOKIE, FILES, SERVER, CONTENT */ - public function __construct(array $get, array $post, array $cookie, array $files, array $server, $content = null) + public function __construct(...$params) { - $this->createIlluminateRequest($get, $post, $cookie, $files, $server, $content); + $this->createIlluminateRequest(...$params); } /** @@ -56,8 +63,8 @@ public function __construct(array $get, array $post, array $cookie, array $files * @param array $files * @param array $server * @param string $content - * @return \Illuminate\Http\Request - * @throws \LogicException + * + * @return void */ protected function createIlluminateRequest($get, $post, $cookie, $files, $server, $content = null) { @@ -86,7 +93,7 @@ protected function createIlluminateRequest($get, $post, $cookie, $files, $server $request = new SymfonyRequest($get, $post, [], $cookie, $files, $server, $content); if (0 === strpos($request->headers->get('CONTENT_TYPE'), 'application/x-www-form-urlencoded') - && in_array(strtoupper($request->server->get('REQUEST_METHOD', 'GET')), array('PUT', 'DELETE', 'PATCH')) + && in_array(strtoupper($request->server->get('REQUEST_METHOD', 'GET')), ['PUT', 'DELETE', 'PATCH']) ) { parse_str($request->getContent(), $data); $request->request = new ParameterBag($data); @@ -115,16 +122,17 @@ public function getIlluminateRequest() * Transforms request parameters. * * @param \Swoole\Http\Request $request + * * @return array */ protected static function toIlluminateParameters(SwooleRequest $request) { - $get = isset($request->get) ? $request->get : []; - $post = isset($request->post) ? $request->post : []; - $cookie = isset($request->cookie) ? $request->cookie : []; - $files = isset($request->files) ? $request->files : []; - $header = isset($request->header) ? $request->header : []; - $server = isset($request->server) ? $request->server : []; + $get = $request->get ?? []; + $post = $request->post ?? []; + $cookie = $request->cookie ?? []; + $files = $request->files ?? []; + $header = $request->header ?? []; + $server = $request->server ?? []; $server = static::transformServerParameters($server, $header); $content = $request->rawContent(); @@ -136,6 +144,7 @@ protected static function toIlluminateParameters(SwooleRequest $request) * * @param array $server * @param array $header + * * @return array */ protected static function transformServerParameters(array $server, array $header) @@ -166,31 +175,27 @@ protected static function transformServerParameters(array $server, array $header * * @param \Swoole\Http\Request $swooleRequest * @param \Swoole\Http\Response $swooleResponse - * @param string $path + * @param string $publicPath + * * @return boolean */ public static function handleStatic($swooleRequest, $swooleResponse, string $publicPath) { $uri = $swooleRequest->server['request_uri'] ?? ''; - $extension = substr(strrchr($uri, '.'), 1); - if ($extension && in_array($extension, static::BLACK_LIST)) { - return; - } + $extension = strtok(pathinfo($uri, PATHINFO_EXTENSION), '?'); + $fileName = $publicPath . $uri; - $filename = $publicPath . $uri; - if (! is_file($filename) || filesize($filename) === 0) { - return; + if ($extension && in_array($extension, static::EXTENSION_BLACKLIST)) { + return false; } - $swooleResponse->status(200); - $mime = mime_content_type($filename); - if ($extension === 'js') { - $mime = 'text/javascript'; - } elseif ($extension === 'css') { - $mime = 'text/css'; + if (! is_file($fileName) || ! filesize($fileName)) { + return false; } - $swooleResponse->header('Content-Type', $mime); - $swooleResponse->sendfile($filename); + + $swooleResponse->status(IlluminateResponse::HTTP_OK); + $swooleResponse->header('Content-Type', MimeType::get($extension)); + $swooleResponse->sendfile($fileName); return true; } diff --git a/src/Transformers/Response.php b/src/Transformers/Response.php index 5efd0239..416a26fa 100644 --- a/src/Transformers/Response.php +++ b/src/Transformers/Response.php @@ -10,6 +10,8 @@ class Response { + const CHUNK_SIZE = 8192; + /** * @var \Swoole\Http\Response */ @@ -25,7 +27,8 @@ class Response * * @param $illuminateResponse * @param \Swoole\Http\Response $swooleResponse - * @return \SwooleTW\Http\Server\Response + * + * @return \SwooleTW\Http\Transformers\Response */ public static function make($illuminateResponse, SwooleResponse $swooleResponse) { @@ -45,7 +48,7 @@ public function __construct($illuminateResponse, SwooleResponse $swooleResponse) } /** - * Sends HTTP headers and content. + * Send HTTP headers and content. * * @throws \InvalidArgumentException */ @@ -56,7 +59,7 @@ public function send() } /** - * Sends HTTP headers. + * Send HTTP headers. * * @throws \InvalidArgumentException */ @@ -85,38 +88,62 @@ protected function sendHeaders() $this->swooleResponse->status($illuminateResponse->getStatusCode()); // cookies + // $cookie->isRaw() is supported after symfony/http-foundation 3.1 + // and Laravel 5.3, so we can add it back now foreach ($illuminateResponse->headers->getCookies() as $cookie) { - // may need to consider rawcookie - $this->swooleResponse->cookie( - $cookie->getName(), $cookie->getValue(), - $cookie->getExpiresTime(), $cookie->getPath(), - $cookie->getDomain(), $cookie->isSecure(), + $method = $cookie->isRaw() ? 'rawcookie' : 'cookie'; + $this->swooleResponse->$method( + $cookie->getName(), + $cookie->getValue(), + $cookie->getExpiresTime(), + $cookie->getPath(), + $cookie->getDomain(), + $cookie->isSecure(), $cookie->isHttpOnly() ); } } /** - * Sends HTTP content. + * Send HTTP content. */ protected function sendContent() { $illuminateResponse = $this->getIlluminateResponse(); - if ($illuminateResponse instanceof StreamedResponse && - property_exists($illuminateResponse, 'output') - ) { + if ($illuminateResponse instanceof StreamedResponse && property_exists($illuminateResponse, 'output')) { + // TODO Add Streamed Response with output $this->swooleResponse->end($illuminateResponse->output); } elseif ($illuminateResponse instanceof BinaryFileResponse) { $this->swooleResponse->sendfile($illuminateResponse->getFile()->getPathname()); } else { - $this->swooleResponse->end($illuminateResponse->getContent()); + $this->sendInChunk($illuminateResponse->getContent()); + } + } + + /** + * Send content in chunk + * + * @param string $content + */ + protected function sendInChunk($content) + { + if (strlen($content) <= static::CHUNK_SIZE) { + $this->swooleResponse->end($content); + return; + } + + foreach (str_split($content, static::CHUNK_SIZE) as $chunk) { + $this->swooleResponse->write($chunk); } + + $this->swooleResponse->end(); } /** * @param \Swoole\Http\Response $swooleResponse - * @return \SwooleTW\Http\Server\Response + * + * @return \SwooleTW\Http\Transformers\Response */ protected function setSwooleResponse(SwooleResponse $swooleResponse) { @@ -135,7 +162,8 @@ public function getSwooleResponse() /** * @param mixed illuminateResponse - * @return \SwooleTW\Http\Server\Response + * + * @return \SwooleTW\Http\Transformers\Response */ protected function setIlluminateResponse($illuminateResponse) { diff --git a/src/Transformers/StreamedResponse.php b/src/Transformers/StreamedResponse.php new file mode 100644 index 00000000..31a5e59b --- /dev/null +++ b/src/Transformers/StreamedResponse.php @@ -0,0 +1,38 @@ +output; + } + + /** + * Set output buffer + * + * @param string $output + */ + public function setOutput(?string $output = null): void + { + $this->output = $output; + } +} \ No newline at end of file diff --git a/src/Websocket/Authenticatable.php b/src/Websocket/Authenticatable.php index ae9d1c3d..92276511 100644 --- a/src/Websocket/Authenticatable.php +++ b/src/Websocket/Authenticatable.php @@ -2,15 +2,24 @@ namespace SwooleTW\Http\Websocket; -use InvalidArgumentException; use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract; +use InvalidArgumentException; +/** + * Trait Authenticatable + * + * @property-read \SwooleTW\Http\Websocket\Rooms\RoomContract $room + */ trait Authenticatable { protected $userId; /** * Login using current user. + * + * @param \Illuminate\Contracts\Auth\Authenticatable $user + * + * @return mixed */ public function loginUsing(AuthenticatableContract $user) { @@ -19,6 +28,10 @@ public function loginUsing(AuthenticatableContract $user) /** * Login using current userId. + * + * @param $userId + * + * @return mixed */ public function loginUsingId($userId) { @@ -27,11 +40,13 @@ public function loginUsingId($userId) /** * Logout with current sender's fd. + * + * @return mixed */ public function logout() { if (is_null($userId = $this->getUserId())) { - return; + return null; } return $this->leave(static::USER_PREFIX . $userId); @@ -39,13 +54,18 @@ public function logout() /** * Set multiple recepients' fds by users. + * + * @param $users + * + * @return \SwooleTW\Http\Websocket\Authenticatable */ public function toUser($users) { $users = is_object($users) ? func_get_args() : $users; - $userIds = array_map(function ($user) { + $userIds = array_map(function (AuthenticatableContract $user) { $this->checkUser($user); + return $user->getAuthIdentifier(); }, $users); @@ -54,6 +74,10 @@ public function toUser($users) /** * Set multiple recepients' fds by userIds. + * + * @param $userIds + * + * @return \SwooleTW\Http\Websocket\Authenticatable */ public function toUserId($userIds) { @@ -89,6 +113,10 @@ public function getUserId() /** * Check if a user is online by given userId. + * + * @param $userId + * + * @return bool */ public function isUserIdOnline($userId) { @@ -97,6 +125,8 @@ public function isUserIdOnline($userId) /** * Check if user object implements AuthenticatableContract. + * + * @param $user */ protected function checkUser($user) { diff --git a/src/Websocket/Facades/Room.php b/src/Websocket/Facades/Room.php index e0a26fd4..5050e4f6 100644 --- a/src/Websocket/Facades/Room.php +++ b/src/Websocket/Facades/Room.php @@ -4,6 +4,15 @@ use Illuminate\Support\Facades\Facade; +/** + * @method static $this prepare() + * @method static $this add($fd, $rooms) + * @method static $this delete($fd, $rooms) + * @method static array getClients($room) + * @method static array getRooms($fd) + * + * @see \SwooleTW\Http\Websocket\Rooms\RoomContract + */ class Room extends Facade { /** @@ -15,4 +24,4 @@ protected static function getFacadeAccessor() { return 'swoole.room'; } -} \ No newline at end of file +} diff --git a/src/Websocket/Facades/Websocket.php b/src/Websocket/Facades/Websocket.php index 8c24866b..6fedbcb4 100644 --- a/src/Websocket/Facades/Websocket.php +++ b/src/Websocket/Facades/Websocket.php @@ -4,6 +4,36 @@ use Illuminate\Support\Facades\Facade; +/** + * @method static $this broadcast() + * @method static $this to($values) + * @method static $this join($rooms) + * @method static $this leave($rooms) + * @method static boolean emit($event, $data) + * @method static $this in($room) + * @method static $this on($event, $callback) + * @method static boolean eventExists($event) + * @method static mixed call($event, $data) + * @method static boolean close($fd) + * @method static $this setSender($fd) + * @method static int getSender() + * @method static boolean getIsBroadcast() + * @method static array getTo() + * @method static $this reset() + * @method static $this middleware($middleware) + * @method static $this setContainer($container) + * @method static $this setPipeline($pipeline) + * @method static \Illuminate\Contracts\Pipeline\Pipeline getPipeline() + * @method static mixed loginUsing($user) + * @method static $this loginUsingId($userId) + * @method static $this logout() + * @method static $this toUser($users) + * @method static $this toUserId($userIds) + * @method static string getUserId() + * @method static boolean isUserIdOnline($userId) + * + * @see \SwooleTW\Http\Websocket\Websocket + */ class Websocket extends Facade { /** @@ -15,4 +45,4 @@ protected static function getFacadeAccessor() { return 'swoole.websocket'; } -} \ No newline at end of file +} diff --git a/src/Websocket/HandShakeHandler.php b/src/Websocket/HandShakeHandler.php new file mode 100644 index 00000000..291fe17a --- /dev/null +++ b/src/Websocket/HandShakeHandler.php @@ -0,0 +1,48 @@ +header['sec-websocket-key']; + + if (0 === preg_match('#^[+/0-9A-Za-z]{21}[AQgw]==$#', $socketkey) || 16 !== strlen(base64_decode($socketkey))) { + $response->end(); + + return false; + } + + $headers = [ + 'Upgrade' => 'websocket', + 'Connection' => 'Upgrade', + 'Sec-WebSocket-Accept' => base64_encode(sha1($socketkey . '258EAFA5-E914-47DA-95CA-C5AB0DC85B11', true)), + 'Sec-WebSocket-Version' => '13', + ]; + + if (isset($request->header['sec-websocket-protocol'])) { + $headers['Sec-WebSocket-Protocol'] = $request->header['sec-websocket-protocol']; + } + + foreach ($headers as $header => $val) { + $response->header($header, $val); + } + + $response->status(101); + $response->end(); + + return true; + } +} diff --git a/src/Websocket/Middleware/Authenticate.php b/src/Websocket/Middleware/Authenticate.php index 27315d7b..caf1c675 100644 --- a/src/Websocket/Middleware/Authenticate.php +++ b/src/Websocket/Middleware/Authenticate.php @@ -6,19 +6,23 @@ use Illuminate\Auth\AuthenticationException; use Illuminate\Contracts\Auth\Factory as Auth; +/** + * Class Authenticate + */ class Authenticate { /** * The authentication factory instance. * - * @var \Illuminate\Contracts\Auth\Factory + * @var \Illuminate\Contracts\Auth\Factory|mixed */ protected $auth; /** * Create a new middleware instance. * - * @param \Illuminate\Contracts\Auth\Factory $auth + * @param \Illuminate\Contracts\Auth\Factory $auth + * * @return void */ public function __construct(Auth $auth) @@ -29,15 +33,16 @@ public function __construct(Auth $auth) /** * Handle an incoming request. * - * @param \Illuminate\Http\Request $request - * @param \Closure $next + * @param \Illuminate\Http\Request $request + * @param \Closure $next + * * @return mixed * - * @throws \Illuminate\Auth\AuthenticationException */ public function handle($request, Closure $next) { try { + $this->auth->setRequest($request); if ($user = $this->auth->authenticate()) { $request->setUserResolver(function () use ($user) { return $user; diff --git a/src/Websocket/Middleware/DecryptCookies.php b/src/Websocket/Middleware/DecryptCookies.php index 6a76c65e..2807ea1b 100644 --- a/src/Websocket/Middleware/DecryptCookies.php +++ b/src/Websocket/Middleware/DecryptCookies.php @@ -3,11 +3,13 @@ namespace SwooleTW\Http\Websocket\Middleware; use Closure; -use Symfony\Component\HttpFoundation\Cookie; -use Symfony\Component\HttpFoundation\Request; use Illuminate\Contracts\Encryption\DecryptException; use Illuminate\Contracts\Encryption\Encrypter as EncrypterContract; +use Symfony\Component\HttpFoundation\Request; +/** + * Class DecryptCookies + */ class DecryptCookies { /** @@ -35,7 +37,8 @@ class DecryptCookies /** * Create a new CookieGuard instance. * - * @param \Illuminate\Contracts\Encryption\Encrypter $encrypter + * @param \Illuminate\Contracts\Encryption\Encrypter $encrypter + * * @return void */ public function __construct(EncrypterContract $encrypter) @@ -46,7 +49,8 @@ public function __construct(EncrypterContract $encrypter) /** * Disable encryption for the given cookie name(s). * - * @param string|array $name + * @param string|array $name + * * @return void */ public function disableFor($name) @@ -57,8 +61,9 @@ public function disableFor($name) /** * Handle an incoming request. * - * @param \Illuminate\Http\Request $request - * @param \Closure $next + * @param \Illuminate\Http\Request $request + * @param \Closure $next + * * @return \Symfony\Component\HttpFoundation\Response */ public function handle($request, Closure $next) @@ -69,7 +74,8 @@ public function handle($request, Closure $next) /** * Decrypt the cookies on the request. * - * @param \Symfony\Component\HttpFoundation\Request $request + * @param \Symfony\Component\HttpFoundation\Request $request + * * @return \Symfony\Component\HttpFoundation\Request */ protected function decrypt(Request $request) @@ -92,21 +98,23 @@ protected function decrypt(Request $request) /** * Decrypt the given cookie and return the value. * - * @param string $name - * @param string|array $cookie + * @param string $name + * @param string|array $cookie + * * @return string|array */ protected function decryptCookie($name, $cookie) { return is_array($cookie) - ? $this->decryptArray($cookie) - : $this->encrypter->decrypt($cookie, static::serialized($name)); + ? $this->decryptArray($cookie) + : $this->encrypter->decrypt($cookie, static::serialized($name)); } /** * Decrypt an array based cookie. * - * @param array $cookie + * @param array $cookie + * * @return array */ protected function decryptArray(array $cookie) @@ -114,7 +122,7 @@ protected function decryptArray(array $cookie) $decrypted = []; foreach ($cookie as $key => $value) { - if (is_string($value)) { + if (\is_string($value)) { $decrypted[$key] = $this->encrypter->decrypt($value, static::serialized($key)); } } @@ -126,20 +134,22 @@ protected function decryptArray(array $cookie) * Determine whether encryption has been disabled for the given cookie. * * @param string $name + * * @return bool */ - public function isDisabled($name) + public function isDisabled(string $name) { - return in_array($name, $this->except); + return \in_array($name, $this->except); } /** * Determine if the cookie contents should be serialized. * - * @param string $name + * @param string $name + * * @return bool */ - public static function serialized($name) + public static function serialized(string $name) { return static::$serialize; } diff --git a/src/Websocket/Middleware/StartSession.php b/src/Websocket/Middleware/StartSession.php index bb5de3eb..6be32c6d 100644 --- a/src/Websocket/Middleware/StartSession.php +++ b/src/Websocket/Middleware/StartSession.php @@ -3,10 +3,14 @@ namespace SwooleTW\Http\Websocket\Middleware; use Closure; +use Illuminate\Contracts\Session\Session; use Illuminate\Http\Request; use Illuminate\Session\SessionManager; -use Illuminate\Contracts\Session\Session; +use Illuminate\Support\Arr; +/** + * Class StartSession + */ class StartSession { /** @@ -19,7 +23,8 @@ class StartSession /** * Create a new session middleware. * - * @param \Illuminate\Session\SessionManager $manager + * @param \Illuminate\Session\SessionManager $manager + * * @return void */ public function __construct(SessionManager $manager) @@ -30,16 +35,15 @@ public function __construct(SessionManager $manager) /** * Handle an incoming request. * - * @param \Illuminate\Http\Request $request - * @param \Closure $next + * @param \Illuminate\Http\Request $request + * @param \Closure $next + * * @return mixed */ public function handle($request, Closure $next) { if ($this->sessionConfigured()) { - $request->setLaravelSession( - $session = $this->startSession($request) - ); + $request->setLaravelSession($this->startSession($request)); } return $next($request); @@ -48,12 +52,13 @@ public function handle($request, Closure $next) /** * Start the session for the given request. * - * @param \Illuminate\Http\Request $request + * @param \Illuminate\Http\Request $request + * * @return \Illuminate\Contracts\Session\Session */ protected function startSession(Request $request) { - return tap($this->getSession($request), function ($session) use ($request) { + return tap($this->getSession($request), function (Session $session) use ($request) { $session->setRequestOnHandler($request); $session->start(); @@ -63,12 +68,13 @@ protected function startSession(Request $request) /** * Get the session implementation from the manager. * - * @param \Illuminate\Http\Request $request + * @param \Illuminate\Http\Request $request + * * @return \Illuminate\Contracts\Session\Session */ public function getSession(Request $request) { - return tap($this->manager->driver(), function ($session) use ($request) { + return tap($this->manager->driver(), function (Session $session) use ($request) { $session->setId($request->cookies->get($session->getName())); }); } diff --git a/src/Websocket/Parser.php b/src/Websocket/Parser.php index c8a22543..b61c93c6 100644 --- a/src/Websocket/Parser.php +++ b/src/Websocket/Parser.php @@ -15,6 +15,9 @@ abstract class Parser * Execute strategies before decoding payload. * If return value is true will skip decoding. * + * @param \Swoole\WebSocket\Server $server + * @param \Swoole\WebSocket\Frame $frame + * * @return boolean */ public function execute($server, $frame) @@ -22,10 +25,13 @@ public function execute($server, $frame) $skip = false; foreach ($this->strategies as $strategy) { - $result = App::call($strategy . '@handle', [ - 'server' => $server, - 'frame' => $frame - ]); + $result = App::call( + $strategy . '@handle', + [ + 'server' => $server, + 'frame' => $frame, + ] + ); if ($result === true) { $skip = true; break; @@ -40,6 +46,7 @@ public function execute($server, $frame) * * @param string $event * @param mixed $data + * * @return mixed */ abstract public function encode(string $event, $data); @@ -49,6 +56,7 @@ abstract public function encode(string $event, $data); * Define and return event name and payload data here. * * @param \Swoole\Websocket\Frame $frame + * * @return array */ abstract public function decode($frame); diff --git a/src/Websocket/Pusher.php b/src/Websocket/Pusher.php new file mode 100644 index 00000000..32b8e91f --- /dev/null +++ b/src/Websocket/Pusher.php @@ -0,0 +1,269 @@ +opcode = $opcode; + $this->sender = $sender; + $this->descriptors = $descriptors; + $this->broadcast = $broadcast; + $this->assigned = $assigned; + $this->event = $event; + $this->message = $message; + $this->server = $server; + } + + /** + * Static constructor + * + * @param array $data + * @param \Swoole\Websocket\Server $server + * + * @return \SwooleTW\Http\Websocket\Pusher + */ + public static function make(array $data, $server) + { + return new static( + $data['opcode'] ?? 1, + $data['sender'] ?? 0, + $data['fds'] ?? [], + $data['broadcast'] ?? false, + $data['assigned'] ?? false, + $data['event'] ?? null, + $data['message'] ?? null, + $server + ); + } + + /** + * @return int + */ + public function getOpcode(): int + { + return $this->opcode; + } + + /** + * @return int + */ + public function getSender(): int + { + return $this->sender; + } + + /** + * @return array + */ + public function getDescriptors(): array + { + return $this->descriptors; + } + + /** + * @param int $descriptor + * + * @return self + */ + public function addDescriptor($descriptor): self + { + return $this->addDescriptors([$descriptor]); + } + + /** + * @param array $descriptors + * + * @return self + */ + public function addDescriptors(array $descriptors): self + { + $this->descriptors = array_values( + array_unique( + array_merge($this->descriptors, $descriptors) + ) + ); + + return $this; + } + + /** + * @param int $descriptor + * + * @return bool + */ + public function hasDescriptor(int $descriptor): bool + { + return in_array($descriptor, $this->descriptors); + } + + /** + * @return bool + */ + public function isBroadcast(): bool + { + return $this->broadcast; + } + + /** + * @return bool + */ + public function isAssigned(): bool + { + return $this->assigned; + } + + /** + * @return string + */ + public function getEvent(): string + { + return $this->event; + } + + /** + * @return mixed|null + */ + public function getMessage() + { + return $this->message; + } + + /** + * @return \Swoole\Websocket\Server + */ + public function getServer() + { + return $this->server; + } + + /** + * @return bool + */ + public function shouldBroadcast(): bool + { + return $this->broadcast && empty($this->descriptors) && ! $this->assigned; + } + + /** + * Returns all descriptors that are websocket + * + * @param \Swoole\Connection\Iterator $descriptors + * + * @return array + */ + protected function getWebsocketConnections(): array + { + return array_filter(iterator_to_array($this->server->connections), function ($fd) { + return $this->server->isEstablished($fd); + }); + } + + /** + * @param int $fd + * + * @return bool + */ + public function shouldPushToDescriptor(int $fd): bool + { + if (! $this->server->isEstablished($fd)) { + return false; + } + + return $this->broadcast ? $this->sender !== (int) $fd : true; + } + + /** + * Push message to related descriptors + * + * @param mixed $payload + * + * @return void + */ + public function push($payload): void + { + // attach sender if not broadcast + if (! $this->broadcast && $this->sender && ! $this->hasDescriptor($this->sender)) { + $this->addDescriptor($this->sender); + } + + // check if to broadcast to other clients + if ($this->shouldBroadcast()) { + $this->addDescriptors($this->getWebsocketConnections()); + } + + // push message to designated fds + foreach ($this->descriptors as $descriptor) { + if ($this->shouldPushToDescriptor($descriptor)) { + $this->server->push($descriptor, $payload, $this->opcode); + } + } + } +} diff --git a/src/Websocket/Rooms/RedisRoom.php b/src/Websocket/Rooms/RedisRoom.php index 410ac391..46ef7e9b 100644 --- a/src/Websocket/Rooms/RedisRoom.php +++ b/src/Websocket/Rooms/RedisRoom.php @@ -2,47 +2,74 @@ namespace SwooleTW\Http\Websocket\Rooms; +use Illuminate\Support\Arr; use Predis\Client as RedisClient; -use SwooleTW\Http\Websocket\Rooms\RoomContract; +use Predis\Pipeline\Pipeline; +/** + * Class RedisRoom + */ class RedisRoom implements RoomContract { + /** + * @var \Predis\Client + */ protected $redis; + /** + * @var array + */ protected $config; + /** + * @var string + */ protected $prefix = 'swoole:'; + /** + * RedisRoom constructor. + * + * @param array $config + */ public function __construct(array $config) { $this->config = $config; } - public function prepare(RedisClient $redis = null) + /** + * @param \Predis\Client|null $redis + * + * @return \SwooleTW\Http\Websocket\Rooms\RoomContract + */ + public function prepare(RedisClient $redis = null): RoomContract { $this->setRedis($redis); $this->setPrefix(); $this->cleanRooms(); + + return $this; } /** * Set redis client. + * + * @param \Predis\Client|null $redis */ - public function setRedis(RedisClient $redis = null) + public function setRedis(?RedisClient $redis = null) { - $server = $this->config['server'] ?? []; - $options = $this->config['options'] ?? []; + if (! $redis) { + $server = Arr::get($this->config, 'server', []); + $options = Arr::get($this->config, 'options', []); - // forbid setting prefix from options - if (array_key_exists('prefix', $options)) { - unset($options['prefix']); - } + // forbid setting prefix from options + if (Arr::has($options, 'prefix')) { + $options = Arr::except($options, 'prefix'); + } - if ($redis) { - $this->redis = $redis; - } else { - $this->redis = new RedisClient($server, $options); + $redis = new RedisClient($server, $options); } + + $this->redis = $redis; } /** @@ -50,8 +77,8 @@ public function setRedis(RedisClient $redis = null) */ protected function setPrefix() { - if (array_key_exists('prefix', $this->config)) { - $this->prefix = $this->config['prefix']; + if ($prefix = Arr::get($this->config, 'prefix')) { + $this->prefix = $prefix; } } @@ -63,35 +90,56 @@ public function getRedis() return $this->redis; } - public function add(int $fd, $roomNames) + /** + * Add multiple socket fds to a room. + * + * @param int fd + * @param array|string rooms + */ + public function add(int $fd, $rooms) { - $roomNames = is_array($roomNames) ? $roomNames : [$roomNames]; + $rooms = is_array($rooms) ? $rooms : [$rooms]; - $this->addValue($fd, $roomNames, 'fds'); + $this->addValue($fd, $rooms, RoomContract::DESCRIPTORS_KEY); - foreach ($roomNames as $room) { - $this->addValue($room, [$fd], 'rooms'); + foreach ($rooms as $room) { + $this->addValue($room, [$fd], RoomContract::ROOMS_KEY); } } - public function delete(int $fd, $roomNames = []) + /** + * Delete multiple socket fds from a room. + * + * @param int fd + * @param array|string rooms + */ + public function delete(int $fd, $rooms) { - $roomNames = is_array($roomNames) ? $roomNames : [$roomNames]; - $roomNames = count($roomNames) ? $roomNames : $this->getRooms($fd); + $rooms = is_array($rooms) ? $rooms : [$rooms]; + $rooms = count($rooms) ? $rooms : $this->getRooms($fd); - $this->removeValue($fd, $roomNames, 'fds'); + $this->removeValue($fd, $rooms, RoomContract::DESCRIPTORS_KEY); - foreach ($roomNames as $room) { - $this->removeValue($room, [$fd], 'rooms'); + foreach ($rooms as $room) { + $this->removeValue($room, [$fd], RoomContract::ROOMS_KEY); } } + /** + * Add value to redis. + * + * @param $key + * @param array $values + * @param string $table + * + * @return $this + */ public function addValue($key, array $values, string $table) { $this->checkTable($table); $redisKey = $this->getKey($key, $table); - $this->redis->pipeline(function ($pipe) use ($redisKey, $values) { + $this->redis->pipeline(function (Pipeline $pipe) use ($redisKey, $values) { foreach ($values as $value) { $pipe->sadd($redisKey, $value); } @@ -100,12 +148,21 @@ public function addValue($key, array $values, string $table) return $this; } + /** + * Remove value from redis. + * + * @param $key + * @param array $values + * @param string $table + * + * @return $this + */ public function removeValue($key, array $values, string $table) { $this->checkTable($table); $redisKey = $this->getKey($key, $table); - $this->redis->pipeline(function ($pipe) use ($redisKey, $values) { + $this->redis->pipeline(function (Pipeline $pipe) use ($redisKey, $values) { foreach ($values as $value) { $pipe->srem($redisKey, $value); } @@ -114,39 +171,79 @@ public function removeValue($key, array $values, string $table) return $this; } + /** + * Get all sockets by a room key. + * + * @param string room + * + * @return array + */ public function getClients(string $room) { - return $this->getValue($room, 'rooms'); + return $this->getValue($room, RoomContract::ROOMS_KEY) ?? []; } + /** + * Get all rooms by a fd. + * + * @param int fd + * + * @return array + */ public function getRooms(int $fd) { - return $this->getValue($fd, 'fds'); + return $this->getValue($fd, RoomContract::DESCRIPTORS_KEY) ?? []; } + /** + * Check table for rooms and descriptors. + * + * @param string $table + */ protected function checkTable(string $table) { - if (! in_array($table, ['rooms', 'fds'])) { - throw new \InvalidArgumentException('Invalid table name.'); + if (! in_array($table, [RoomContract::ROOMS_KEY, RoomContract::DESCRIPTORS_KEY])) { + throw new \InvalidArgumentException("Invalid table name: `{$table}`."); } } + /** + * Get value. + * + * @param string $key + * @param string $table + * + * @return array + */ public function getValue(string $key, string $table) { $this->checkTable($table); - return $this->redis->smembers($this->getKey($key, $table)); + $result = $this->redis->smembers($this->getKey($key, $table)); + + // Try to fix occasional non-array returned result + return is_array($result) ? $result : []; } + /** + * Get key. + * + * @param string $key + * @param string $table + * + * @return string + */ public function getKey(string $key, string $table) { return "{$this->prefix}{$table}:{$key}"; } - protected function cleanRooms() + /** + * Clean all rooms. + */ + protected function cleanRooms(): void { - $keys = $this->redis->keys("{$this->prefix}*"); - if (count($keys)) { + if (count($keys = $this->redis->keys("{$this->prefix}*"))) { $this->redis->del($keys); } } diff --git a/src/Websocket/Rooms/RoomContract.php b/src/Websocket/Rooms/RoomContract.php index 8a1252a7..28aaefee 100644 --- a/src/Websocket/Rooms/RoomContract.php +++ b/src/Websocket/Rooms/RoomContract.php @@ -4,38 +4,58 @@ interface RoomContract { + /** + * Rooms key + * + * @const string + */ + public const ROOMS_KEY = 'rooms'; + + /** + * Descriptors key + * + * @const string + */ + public const DESCRIPTORS_KEY = 'fds'; + /** * Do some init stuffs before workers started. + * + * @return \SwooleTW\Http\Websocket\Rooms\RoomContract */ - public function prepare(); + public function prepare(): RoomContract; /** * Add multiple socket fds to a room. * - * @int fd - * @string|array rooms + * @param int fd + * @param array|string rooms */ public function add(int $fd, $rooms); /** * Delete multiple socket fds from a room. * - * @int fd - * @string|array rooms + * @param int fd + * @param array|string rooms */ public function delete(int $fd, $rooms); /** * Get all sockets by a room key. * - * @string room + * @param string room + * + * @return array */ public function getClients(string $room); /** * Get all rooms by a fd. * - * @int fd + * @param int fd + * + * @return array */ public function getRooms(int $fd); } diff --git a/src/Websocket/Rooms/TableRoom.php b/src/Websocket/Rooms/TableRoom.php index ad3e9146..ae9b0bfd 100644 --- a/src/Websocket/Rooms/TableRoom.php +++ b/src/Websocket/Rooms/TableRoom.php @@ -3,27 +3,53 @@ namespace SwooleTW\Http\Websocket\Rooms; use Swoole\Table; -use SwooleTW\Http\Websocket\Rooms\RoomContract; class TableRoom implements RoomContract { + /** + * @var array + */ protected $config; + /** + * @var \Swoole\Table + */ protected $rooms; + /** + * @var \Swoole\Table + */ protected $fds; + /** + * TableRoom constructor. + * + * @param array $config + */ public function __construct(array $config) { $this->config = $config; } - public function prepare() + /** + * Do some init stuffs before workers started. + * + * @return \SwooleTW\Http\Websocket\Rooms\RoomContract + */ + public function prepare(): RoomContract { $this->initRoomsTable(); $this->initFdsTable(); + + return $this; } + /** + * Add a socket fd to multiple rooms. + * + * @param int fd + * @param array|string rooms + */ public function add(int $fd, $roomNames) { $rooms = $this->getRooms($fd); @@ -45,6 +71,12 @@ public function add(int $fd, $roomNames) $this->setRooms($fd, $rooms); } + /** + * Delete a socket fd from multiple rooms. + * + * @param int fd + * @param array|string rooms + */ public function delete(int $fd, $roomNames = []) { $allRooms = $this->getRooms($fd); @@ -59,40 +91,72 @@ public function delete(int $fd, $roomNames = []) continue; } - $this->setClients($room, array_values(array_diff($fds, [$fd])), 'rooms'); + $this->setClients($room, array_values(array_diff($fds, [$fd]))); $removeRooms[] = $room; } - $this->setRooms($fd, array_values(array_diff($allRooms, $removeRooms)), 'fds'); + $this->setRooms($fd, array_values(array_diff($allRooms, $removeRooms))); } + /** + * Get all sockets by a room key. + * + * @param string room + * + * @return array + */ public function getClients(string $room) { - return $this->getValue($room, 'rooms'); + return $this->getValue($room, RoomContract::ROOMS_KEY) ?? []; } + /** + * Get all rooms by a fd. + * + * @param int fd + * + * @return array + */ public function getRooms(int $fd) { - return $this->getValue($fd, 'fds'); + return $this->getValue($fd, RoomContract::DESCRIPTORS_KEY) ?? []; } - protected function setClients(string $room, array $fds) + /** + * @param string $room + * @param array $fds + * + * @return \SwooleTW\Http\Websocket\Rooms\TableRoom + */ + protected function setClients(string $room, array $fds): TableRoom { - return $this->setValue($room, $fds, 'rooms'); + return $this->setValue($room, $fds, RoomContract::ROOMS_KEY); } - protected function setRooms(int $fd, array $rooms) + /** + * @param int $fd + * @param array $rooms + * + * @return \SwooleTW\Http\Websocket\Rooms\TableRoom + */ + protected function setRooms(int $fd, array $rooms): TableRoom { - return $this->setValue($fd, $rooms, 'fds'); + return $this->setValue($fd, $rooms, RoomContract::DESCRIPTORS_KEY); } - protected function initRoomsTable() + /** + * Init rooms table + */ + protected function initRoomsTable(): void { $this->rooms = new Table($this->config['room_rows']); $this->rooms->column('value', Table::TYPE_STRING, $this->config['room_size']); $this->rooms->create(); } + /** + * Init descriptors table + */ protected function initFdsTable() { $this->fds = new Table($this->config['client_rows']); @@ -100,17 +164,32 @@ protected function initFdsTable() $this->fds->create(); } + /** + * Set value to table + * + * @param $key + * @param array $value + * @param string $table + * + * @return $this + */ public function setValue($key, array $value, string $table) { $this->checkTable($table); - $this->$table->set($key, [ - 'value' => json_encode($value) - ]); + $this->$table->set($key, ['value' => json_encode($value)]); return $this; } + /** + * Get value from table + * + * @param string $key + * @param string $table + * + * @return array|mixed + */ public function getValue(string $key, string $table) { $this->checkTable($table); @@ -120,10 +199,15 @@ public function getValue(string $key, string $table) return $value ? json_decode($value['value'], true) : []; } + /** + * Check table for exists + * + * @param string $table + */ protected function checkTable(string $table) { if (! property_exists($this, $table) || ! $this->$table instanceof Table) { - throw new \InvalidArgumentException('Invalid table name.'); + throw new \InvalidArgumentException("Invalid table name: `{$table}`."); } } } diff --git a/src/Websocket/SimpleParser.php b/src/Websocket/SimpleParser.php index a052dafc..fd3758ae 100644 --- a/src/Websocket/SimpleParser.php +++ b/src/Websocket/SimpleParser.php @@ -2,9 +2,6 @@ namespace SwooleTW\Http\Websocket; -use Swoole\Websocket\Server; -use SwooleTW\Http\Websocket\Parser; - class SimpleParser extends Parser { /** @@ -17,14 +14,17 @@ class SimpleParser extends Parser * * @param string $event * @param mixed $data + * * @return mixed */ public function encode(string $event, $data) { - return json_encode([ - 'event' => $event, - 'data' => $data - ]); + return json_encode( + [ + 'event' => $event, + 'data' => $data, + ] + ); } /** @@ -32,6 +32,7 @@ public function encode(string $event, $data) * Define and return event name and payload data here. * * @param \Swoole\Websocket\Frame $frame + * * @return array */ public function decode($frame) @@ -40,7 +41,7 @@ public function decode($frame) return [ 'event' => $data['event'] ?? null, - 'data' => $data['data'] ?? null + 'data' => $data['data'] ?? null, ]; } } diff --git a/src/Websocket/SocketIO/Packet.php b/src/Websocket/SocketIO/Packet.php index 07a4188a..f1f34ae4 100644 --- a/src/Websocket/SocketIO/Packet.php +++ b/src/Websocket/SocketIO/Packet.php @@ -2,6 +2,9 @@ namespace SwooleTW\Http\Websocket\SocketIO; +/** + * Class Packet + */ class Packet { /** @@ -84,7 +87,7 @@ class Packet 3 => 'PONG', 4 => 'MESSAGE', 5 => 'UPGRADE', - 6 => 'NOOP' + 6 => 'NOOP', ]; /** @@ -97,11 +100,15 @@ class Packet 3 => 'ACK', 4 => 'ERROR', 5 => 'BINARY_EVENT', - 6 => 'BINARY_ACK' + 6 => 'BINARY_ACK', ]; /** * Get socket packet type of a raw payload. + * + * @param string $packet + * + * @return int|null */ public static function getSocketType(string $packet) { @@ -116,6 +123,10 @@ public static function getSocketType(string $packet) /** * Get data packet from a raw payload. + * + * @param string $packet + * + * @return array|null */ public static function getPayload(string $packet) { @@ -135,12 +146,17 @@ public static function getPayload(string $packet) return [ 'event' => $data[0], - 'data' => $data[1] ?? null + 'data' => $data[1] ?? null, ]; } /** * Return if a socket packet belongs to specific type. + * + * @param $packet + * @param string $typeName + * + * @return bool */ public static function isSocketType($packet, string $typeName) { diff --git a/src/Websocket/SocketIO/SocketIOParser.php b/src/Websocket/SocketIO/SocketIOParser.php index 8c3e50d6..53483fb4 100644 --- a/src/Websocket/SocketIO/SocketIOParser.php +++ b/src/Websocket/SocketIO/SocketIOParser.php @@ -3,16 +3,18 @@ namespace SwooleTW\Http\Websocket\SocketIO; use SwooleTW\Http\Websocket\Parser; -use SwooleTW\Http\Websocket\SocketIO\Packet; use SwooleTW\Http\Websocket\SocketIO\Strategies\HeartbeatStrategy; +/** + * Class SocketIOParser + */ class SocketIOParser extends Parser { /** * Strategy classes need to implement handle method. */ protected $strategies = [ - HeartbeatStrategy::class + HeartbeatStrategy::class, ]; /** @@ -20,6 +22,7 @@ class SocketIOParser extends Parser * * @param string $event * @param mixed $data + * * @return mixed */ public function encode(string $event, $data) @@ -37,6 +40,7 @@ public function encode(string $event, $data) * Define and return payload here. * * @param \Swoole\Websocket\Frame $frame + * * @return array */ public function decode($frame) @@ -45,7 +49,7 @@ public function decode($frame) return [ 'event' => $payload['event'] ?? null, - 'data' => $payload['data'] ?? null + 'data' => $payload['data'] ?? null, ]; } } diff --git a/src/Websocket/SocketIO/Strategies/HeartbeatStrategy.php b/src/Websocket/SocketIO/Strategies/HeartbeatStrategy.php index e1fe7c64..5d65bc09 100644 --- a/src/Websocket/SocketIO/Strategies/HeartbeatStrategy.php +++ b/src/Websocket/SocketIO/Strategies/HeartbeatStrategy.php @@ -9,6 +9,9 @@ class HeartbeatStrategy /** * If return value is true will skip decoding. * + * @param \Swoole\WebSocket\Server $server + * @param \Swoole\WebSocket\Frame $frame + * * @return boolean */ public function handle($server, $frame) diff --git a/src/Websocket/SocketIO/WebsocketHandler.php b/src/Websocket/SocketIO/WebsocketHandler.php index c78f6498..69319692 100644 --- a/src/Websocket/SocketIO/WebsocketHandler.php +++ b/src/Websocket/SocketIO/WebsocketHandler.php @@ -2,12 +2,12 @@ namespace SwooleTW\Http\Websocket\SocketIO; -use Swoole\Websocket\Frame; use Illuminate\Http\Request; use Illuminate\Support\Facades\App; use Illuminate\Support\Facades\Config; +use Swoole\Websocket\Frame; +use SwooleTW\Http\Server\Facades\Server; use SwooleTW\Http\Websocket\HandlerContract; -use SwooleTW\Http\Websocket\SocketIO\Packet; class WebsocketHandler implements HandlerContract { @@ -16,21 +16,25 @@ class WebsocketHandler implements HandlerContract * * @param int $fd * @param \Illuminate\Http\Request $request + * + * @return bool */ public function onOpen($fd, Request $request) { if (! $request->input('sid')) { - $payload = json_encode([ - 'sid' => base64_encode(uniqid()), - 'upgrades' => [], - 'pingInterval' => Config::get('swoole_websocket.ping_interval'), - 'pingTimeout' => Config::get('swoole_websocket.ping_timeout') - ]); + $payload = json_encode( + [ + 'sid' => base64_encode(uniqid()), + 'upgrades' => [], + 'pingInterval' => Config::get('swoole_websocket.ping_interval'), + 'pingTimeout' => Config::get('swoole_websocket.ping_timeout'), + ] + ); $initPayload = Packet::OPEN . $payload; $connectPayload = Packet::MESSAGE . Packet::CONNECT; - App::make('swoole.server')->push($fd, $initPayload); - App::make('swoole.server')->push($fd, $connectPayload); + App::make(Server::class)->push($fd, $initPayload); + App::make(Server::class)->push($fd, $connectPayload); return true; } diff --git a/src/Websocket/Websocket.php b/src/Websocket/Websocket.php index d0da2d59..41f05494 100644 --- a/src/Websocket/Websocket.php +++ b/src/Websocket/Websocket.php @@ -2,14 +2,18 @@ namespace SwooleTW\Http\Websocket; -use InvalidArgumentException; +use Illuminate\Contracts\Container\Container; +use Illuminate\Contracts\Pipeline\Pipeline as PipelineContract; use Illuminate\Support\Facades\App; use Illuminate\Support\Facades\Config; -use SwooleTW\Http\Websocket\Authenticatable; -use Illuminate\Contracts\Container\Container; +use InvalidArgumentException; +use SwooleTW\Http\Server\Facades\Server; +use SwooleTW\Http\Server\Manager; use SwooleTW\Http\Websocket\Rooms\RoomContract; -use Illuminate\Contracts\Pipeline\Pipeline as PipelineContract; +/** + * Class Websocket + */ class Websocket { use Authenticatable; @@ -56,22 +60,29 @@ class Websocket /** * Pipeline instance. * - * @var PipelineContract + * @var \Illuminate\Contracts\Pipeline\Pipeline */ protected $pipeline; /** * Room adapter. * - * @var RoomContract + * @var \SwooleTW\Http\Websocket\Rooms\RoomContract */ protected $room; + /** + * DI Container. + * + * @var \Illuminate\Contracts\Container\Container + */ + protected $container; + /** * Websocket constructor. * - * @var RoomContract $room - * @var PipelineContract $pipeline + * @param \SwooleTW\Http\Websocket\Rooms\RoomContract $room + * @param \Illuminate\Contracts\Pipeline\Pipeline $pipeline */ public function __construct(RoomContract $room, PipelineContract $pipeline) { @@ -83,7 +94,7 @@ public function __construct(RoomContract $room, PipelineContract $pipeline) /** * Set broadcast to true. */ - public function broadcast() + public function broadcast(): self { $this->isBroadcast = true; @@ -94,9 +105,10 @@ public function broadcast() * Set multiple recipients fd or room names. * * @param integer, string, array + * * @return $this */ - public function to($values) + public function to($values): self { $values = is_string($values) || is_integer($values) ? func_get_args() : $values; @@ -113,9 +125,10 @@ public function to($values) * Join sender to multiple rooms. * * @param string, array $rooms + * * @return $this */ - public function join($rooms) + public function join($rooms): self { $rooms = is_string($rooms) || is_integer($rooms) ? func_get_args() : $rooms; @@ -127,10 +140,11 @@ public function join($rooms) /** * Make sender leave multiple rooms. * - * @param string, array + * @param array $rooms + * * @return $this */ - public function leave($rooms = []) + public function leave($rooms = []): self { $rooms = is_string($rooms) || is_integer($rooms) ? func_get_args() : $rooms; @@ -144,9 +158,10 @@ public function leave($rooms = []) * * @param string * @param mixed + * * @return boolean */ - public function emit(string $event, $data) + public function emit(string $event, $data): bool { $fds = $this->getFds(); $assigned = ! empty($this->to); @@ -155,30 +170,40 @@ public function emit(string $event, $data) // that means trying to emit to a non-existing room // skip it directly instead of pushing to a task queue if (empty($fds) && $assigned) { + $this->reset(); return false; } - $result = App::make('swoole.server')->task([ - 'action' => static::PUSH_ACTION, - 'data' => [ - 'sender' => $this->sender, - 'fds' => $fds, - 'broadcast' => $this->isBroadcast, - 'assigned' => $assigned, - 'event' => $event, - 'message' => $data - ] - ]); + $payload = [ + 'sender' => $this->sender, + 'fds' => $fds, + 'broadcast' => $this->isBroadcast, + 'assigned' => $assigned, + 'event' => $event, + 'message' => $data, + ]; + + $result = true; + $server = App::make(Server::class); + if ($server->taskworker) { + App::make(Manager::class)->pushMessage($server, $payload); + } else { + $result = $server->task([ + 'action' => static::PUSH_ACTION, + 'data' => $payload + ]); + } $this->reset(); - return $result === false ? false : true; + return $result !== false; } /** * An alias of `join` function. * * @param string + * * @return $this */ public function in($room) @@ -193,6 +218,7 @@ public function in($room) * * @param string * @param callback + * * @return $this */ public function on(string $event, $callback) @@ -212,6 +238,7 @@ public function on(string $event, $callback) * Check if this event name exists. * * @param string + * * @return boolean */ public function eventExists(string $event) @@ -224,6 +251,7 @@ public function eventExists(string $event) * * @param string * @param mixed + * * @return mixed */ public function call(string $event, $data = null) @@ -243,7 +271,7 @@ public function call(string $event, $data = null) return App::call($this->callbacks[$event], [ 'websocket' => $this, - $dataKey => $data + $dataKey => $data, ]); } @@ -251,17 +279,19 @@ public function call(string $event, $data = null) * Close current connection. * * @param integer + * * @return boolean */ public function close(int $fd = null) { - return App::make('swoole.server')->close($fd ?: $this->sender); + return App::make(Server::class)->close($fd ?: $this->sender); } /** * Set sender fd. * * @param integer + * * @return $this */ public function setSender(int $fd) @@ -320,6 +350,10 @@ protected function getFds() /** * Reset some data status. + * + * @param bool $force + * + * @return $this */ public function reset($force = false) { @@ -336,6 +370,10 @@ public function reset($force = false) /** * Get or set middleware. + * + * @param array|string|null $middleware + * + * @return array|\SwooleTW\Http\Websocket\Websocket */ public function middleware($middleware = null) { @@ -362,6 +400,10 @@ protected function setDefaultMiddleware() /** * Set container to pipeline. + * + * @param \Illuminate\Contracts\Container\Container $container + * + * @return $this */ public function setContainer(Container $container) { @@ -373,10 +415,16 @@ public function setContainer(Container $container) $resetPipeline = $closure->bindTo($pipeline, $pipeline); $resetPipeline(); + + return $this; } /** * Set pipeline. + * + * @param \Illuminate\Contracts\Pipeline\Pipeline $pipeline + * + * @return $this */ public function setPipeline(PipelineContract $pipeline) { @@ -396,7 +444,8 @@ public function getPipeline() /** * Set the given request through the middleware. * - * @param \Illuminate\Http\Request $request + * @param \Illuminate\Http\Request $request + * * @return \Illuminate\Http\Request */ protected function setRequestThroughMiddleware($request) diff --git a/stubs/5.5/MySqlConnector.stub b/stubs/5.5/MySqlConnector.stub new file mode 100644 index 00000000..13315b10 --- /dev/null +++ b/stubs/5.5/MySqlConnector.stub @@ -0,0 +1,52 @@ +causedByLostConnection($e) || Str::contains($e->getMessage(), 'is closed')) { + return $this->createPdoConnection($dsn, $username, $password, $options); + } + + throw $e; + } +} diff --git a/stubs/5.6/MySqlConnector.stub b/stubs/5.6/MySqlConnector.stub new file mode 100644 index 00000000..2efa720a --- /dev/null +++ b/stubs/5.6/MySqlConnector.stub @@ -0,0 +1,52 @@ +causedByLostConnection($e) || Str::contains($e->getMessage(), 'is closed')) { + return $this->createPdoConnection($dsn, $username, $password, $options); + } + + throw $e; + } +} diff --git a/stubs/5.6/SwooleTaskQueue.stub b/stubs/5.6/SwooleTaskQueue.stub new file mode 100644 index 00000000..2e331e8d --- /dev/null +++ b/stubs/5.6/SwooleTaskQueue.stub @@ -0,0 +1,113 @@ +swoole = $swoole; + } + + /** + * Push a new job onto the queue. + * + * @param string|object $job + * @param mixed $data + * @param string $queue + * + * @return mixed + */ + public function push($job, $data = '', $queue = null) + { + return $this->pushRaw($this->createPayload($job, $data), $queue); + } + + /** + * Push a raw payload onto the queue. + * + * @param string $payload + * @param string $queue + * @param array $options + * + * @return mixed + */ + public function pushRaw($payload, $queue = null, array $options = []) + { + return $this->swoole->task($payload, ! is_numeric($queue) ? -1 : (int)$queue); + } + + /** + * Push a new job onto the queue after a delay. + * + * @param \DateTimeInterface|\DateInterval|int $delay + * @param string|object $job + * @param mixed $data + * @param string $queue + * + * @return mixed + */ + public function later($delay, $job, $data = '', $queue = null) + { + return Timer::after($this->secondsUntil($delay) * 1000, function () use ($job, $data, $queue) { + return $this->push($job, $data, $queue); + }); + } + + /** + * Create a typical, string based queue payload array. + * + * @param string $job + * @param mixed $data + * + * @throws \Exception + */ + protected function createStringPayload($job, $data) + { + throw new Exception('Unsupported empty data'); + } + + /** + * Get the size of the queue. + * + * @param string $queue + * + * @return int + */ + public function size($queue = null) + { + return -1; + } + + /** + * Pop the next job off of the queue. + * + * @param string $queue + * + * @return \Illuminate\Contracts\Queue\Job|null + */ + public function pop($queue = null) + { + return null; + } +} diff --git a/stubs/5.7/SwooleTaskQueue.stub b/stubs/5.7/SwooleTaskQueue.stub new file mode 100644 index 00000000..5eb0e73d --- /dev/null +++ b/stubs/5.7/SwooleTaskQueue.stub @@ -0,0 +1,114 @@ +swoole = $swoole; + } + + /** + * Push a new job onto the queue. + * + * @param string|object $job + * @param mixed $data + * @param string $queue + * + * @return mixed + */ + public function push($job, $data = '', $queue = null) + { + return $this->pushRaw($this->createPayload($job, $data), $queue); + } + + /** + * Push a raw payload onto the queue. + * + * @param string $payload + * @param string $queue + * @param array $options + * + * @return mixed + */ + public function pushRaw($payload, $queue = null, array $options = []) + { + return $this->swoole->task($payload, ! is_numeric($queue) ? -1 : (int)$queue); + } + + /** + * Push a new job onto the queue after a delay. + * + * @param \DateTimeInterface|\DateInterval|int $delay + * @param string|object $job + * @param mixed $data + * @param string $queue + * + * @return mixed + */ + public function later($delay, $job, $data = '', $queue = null) + { + return Timer::after($this->secondsUntil($delay) * 1000, function () use ($job, $data, $queue) { + return $this->push($job, $data, $queue); + }); + } + + /** + * Create a typical, string based queue payload array. + * + * @param string $job + * @param string $queue + * @param mixed $data + * + * @throws \Exception + */ + protected function createStringPayload($job, $queue, $data) + { + throw new Exception('Unsupported empty data'); + } + + /** + * Get the size of the queue. + * + * @param string $queue + * + * @return int + */ + public function size($queue = null) + { + return -1; + } + + /** + * Pop the next job off of the queue. + * + * @param string $queue + * + * @return \Illuminate\Contracts\Queue\Job|null + */ + public function pop($queue = null) + { + return null; + } +} diff --git a/tests/Coroutine/ConnectorFactoryTest.php b/tests/Coroutine/ConnectorFactoryTest.php new file mode 100644 index 00000000..27e70a4a --- /dev/null +++ b/tests/Coroutine/ConnectorFactoryTest.php @@ -0,0 +1,50 @@ +mockEnv('Laravel\Lumen'); + } + + public function testItHasNeededStubByVersion() + { + $version = FW::version(); + + $search = version_compare($version, '5.6', '>=') ? '5.6' : '5.5'; + $stub = ConnectorFactory::stub($version); + + $this->assertTrue(Str::contains($stub, $search)); + } + + public function testItCanCompareNeededStubByVersion() + { + $version = '5.5'; + $search = '5.6'; + + $stub = ConnectorFactory::stub($version); + + $this->assertNotTrue(Str::contains($stub, $search)); + } + + public function testItCanMakeNeededQueueByVersion() + { + $version = FW::version(); + $queue = ConnectorFactory::make($version); + + $this->assertInstanceOf(ConnectorFactory::CONNECTOR_CLASS, $queue); + } +} \ No newline at end of file diff --git a/tests/Coroutine/ContextTest.php b/tests/Coroutine/ContextTest.php index fb5c6c36..a9ef6800 100644 --- a/tests/Coroutine/ContextTest.php +++ b/tests/Coroutine/ContextTest.php @@ -2,11 +2,11 @@ namespace SwooleTW\Http\Tests\Coroutine; -use TypeError; -use Mockery as m; -use SwooleTW\Http\Tests\TestCase; use Illuminate\Container\Container; +use Mockery as m; use SwooleTW\Http\Coroutine\Context; +use SwooleTW\Http\Tests\TestCase; +use TypeError; class ContextTest extends TestCase { diff --git a/tests/Helpers/MimeTypeTest.php b/tests/Helpers/MimeTypeTest.php new file mode 100644 index 00000000..1dd9e7dd --- /dev/null +++ b/tests/Helpers/MimeTypeTest.php @@ -0,0 +1,33 @@ +assertEquals($mimetype, 'text/css'); + } + + public function testGetWithEmptyString() + { + $extension = ''; + $mimetype = MimeType::get($extension); + + $this->assertEquals($mimetype, 'application/octet-stream'); + } + + public function testFrom() + { + $filename = 'test.css?id=12d123fadf'; + $mimetype = MimeType::from($filename); + + $this->assertEquals($mimetype, 'text/css'); + } +} diff --git a/tests/HotReload/FSEventParserTest.php b/tests/HotReload/FSEventParserTest.php new file mode 100644 index 00000000..33deea92 --- /dev/null +++ b/tests/HotReload/FSEventParserTest.php @@ -0,0 +1,28 @@ +assertInstanceOf(FSEvent::class, $event); + + $this->assertTrue(array_diff($event->getTypes(), [FSEvent::Renamed, FSEvent::OwnerModified]) === []); + $this->assertTrue((new Carbon('Mon Dec 31 01:18:34 2018'))->eq($event->getWhen())); + $this->assertEquals('/Some/Path/To/File/File.php', $event->getPath()); + $this->assertTrue($event->isType(FSEvent::Renamed)); + $this->assertTrue($event->isType(FSEvent::OwnerModified)); + } +} \ No newline at end of file diff --git a/tests/HotReload/FSEventTest.php b/tests/HotReload/FSEventTest.php new file mode 100644 index 00000000..65fef901 --- /dev/null +++ b/tests/HotReload/FSEventTest.php @@ -0,0 +1,29 @@ +assertTrue(array_diff($event->getTypes(), [FSEvent::Renamed, FSEvent::OwnerModified]) === []); + $this->assertTrue((new Carbon('Mon Dec 31 01:18:34 2018'))->eq($event->getWhen())); + $this->assertEquals('/Some/Path/To/File/File.php', $event->getPath()); + $this->assertTrue($event->isType(FSEvent::Renamed)); + $this->assertTrue($event->isType(FSEvent::OwnerModified)); + } +} \ No newline at end of file diff --git a/tests/HotReload/FSOutputTest.php b/tests/HotReload/FSOutputTest.php new file mode 100644 index 00000000..f89cb4bc --- /dev/null +++ b/tests/HotReload/FSOutputTest.php @@ -0,0 +1,24 @@ +assertEquals( + 'File: /Some/Path/To/File/File.php OwnerModified, Renamed at 2018.12.31 01:18:34', + FSOutput::format($event) + ); + } +} \ No newline at end of file diff --git a/tests/HotReload/FSProcessTest.php b/tests/HotReload/FSProcessTest.php new file mode 100644 index 00000000..c4c7b858 --- /dev/null +++ b/tests/HotReload/FSProcessTest.php @@ -0,0 +1,45 @@ +assertInstanceOf(FSProcess::class, $process); + $this->assertInstanceOf(Process::class, $process->make()); + } + + public function testItCanCreateHotReloadProcessWithNeededConfiguration() + { + $process = new FSProcess('.php', true, __DIR__); + $ref = new \ReflectionClass($process); + $configure = $ref->getMethod('configure'); + $configure->setAccessible(true); + $configuration = $configure->invoke($process); + $sampleConfiguration = [ + 'fswatch', + '-rtx', + '-e', + '.*', + '-i', + "\\.php$", + __DIR__, + ]; + + $this->assertInstanceOf(FSProcess::class, $process); + $this->assertInstanceOf(Process::class, $process->make()); + $this->assertTrue( + array_diff($sampleConfiguration, $configuration) === [] + ); + } +} \ No newline at end of file diff --git a/tests/Server/ManagerTest.php b/tests/Server/ManagerTest.php index d0b80fc7..2f56f526 100644 --- a/tests/Server/ManagerTest.php +++ b/tests/Server/ManagerTest.php @@ -10,17 +10,21 @@ use SwooleTW\Http\Server\Sandbox; use SwooleTW\Http\Tests\TestCase; use Illuminate\Container\Container; +use SwooleTW\Http\Websocket\HandShakeHandler; use SwooleTW\Http\Websocket\Parser; +use SwooleTW\Http\Server\PidManager; use SwooleTW\Http\Table\SwooleTable; -use Swoole\Http\Server as HttpServer; +use Laravel\Lumen\Exceptions\Handler; use Illuminate\Support\Facades\Config; use SwooleTW\Http\Websocket\Websocket; +use SwooleTW\Http\Server\Facades\Server; use SwooleTW\Http\Websocket\HandlerContract; use SwooleTW\Http\Websocket\Rooms\TableRoom; use SwooleTW\Http\Websocket\Rooms\RoomContract; use Illuminate\Contracts\Debug\ExceptionHandler; use SwooleTW\Http\Websocket\SocketIO\SocketIOParser; use SwooleTW\Http\Websocket\SocketIO\WebsocketHandler; +use Illuminate\Contracts\Config\Repository as ConfigContract; use SwooleTW\Http\Websocket\Facades\Websocket as WebsocketFacade; class ManagerTest extends TestCase @@ -31,31 +35,33 @@ class ManagerTest extends TestCase 'table_name' => [ 'size' => 1024, 'columns' => [ - ['name' => 'column_name', 'type' => Table::TYPE_STRING, 'size' => 1024] - ] - ] + ['name' => 'column_name', 'type' => Table::TYPE_STRING, 'size' => 1024], + ], + ], ], 'swoole_http.providers' => [], 'swoole_http.resetters' => [], - 'swoole_http.pre_resolved' => ['foo'] + 'swoole_http.pre_resolved' => ['foo'], ]; protected $websocketConfig = [ 'swoole_http.websocket.enabled' => true, 'swoole_websocket.parser' => SocketIOParser::class, 'swoole_websocket.handler' => WebsocketHandler::class, + 'swoole_websocket.handshake.enabled' => true, + 'swoole_websocket.handshake.handler' => HandShakeHandler::class, 'swoole_websocket.default' => 'table', 'swoole_websocket.settings.table' => [ 'room_rows' => 10, 'room_size' => 10, 'client_rows' => 10, - 'client_size' => 10 + 'client_size' => 10, ], 'swoole_websocket.drivers.table' => TableRoom::class, 'swoole_http.tables' => [], 'swoole_http.providers' => [], 'swoole_http.resetters' => [], - 'swoole_http.server.public_path' => '/' + 'swoole_http.server.public_path' => '/', ]; public function testGetFramework() @@ -74,7 +80,7 @@ public function testGetWebsocketParser() { $manager = $this->getWebsocketManager(); - $this->assertTrue($manager->getParser() instanceof SocketIOParser); + $this->assertTrue($manager->getPayloadParser() instanceof SocketIOParser); } public function testRun() @@ -99,19 +105,18 @@ public function testStop() public function testOnStart() { - $filePutContents = false; - $this->mockMethod('file_put_contents', function () use (&$filePutContents) { - $filePutContents = true; - }); + $pidManager = m::mock(PidManager::class); + $pidManager->shouldReceive('write')->once(); $container = $this->getContainer(); $container->singleton('events', function () { return $this->getEvent('swoole.start'); }); + $container->singleton(PidManager::class, function () use ($pidManager) { + return $pidManager; + }); $manager = $this->getManager($container); $manager->onStart(); - - $this->assertTrue($filePutContents); } public function testOnManagerStart() @@ -127,7 +132,7 @@ public function testOnManagerStart() public function testOnWorkerStart() { $server = $this->getServer(); - $manager = $this->getManager(); + $this->getManager(); $container = $this->getContainer($this->getServer(), $this->getConfig(true)); $container->singleton('events', function () { @@ -135,8 +140,8 @@ public function testOnWorkerStart() }); Config::shouldReceive('get') - ->with('swoole_websocket.middleware', []) - ->once(); + ->with('swoole_websocket.middleware', []) + ->once(); WebsocketFacade::shouldReceive('on')->times(3); $manager = $this->getWebsocketManager($container); @@ -144,18 +149,16 @@ public function testOnWorkerStart() $manager->onWorkerStart($server); $app = $manager->getApplication(); - $this->assertTrue($app->make('swoole.sandbox') instanceof Sandbox); + $this->assertTrue($app->make(Sandbox::class) instanceof Sandbox); $this->assertTrue($app->make('swoole.table') instanceof SwooleTable); $this->assertTrue($app->make('swoole.room') instanceof RoomContract); - $this->assertTrue($app->make('swoole.websocket') instanceof Websocket); + $this->assertTrue($app->make(Websocket::class) instanceof Websocket); } public function testLoadApplication() { $server = $this->getServer(); - $manager = $this->getManager(); - - $container = $this->getContainer($this->getServer(), $this->getConfig()); + $container = $this->getContainer($server, $this->getConfig()); $container->singleton('events', function () { return $this->getEvent('swoole.workerStart'); }); @@ -163,8 +166,6 @@ public function testLoadApplication() $path = __DIR__ . '/../fixtures'; $manager = $this->getManager($container, $framework = 'laravel', $path); $manager->onWorkerStart($server); - - $app = $manager->getApplication(); } public function testOnTaskWorkerStart() @@ -177,61 +178,68 @@ public function testOnTaskWorkerStart() return $this->getEvent('swoole.workerStart'); }); - $manager = $this->getManager($container); + $path = __DIR__ . '/../fixtures'; + $manager = $this->getManager($container, $framework = 'laravel', $path); $this->assertNull($manager->onWorkerStart($server)); } public function testOnRequest() { - $server = $this->getServer(); - $manager = $this->getManager(); + $this->getServer(); + $this->getManager(); $container = $this->getContainer($this->getServer(), $this->getConfig(true)); $container->singleton('events', function () { return $this->getEvent('swoole.request', false); }); - $container->singleton('swoole.websocket', function () { + $container->singleton(Websocket::class, function () { $websocket = m::mock(Websocket::class); $websocket->shouldReceive('reset') - ->with(true) - ->once(); + ->with(true) + ->once(); + return $websocket; }); - $container->singleton('swoole.sandbox', function () { + $container->singleton(Sandbox::class, function () { $sandbox = m::mock(Sandbox::class); $sandbox->shouldReceive('setRequest') - ->with(m::type('Illuminate\Http\Request')) - ->once(); + ->with(m::type('Illuminate\Http\Request')) + ->once(); $sandbox->shouldReceive('enable') - ->once(); + ->once(); $sandbox->shouldReceive('run') - ->with(m::type('Illuminate\Http\Request')) - ->once(); + ->with(m::type('Illuminate\Http\Request')) + ->once(); $sandbox->shouldReceive('disable') - ->once(); + ->once(); + return $sandbox; }); + $container->alias(Sandbox::class, 'swoole.sandbox'); + $this->mockMethod('base_path', function () { return '/'; }); $request = m::mock(Request::class); $request->shouldReceive('rawcontent') - ->once() - ->andReturn([]); + ->once() + ->andReturn([]); $response = m::mock(Response::class); $response->shouldReceive('header') - ->twice() - ->andReturnSelf(); + ->twice() + ->andReturnSelf(); $response->shouldReceive('status') - ->once() - ->andReturnSelf(); + ->once() + ->andReturnSelf(); + $response->shouldReceive('write') + ->andReturnSelf(); $response->shouldReceive('end') - ->once() - ->andReturnSelf(); + ->once() + ->andReturnSelf(); $manager = $this->getWebsocketManager(); $manager->setApplication($container); @@ -245,16 +253,21 @@ public function testOnRequestException() $container->singleton('events', function () { return $this->getEvent('swoole.request', false); }); - $container->singleton('swoole.sandbox', function () { + $container->singleton(Sandbox::class, function () { $sandbox = m::mock(Sandbox::class); $sandbox->shouldReceive('disable') - ->once(); + ->once(); + return $sandbox; }); + + $container->alias(Sandbox::class, 'swoole.sandbox'); + $container->singleton(ExceptionHandler::class, function () { $handler = m::mock(ExceptionHandler::class); $handler->shouldReceive('render') - ->once(); + ->once(); + return $handler; }); @@ -264,19 +277,21 @@ public function testOnRequestException() $request = m::mock(Request::class); $request->shouldReceive('rawcontent') - ->once() - ->andReturn([]); + ->once() + ->andReturn([]); $response = m::mock(Response::class); $response->shouldReceive('header') - ->twice() - ->andReturnSelf(); + ->twice() + ->andReturnSelf(); $response->shouldReceive('status') - ->once() - ->andReturnSelf(); + ->once() + ->andReturnSelf(); + $response->shouldReceive('write') + ->andReturnSelf(); $response->shouldReceive('end') - ->once() - ->andReturnSelf(); + ->once() + ->andReturnSelf(); $manager = $this->getManager($container); $manager->setApplication($container); @@ -295,30 +310,25 @@ public function testOnTask() public function testOnShutdown() { - $fileExists = false; - $this->mockMethod('file_exists', function () use (&$fileExists) { - return $fileExists = true; - }); + $pidManager = m::mock(PidManager::class); + $pidManager->shouldReceive('delete')->once(); - $unlink = false; - $this->mockMethod('unlink', function () use (&$unlink) { - return $unlink = true; + $container = $this->getContainer(); + $container->singleton(PidManager::class, function () use ($pidManager) { + return $pidManager; }); - $manager = $this->getManager(); + $manager = $this->getManager($container); $manager->onShutdown(); - - $this->assertTrue($fileExists); - $this->assertTrue($unlink); } public function testSetParser() { $parser = m::mock(Parser::class); $manager = $this->getManager(); - $manager->setParser($parser); + $manager->setPayloadParser($parser); - $this->assertSame($parser, $manager->getParser()); + $this->assertSame($parser, $manager->getPayloadParser()); } public function testSetWebsocketHandler() @@ -337,8 +347,9 @@ public function testLogServerError() $container->singleton(ExceptionHandler::class, function () use ($exception) { $handler = m::mock(ExceptionHandler::class); $handler->shouldReceive('report') - ->with($exception) - ->once(); + ->with($exception) + ->once(); + return $handler; }); $manager = $this->getManager($container); @@ -350,56 +361,126 @@ public function testOnOpen() { $request = m::mock(Request::class); $request->shouldReceive('rawcontent') - ->once() - ->andReturn([]); + ->once() + ->andReturn([]); $request->fd = 1; $container = $this->getContainer(); - $container->singleton('swoole.websocket', function () { + $container->singleton(Websocket::class, function () { $websocket = m::mock(Websocket::class); $websocket->shouldReceive('reset') - ->with(true) - ->once() - ->andReturnSelf(); + ->with(true) + ->once() + ->andReturnSelf(); $websocket->shouldReceive('setSender') - ->with(1) - ->once(); + ->with(1) + ->once(); $websocket->shouldReceive('eventExists') - ->with('connect') - ->once() - ->andReturn(true); + ->with('connect') + ->once() + ->andReturn(true); $websocket->shouldReceive('setContainer') - ->with(m::type(Container::class)) - ->once(); - $websocket->shouldReceive('call') - ->with('connect', m::type('Illuminate\Http\Request')) - ->once(); + ->with(m::type(Container::class)) + ->once(); + $websocket->shouldReceive('call') + ->with('connect', m::type('Illuminate\Http\Request')) + ->once(); + return $websocket; }); - $container->singleton('swoole.sandbox', function () { + $container->singleton(Sandbox::class, function () { $sandbox = m::mock(Sandbox::class); $sandbox->shouldReceive('setRequest') - ->with(m::type('Illuminate\Http\Request')) - ->once(); + ->with(m::type('Illuminate\Http\Request')) + ->once(); $sandbox->shouldReceive('enable') - ->once(); + ->once(); $sandbox->shouldReceive('disable') - ->once(); + ->once(); $sandbox->shouldReceive('getApplication') + ->once() + ->andReturn(m::mock(Container::class)); + + return $sandbox; + }); + + $container->alias(Sandbox::class, 'swoole.sandbox'); + + $handler = m::mock(HandlerContract::class); + $handler->shouldReceive('onOpen') + ->with(1, m::type('Illuminate\Http\Request')) + ->andReturn(true); + + $manager = $this->getWebsocketManager(); + $manager->setApplication($container); + $manager->setWebsocketHandler($handler); + $manager->onOpen(m::mock('server'), $request); + } + + public function testOnHandShake() + { + $request = m::mock(Request::class); + $request->shouldReceive('rawcontent') ->once() - ->andReturn(m::mock(Container::class)); + ->andReturn([]); + $request->fd = 1; + $request->header['sec-websocket-key'] = 'Bet8DkPFq9ZxvIBvPcNy1A=='; + + $response = m::mock(Response::class); + $response->shouldReceive('header')->withAnyArgs()->times(4)->andReturnSelf(); + $response->shouldReceive('status')->with(101)->once()->andReturnSelf(); + $response->shouldReceive('end')->withAnyArgs()->once()->andReturnSelf(); + + $container = $this->getContainer($this->getServer(), $this->getConfig(true)); + $container->singleton(Websocket::class, function () { + $websocket = m::mock(Websocket::class); + $websocket->shouldReceive('reset') + ->with(true) + ->once() + ->andReturnSelf(); + $websocket->shouldReceive('setSender') + ->with(1) + ->once(); + $websocket->shouldReceive('eventExists') + ->with('connect') + ->once() + ->andReturn(true); + $websocket->shouldReceive('setContainer') + ->with(m::type(Container::class)) + ->once(); + $websocket->shouldReceive('call') + ->with('connect', m::type('Illuminate\Http\Request')) + ->once(); + + return $websocket; + }); + $container->singleton(Sandbox::class, function () { + $sandbox = m::mock(Sandbox::class); + $sandbox->shouldReceive('setRequest') + ->with(m::type('Illuminate\Http\Request')) + ->once(); + $sandbox->shouldReceive('enable') + ->once(); + $sandbox->shouldReceive('disable') + ->once(); + $sandbox->shouldReceive('getApplication') + ->once() + ->andReturn(m::mock(Container::class)); + return $sandbox; }); + $container->alias(Sandbox::class, 'swoole.sandbox'); + $handler = m::mock(HandlerContract::class); $handler->shouldReceive('onOpen') - ->with(1, m::type('Illuminate\Http\Request')) - ->andReturn(true); + ->with(1, m::type('Illuminate\Http\Request')) + ->andReturn(true); $manager = $this->getWebsocketManager(); $manager->setApplication($container); $manager->setWebsocketHandler($handler); - $manager->onOpen('server', $request); + $manager->onHandShake($request, $response); } public function testOnMessage() @@ -409,48 +490,52 @@ public function testOnMessage() $parser = m::mock(Parser::class); $parser->shouldReceive('execute') - ->with('server', $frame) - ->once() - ->andReturn(false); + ->with('server', $frame) + ->once() + ->andReturn(false); $parser->shouldReceive('decode') - ->with($frame) - ->once() - ->andReturn($payload = [ - 'event' => 'event', - 'data' => 'data' - ]); + ->with($frame) + ->once() + ->andReturn($payload = [ + 'event' => 'event', + 'data' => 'data', + ]); $container = $this->getContainer(); - $container->singleton('swoole.websocket', function () use ($payload) { + $container->singleton(Websocket::class, function () use ($payload) { $websocket = m::mock(Websocket::class); $websocket->shouldReceive('reset') - ->with(true) - ->once() - ->andReturnSelf(); + ->with(true) + ->once() + ->andReturnSelf(); $websocket->shouldReceive('setSender') - ->with(1) - ->once(); + ->with(1) + ->once(); $websocket->shouldReceive('eventExists') - ->with($payload['event']) - ->once() - ->andReturn(true); + ->with($payload['event']) + ->once() + ->andReturn(true); $websocket->shouldReceive('call') - ->with($payload['event'], $payload['data']) - ->once(); + ->with($payload['event'], $payload['data']) + ->once(); + return $websocket; }); - $container->singleton('swoole.sandbox', function () { + $container->singleton(Sandbox::class, function () { $sandbox = m::mock(Sandbox::class); $sandbox->shouldReceive('enable') - ->once(); + ->once(); $sandbox->shouldReceive('disable') - ->once(); + ->once(); + return $sandbox; }); + $container->alias(Sandbox::class, 'swoole.sandbox'); + $manager = $this->getWebsocketManager(); $manager->setApplication($container); - $manager->setParser($parser); + $manager->setPayloadParser($parser); $manager->onMessage('server', $frame); } @@ -458,24 +543,25 @@ public function testOnClose() { $fd = 1; $app = $this->getContainer(); - $app->singleton('swoole.websocket', function () use ($fd) { + $app->singleton(Websocket::class, function () use ($fd) { $websocket = m::mock(Websocket::class); $websocket->shouldReceive('reset') - ->with(true) - ->once() - ->andReturnSelf(); + ->with(true) + ->once() + ->andReturnSelf(); $websocket->shouldReceive('setSender') - ->with($fd) - ->once(); + ->with($fd) + ->once(); $websocket->shouldReceive('eventExists') - ->with('disconnect') - ->once() - ->andReturn(true); + ->with('disconnect') + ->once() + ->andReturn(true); $websocket->shouldReceive('call') - ->with('disconnect') - ->once(); + ->with('disconnect') + ->once(); $websocket->shouldReceive('leave') - ->once(); + ->once(); + return $websocket; }); @@ -483,72 +569,73 @@ public function testOnClose() $server->shouldReceive('on'); $container = $this->getContainer($server); - $container->singleton('swoole.server', function () use ($fd) { + $container->singleton(Server::class, function () use ($fd) { $server = m::mock('server'); $server->shouldReceive('on'); $server->taskworker = false; $server->master_pid = -1; $server->shouldReceive('connection_info') - ->with($fd) - ->once() - ->andReturn([ - 'websocket_status' => true - ]); + ->with($fd) + ->once() + ->andReturn([ + 'websocket_status' => true, + ]); + return $server; }); + $container->alias(Server::class, 'swoole.server'); + $manager = $this->getWebsocketManager($container); $manager->setApplication($app); $manager->onClose('server', $fd, 'reactorId'); } - public function testNormalizePushMessage() + public function testIsWebsocketPushPayload() { - $data = [ - 'opcode' => 'opcode', - 'sender' => 'sender', - 'fds' => 'fds', - 'broadcast' => 'broadcast', - 'assigned' => 'assigned', - 'event' => 'event', - 'message' => 'message' + $payload = [ + 'action' => 'push', + 'data' => [ + 'opcode' => 'opcode', + 'sender' => 'sender', + 'fds' => 'fds', + 'broadcast' => 'broadcast', + 'assigned' => 'assigned', + 'event' => 'event', + 'message' => 'message', + ] ]; $manager = $this->getWebsocketManager(); - $result = $manager->normalizePushData($data); - - $this->assertSame($data['opcode'], $result[0]); - $this->assertSame($data['sender'], $result[1]); - $this->assertSame($data['fds'], $result[2]); - $this->assertSame($data['broadcast'], $result[3]); - $this->assertSame($data['assigned'], $result[4]); - $this->assertSame($data['event'], $result[5]); - $this->assertSame($data['message'], $result[6]); + + $this->assertTrue($manager->isWebsocketPushPayload($payload)); + $this->assertFalse($manager->isWebsocketPushPayload(['foo' => 'bar'])); } public function testPushMessage() { $data = [ + 'sender' => 1, 'fds' => [1, 2, 3], - 'event' => 'event', - 'message' => 'message' + 'event' => $event = 'event', + 'message' => $message = 'message', + 'broadcast' => true ]; $parser = m::mock(Parser::class); $parser->shouldReceive('encode') - ->with($data['event'], $data['message']) - ->once() - ->andReturn(false); + ->with($data['event'], $data['message']) + ->once() + ->andReturn(false); $server = m::mock('server'); - $server->shouldReceive('exist') - ->times(count($data['fds'])) + $server->shouldReceive('isEstablished') ->andReturn(true); $server->shouldReceive('push') - ->times(count($data['fds'])); + ->twice(); $manager = $this->getWebsocketManager(); - $manager->setParser($parser); + $manager->setPayloadParser($parser); $manager->pushMessage($server, $data); } @@ -568,12 +655,16 @@ protected function getContainer($server = null, $config = null) $config = $config ?? $this->getConfig(); $container = new Container; - $container->singleton('config', function () use ($config) { + $container->singleton(ConfigContract::class, function () use ($config) { return $config; }); - $container->singleton('swoole.server', function () use ($server) { + $container->alias(ConfigContract::class, 'config'); + + $container->singleton(Server::class, function () use ($server) { return $server; }); + $container->alias(Server::class, 'swoole.server'); + $container->singleton(ExceptionHandler::class, Handler::class); return $container; } @@ -590,18 +681,18 @@ protected function getServer() protected function getConfig($websocket = false) { - $config = m::mock('config'); + $config = m::mock(ConfigContract::class); $settings = $websocket ? 'websocketConfig' : 'config'; $callback = function ($key) use ($settings) { return $this->$settings[$key] ?? ''; }; $config->shouldReceive('get') - ->with(m::type('string'), m::any()) - ->andReturnUsing($callback); + ->with(m::type('string'), m::any()) + ->andReturnUsing($callback); $config->shouldReceive('get') - ->with(m::type('string')) - ->andReturnUsing($callback); + ->with(m::type('string')) + ->andReturnUsing($callback); return $config; } @@ -609,9 +700,9 @@ protected function getConfig($websocket = false) protected function getEvent($name, $default = true) { $event = m::mock('event') - ->shouldReceive('fire') - ->with($name, m::any()) - ->once(); + ->shouldReceive('dispatch') + ->with($name, m::any()) + ->once(); $event = $default ? $event->with($name, m::any()) : $event->with($name); diff --git a/tests/Server/PidManagerTest.php b/tests/Server/PidManagerTest.php new file mode 100644 index 00000000..2977055c --- /dev/null +++ b/tests/Server/PidManagerTest.php @@ -0,0 +1,97 @@ +assertEquals($pidFile, $pidManager->file()); + + $pidManager->setPidFile($pidFile = 'laravel-swoole'); + $this->assertEquals($pidFile, $pidManager->file()); + } + + public function testWrite() + { + $pidManager = new PidManager($pidFile = 'foo/bar'); + + $this->mockMethod('is_writable', function () { + return true; + }); + + $inputFilePath = ''; + $inputFileContent = ''; + $this->mockMethod('file_put_contents', function ($pidPath, $pidContent) use (&$inputFilePath, &$inputFileContent) { + $inputFilePath = $pidPath; + $inputFileContent = $pidContent; + }); + + $pidManager->write($masterPid = 1, $managerPid = 2); + + $this->assertEquals($pidFile, $inputFilePath); + $this->assertEquals('1,2', $inputFileContent); + } + + public function testWriteWithException() + { + $pidManager = new PidManager($pidFile = 'foo/bar'); + + $this->mockMethod('is_writable', function () { + return false; + }); + + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('Pid file "foo/bar" is not writable'); + + $pidManager->write($masterPid = 1, $managerPid = 2); + } + + public function testRead() + { + $pidManager = new PidManager($pidFile = 'foo/bar'); + + $this->mockMethod('is_readable', function () { + return true; + }); + + $this->mockMethod('file_get_contents', function () { + return '1,2'; + }); + + $this->assertEquals( + ['masterPid' => '1', 'managerPid' => '2'], + $pidManager->read() + ); + } + + public function testDelete() + { + $pidManager = new PidManager($pidFile = 'foo/bar'); + + $this->mockMethod('is_writable', function () { + return true; + }); + + $unlinkCalled = false; + $this->mockMethod('unlink', function () use (&$unlinkCalled) { + return $unlinkCalled = true; + }); + + $pidManager->delete(); + + $this->assertTrue($unlinkCalled); + } + + protected function mockMethod($name, \Closure $function, $namespace = null) + { + parent::mockMethod($name, $function, 'SwooleTW\Http\Server'); + } +} diff --git a/tests/Server/Resetters/BindRequestTest.php b/tests/Server/Resetters/BindRequestTest.php new file mode 100644 index 00000000..68816b6f --- /dev/null +++ b/tests/Server/Resetters/BindRequestTest.php @@ -0,0 +1,30 @@ +shouldReceive('getRequest') + ->once() + ->andReturn($request); + + $container = new Container; + + $resetter = new BindRequest; + $app = $resetter->handle($container, $sandbox); + + $this->assertSame($request, $app->make('request')); + } +} diff --git a/tests/Server/Resetters/ClearInstanceTest.php b/tests/Server/Resetters/ClearInstanceTest.php new file mode 100644 index 00000000..2b93c265 --- /dev/null +++ b/tests/Server/Resetters/ClearInstanceTest.php @@ -0,0 +1,29 @@ +shouldReceive('getConfig->get') + ->with('swoole_http.instances', []) + ->once() + ->andReturn(['foo']); + + $container = new Container; + $container->instance('foo', m::mock('foo')); + + $resetter = new ClearInstances; + $app = $resetter->handle($container, $sandbox); + + $this->assertFalse($app->bound('foo')); + } +} diff --git a/tests/Server/Resetters/RebindKernelContainerTest.php b/tests/Server/Resetters/RebindKernelContainerTest.php new file mode 100644 index 00000000..c4c66639 --- /dev/null +++ b/tests/Server/Resetters/RebindKernelContainerTest.php @@ -0,0 +1,31 @@ +shouldReceive('isLaravel') + ->once() + ->andReturn(true); + + $kernel = m::mock(Kernel::class); + + $container = new Container; + $container->instance(Kernel::class, $kernel); + + $resetter = new RebindKernelContainer; + $app = $resetter->handle($container, $sandbox); + + $this->assertSame($app, $app->make(Kernel::class)->app); + } +} diff --git a/tests/Server/Resetters/RebindRouterContainerTest.php b/tests/Server/Resetters/RebindRouterContainerTest.php new file mode 100644 index 00000000..1f3834a6 --- /dev/null +++ b/tests/Server/Resetters/RebindRouterContainerTest.php @@ -0,0 +1,79 @@ +shouldReceive('isLaravel') + ->once() + ->andReturn(true); + $sandbox->shouldReceive('getRequest') + ->once() + ->andReturn($request); + + $router = m::mock('router'); + + $container = new Container; + $container->instance('router', $router); + + $route = m::mock('route'); + $route->controller = 'controller'; + $route->shouldReceive('setContainer') + ->once() + ->with($container); + + $routes = m::mock('routes'); + $routes->shouldReceive('match') + ->once() + ->with($request) + ->andReturn($route); + + $router->routes = $routes; + + $resetter = new RebindRouterContainer; + $app = $resetter->handle($container, $sandbox); + + $this->assertSame($app, $router->container); + } + + public function testRebindLumenRouterContainer() + { + $sandbox = m::mock(Sandbox::class); + $sandbox->shouldReceive('isLaravel') + ->once() + ->andReturn(false); + + $router = m::mock('router'); + + $container = m::mock(Container::class); + $container->shouldReceive('offsetSet') + ->with('router', $router) + ->once() + ->andReturnSelf(); + $container->shouldReceive('offsetGet') + ->with('router') + ->andReturn($router); + $container->router = $router; + + $this->mockMethod('property_exists', function () { + return true; + }, 'SwooleTW\Http\Server\Resetters'); + + $resetter = new RebindRouterContainer; + $app = $resetter->handle($container, $sandbox); + + $this->assertSame($app, $app->router->app); + } +} diff --git a/tests/Server/Resetters/RebindViewContainerTest.php b/tests/Server/Resetters/RebindViewContainerTest.php new file mode 100644 index 00000000..c418ae36 --- /dev/null +++ b/tests/Server/Resetters/RebindViewContainerTest.php @@ -0,0 +1,27 @@ +instance('view', $view); + + $resetter = new RebindViewContainer; + $app = $resetter->handle($container, $sandbox); + + $this->assertSame($app, $app->make('view')->container); + $this->assertSame($app, $app->make('view')->shared['app']); + } +} diff --git a/tests/Server/Resetters/ResetConfigTest.php b/tests/Server/Resetters/ResetConfigTest.php new file mode 100644 index 00000000..cb74ffe0 --- /dev/null +++ b/tests/Server/Resetters/ResetConfigTest.php @@ -0,0 +1,28 @@ +shouldReceive('getConfig') + ->once() + ->andReturn($config); + + $container = new Container; + $resetter = new ResetConfig; + $app = $resetter->handle($container, $sandbox); + + $this->assertSame(get_class($config), get_class($app->make('config'))); + } +} diff --git a/tests/Server/Resetters/ResetCookieTest.php b/tests/Server/Resetters/ResetCookieTest.php new file mode 100644 index 00000000..1bec5ff3 --- /dev/null +++ b/tests/Server/Resetters/ResetCookieTest.php @@ -0,0 +1,37 @@ +shouldReceive('getName') + ->once() + ->andReturn('foo'); + + $cookies = m::mock('cookies'); + $cookies->shouldReceive('getQueuedCookies') + ->once() + ->andReturn([$cookie]); + $cookies->shouldReceive('unqueue') + ->once() + ->with('foo'); + + $sandbox = m::mock(Sandbox::class); + + $container = new Container; + $container->instance('cookie', $cookies); + + $resetter = new ResetCookie; + $app = $resetter->handle($container, $sandbox); + } +} diff --git a/tests/Server/Resetters/ResetProvidersTest.php b/tests/Server/Resetters/ResetProvidersTest.php new file mode 100644 index 00000000..dcd2bdcc --- /dev/null +++ b/tests/Server/Resetters/ResetProvidersTest.php @@ -0,0 +1,54 @@ +shouldReceive('register') + ->once(); + $provider->shouldReceive('boot') + ->once(); + + $sandbox = m::mock(Sandbox::class); + $sandbox->shouldReceive('getProviders') + ->once() + ->andReturn([$provider]); + + $this->mockMethod('method_exists', function () { + return true; + }, 'SwooleTW\Http\Server\Resetters'); + + $container = new Container; + $resetter = new ResetProviders; + $app = $resetter->handle($container, $sandbox); + + $reflector = new \ReflectionProperty(TestProvider::class, 'app'); + $reflector->setAccessible(true); + + $this->assertSame($app, $reflector->getValue($provider)); + } +} + +class TestProvider extends ServiceProvider +{ + public function register() + { + // + } + + public function boot() + { + // + } +} + diff --git a/tests/Server/Resetters/ResetSessionTest.php b/tests/Server/Resetters/ResetSessionTest.php new file mode 100644 index 00000000..178be0f4 --- /dev/null +++ b/tests/Server/Resetters/ResetSessionTest.php @@ -0,0 +1,29 @@ +shouldReceive('flush') + ->once(); + $session->shouldReceive('regenerate') + ->once(); + + $sandbox = m::mock(Sandbox::class); + + $container = new Container; + $container->instance('session', $session); + + $resetter = new ResetSession; + $app = $resetter->handle($container, $sandbox); + } +} diff --git a/tests/Server/ResettersTest.php b/tests/Server/ResettersTest.php deleted file mode 100644 index ff416dc2..00000000 --- a/tests/Server/ResettersTest.php +++ /dev/null @@ -1,239 +0,0 @@ -shouldReceive('getRequest') - ->once() - ->andReturn($request); - - $container = new Container; - - $resetter = new BindRequest; - $app = $resetter->handle($container, $sandbox); - - $this->assertSame($request, $app->make('request')); - } - - public function testClearInstance() - { - $sandbox = m::mock(Sandbox::class); - $sandbox->shouldReceive('getConfig->get') - ->with('swoole_http.instances', []) - ->once() - ->andReturn(['foo']); - - $container = new Container; - $container->instance('foo', m::mock('foo')); - - $resetter = new ClearInstances; - $app = $resetter->handle($container, $sandbox); - - $this->assertFalse($app->bound('foo')); - } - - public function testRebindKernelContainer() - { - $sandbox = m::mock(Sandbox::class); - $sandbox->shouldReceive('isLaravel') - ->once() - ->andReturn(true); - - $kernel = m::mock(Kernel::class); - - $container = new Container; - $container->instance(Kernel::class, $kernel); - - $resetter = new RebindKernelContainer; - $app = $resetter->handle($container, $sandbox); - - $this->assertSame($app, $app->make(Kernel::class)->app); - } - - public function testRebindLaravelRouterContainer() - { - $request = m::mock(Request::class); - - $sandbox = m::mock(Sandbox::class); - $sandbox->shouldReceive('isLaravel') - ->once() - ->andReturn(true); - $sandbox->shouldReceive('getRequest') - ->once() - ->andReturn($request); - - $router = m::mock('router'); - - $container = new Container; - $container->instance('router', $router); - - $route = m::mock('route'); - $route->controller = 'controller'; - $route->shouldReceive('setContainer') - ->once() - ->with($container); - - $routes = m::mock('routes'); - $routes->shouldReceive('match') - ->once() - ->with($request) - ->andReturn($route); - - $router->routes = $routes; - - $resetter = new RebindRouterContainer; - $app = $resetter->handle($container, $sandbox); - - $this->assertSame($app, $router->container); - } - - public function testRebindLumenRouterContainer() - { - $sandbox = m::mock(Sandbox::class); - $sandbox->shouldReceive('isLaravel') - ->once() - ->andReturn(false); - - $router = m::mock('router'); - - $container = m::mock(Container::class); - $container->shouldReceive('offsetSet') - ->with('router', $router) - ->once() - ->andReturnSelf(); - $container->shouldReceive('offsetGet') - ->with('router') - ->andReturn($router); - $container->router = $router; - - $this->mockMethod('property_exists', function () { - return true; - }, 'SwooleTW\Http\Server\Resetters'); - - $resetter = new RebindRouterContainer; - $app = $resetter->handle($container, $sandbox); - - $this->assertSame($app, $app->router->app); - } - - public function testRebindViewContainer() - { - $sandbox = m::mock(Sandbox::class); - $view = m::mock('view'); - - $container = new Container; - $container->instance('view', $view); - - $resetter = new RebindViewContainer; - $app = $resetter->handle($container, $sandbox); - - $this->assertSame($app, $app->make('view')->container); - $this->assertSame($app, $app->make('view')->shared['app']); - } - - public function testResetConfig() - { - $config = m::mock('config'); - $sandbox = m::mock(Sandbox::class); - $sandbox->shouldReceive('getConfig') - ->once() - ->andReturn($config); - - $container = new Container; - $resetter = new ResetConfig; - $app = $resetter->handle($container, $sandbox); - - $this->assertSame(get_class($config), get_class($app->make('config'))); - } - - public function testResetCookie() - { - $cookies = m::mock('cookies'); - $cookies->shouldReceive('getQueuedCookies') - ->once() - ->andReturn(['foo']); - $cookies->shouldReceive('unqueue') - ->once() - ->with('foo'); - - $sandbox = m::mock(Sandbox::class); - - $container = new Container; - $container->instance('cookie', $cookies); - - $resetter = new ResetCookie; - $app = $resetter->handle($container, $sandbox); - } - - public function testResetSession() - { - $session = m::mock('session'); - $session->shouldReceive('flush') - ->once(); - $session->shouldReceive('regenerate') - ->once(); - - $sandbox = m::mock(Sandbox::class); - - $container = new Container; - $container->instance('session', $session); - - $resetter = new ResetSession; - $app = $resetter->handle($container, $sandbox); - } - - public function testResetProviders() - { - $provider = m::mock(TestProvider::class); - $provider->shouldReceive('register') - ->once(); - $provider->shouldReceive('boot') - ->once(); - - $sandbox = m::mock(Sandbox::class); - $sandbox->shouldReceive('getProviders') - ->once() - ->andReturn([$provider]); - - $this->mockMethod('method_exists', function () { - return true; - }, 'SwooleTW\Http\Server\Resetters'); - - $container = new Container; - $resetter = new ResetProviders; - $app = $resetter->handle($container, $sandbox); - - $reflector = new \ReflectionProperty(TestProvider::class, 'app'); - $reflector->setAccessible(true); - - $this->assertSame($app, $reflector->getValue($provider)); - } -} - -class TestProvider extends ServiceProvider { - public function register() {} - public function boot() {} -} diff --git a/tests/Server/SandboxTest.php b/tests/Server/SandboxTest.php index 9f04a1e6..876d3bc9 100644 --- a/tests/Server/SandboxTest.php +++ b/tests/Server/SandboxTest.php @@ -2,21 +2,17 @@ namespace SwooleTW\Http\Tests\Server; -use Mockery as m; -use RuntimeException; -use Swoole\Coroutine; -use ReflectionProperty; -use Illuminate\Http\Request; use Illuminate\Config\Repository; -use SwooleTW\Http\Server\Sandbox; -use SwooleTW\Http\Tests\TestCase; use Illuminate\Container\Container; +use Illuminate\Contracts\Container\Container as ContainerContract; +use Illuminate\Http\Request; +use Mockery as m; use SwooleTW\Http\Coroutine\Context; -use SwooleTW\Http\Server\Application; -use Illuminate\Support\Facades\Facade; use SwooleTW\Http\Exceptions\SandboxException; +use SwooleTW\Http\Server\Application; use SwooleTW\Http\Server\Resetters\ResetterContract; -use Illuminate\Contracts\Container\Container as ContainerContract; +use SwooleTW\Http\Server\Sandbox; +use SwooleTW\Http\Tests\TestCase; class SandboxTest extends TestCase { @@ -56,23 +52,23 @@ public function testInitialize() $config = m::mock(Repository::class); $config->shouldReceive('get') - ->with('swoole_http.providers', []) - ->once() - ->andReturn([$providerName]); + ->with('swoole_http.providers', []) + ->once() + ->andReturn([$providerName]); $config->shouldReceive('get') - ->with('swoole_http.resetters', []) - ->once() - ->andReturn([$resetterName]); + ->with('swoole_http.resetters', []) + ->once() + ->andReturn([$resetterName]); $container = m::mock(Container::class); $container->shouldReceive('make') - ->with('config') - ->once() - ->andReturn($config); + ->with(\Illuminate\Contracts\Config\Repository::class) + ->once() + ->andReturn($config); $container->shouldReceive('make') - ->with($resetterName) - ->once() - ->andReturn($resetter); + ->with($resetterName) + ->once() + ->andReturn($resetter); $sandbox = new Sandbox; $sandbox->setBaseApp($container); @@ -99,6 +95,31 @@ public function testGetApplication() $this->assertTrue($sandbox->getSnapshot() instanceof Container); } + public function testGetCachedSnapshot() + { + $container = m::mock(Container::class); + $snapshot = m::mock(Container::class); + $snapshot->shouldReceive('offsetGet') + ->with('foo') + ->once() + ->andReturn($result = 'bar'); + + $sandbox = new Sandbox; + $sandbox->setBaseApp($container); + $sandbox->setSnapshot($snapshot); + + $this->assertTrue($sandbox->getApplication() instanceof Container); + $this->assertEquals($result, $sandbox->getApplication()->foo); + } + + public function testRunWithoutSnapshot() + { + $this->expectException(SandboxException::class); + + $sandbox = new Sandbox; + $sandbox->run($request = m::mock(Request::class)); + } + public function testSetRequest() { $request = m::mock(Request::class); @@ -132,18 +153,19 @@ protected function getContainer() { $config = m::mock(Illuminate\Config\Repository::class); $config->shouldReceive('get') - ->andReturn([]); + ->andReturn([]); $container = m::mock(Container::class); $container->shouldReceive('offsetGet') - ->andReturn((object)[]); + ->andReturn((object) []); $container->shouldReceive('make') - ->andReturn($config); + ->andReturn($config); return $container; } } -class TestResetter implements ResetterContract { +class TestResetter implements ResetterContract +{ public function handle(ContainerContract $app, Sandbox $sandbox) { return 'foo'; diff --git a/tests/SocketIO/PacketTest.php b/tests/SocketIO/PacketTest.php index 484b2d9b..b46b1013 100644 --- a/tests/SocketIO/PacketTest.php +++ b/tests/SocketIO/PacketTest.php @@ -33,35 +33,35 @@ public function testGetPayload() $packet = '42["foo","bar"]'; $this->assertSame([ 'event' => 'foo', - 'data' => 'bar' + 'data' => 'bar', ], Packet::getPayload($packet)); $packet = '42["foo", "bar"]'; $this->assertSame([ 'event' => 'foo', - 'data' => 'bar' + 'data' => 'bar', ], Packet::getPayload($packet)); $packet = '42["foo",{"message":"test"}]'; $this->assertSame([ 'event' => 'foo', 'data' => [ - 'message' => 'test' - ] + 'message' => 'test', + ], ], Packet::getPayload($packet)); $packet = '42["foo", {"message":"test"}]'; $this->assertSame([ 'event' => 'foo', 'data' => [ - 'message' => 'test' - ] + 'message' => 'test', + ], ], Packet::getPayload($packet)); $packet = '42["foo"]'; $this->assertSame([ 'event' => 'foo', - 'data' => null + 'data' => null, ], Packet::getPayload($packet)); } diff --git a/tests/SocketIO/ParserTest.php b/tests/SocketIO/ParserTest.php index ff8c613f..9b952a99 100644 --- a/tests/SocketIO/ParserTest.php +++ b/tests/SocketIO/ParserTest.php @@ -2,10 +2,10 @@ namespace SwooleTW\Http\Tests\SocketIO; +use Illuminate\Support\Facades\App; use Mockery as m; use Swoole\Websocket\Frame; use SwooleTW\Http\Tests\TestCase; -use Illuminate\Support\Facades\App; use SwooleTW\Http\Websocket\SocketIO\SocketIOParser; use SwooleTW\Http\Websocket\SocketIO\Strategies\HeartbeatStrategy; @@ -38,15 +38,15 @@ public function testDecode() $this->assertSame([ 'event' => 'foo', 'data' => [ - 'message' => 'test' - ] + 'message' => 'test', + ], ], $parser->decode($frame)); $payload = '42["foo","bar"]'; $frame->data = $payload; $this->assertSame([ 'event' => 'foo', - 'data' => 'bar' + 'data' => 'bar', ], $parser->decode($frame)); } diff --git a/tests/SocketIO/SocketIOControllerTest.php b/tests/SocketIO/SocketIOControllerTest.php index a805a767..e445e3db 100644 --- a/tests/SocketIO/SocketIOControllerTest.php +++ b/tests/SocketIO/SocketIOControllerTest.php @@ -2,11 +2,11 @@ namespace SwooleTW\Http\Tests\SocketIO; -use Mockery as m; use Illuminate\Http\Request; -use SwooleTW\Http\Tests\TestCase; use Illuminate\Support\Facades\Config; +use Mockery as m; use SwooleTW\Http\Controllers\SocketIOController; +use SwooleTW\Http\Tests\TestCase; class SocketIOControllerTest extends TestCase { @@ -14,18 +14,19 @@ public function testUnknownTransport() { $request = m::mock(Request::class); $request->shouldReceive('input') - ->with('transport') - ->once() - ->andReturn('foo'); + ->with('transport') + ->once() + ->andReturn('foo'); $this->mockMethod('response', function () { $response = m::mock('response'); $response->shouldReceive('json') - ->once() - ->with([ - 'code' => 0, - 'message' => 'Transport unknown' - ], 400); + ->once() + ->with([ + 'code' => 0, + 'message' => 'Transport unknown', + ], 400); + return $response; }); @@ -37,13 +38,13 @@ public function testHasSid() { $request = m::mock(Request::class); $request->shouldReceive('input') - ->with('transport') - ->once() - ->andReturn('websocket'); + ->with('transport') + ->once() + ->andReturn('websocket'); $request->shouldReceive('has') - ->with('sid') - ->once() - ->andReturn(true); + ->with('sid') + ->once() + ->andReturn(true); $controller = new SocketIOController; $result = $controller->upgrade($request); @@ -55,34 +56,35 @@ public function testUpgradePayload() { $request = m::mock(Request::class); $request->shouldReceive('input') - ->with('transport') - ->once() - ->andReturn('websocket'); + ->with('transport') + ->once() + ->andReturn('websocket'); $request->shouldReceive('has') - ->with('sid') - ->once() - ->andReturn(false); + ->with('sid') + ->once() + ->andReturn(false); $base64Encode = false; $this->mockMethod('base64_encode', function () use (&$base64Encode) { $base64Encode = true; + return 'payload'; }); Config::shouldReceive('get') - ->with('swoole_websocket.ping_interval') - ->once() - ->andReturn(1); + ->with('swoole_websocket.ping_interval') + ->once() + ->andReturn(1); Config::shouldReceive('get') - ->with('swoole_websocket.ping_timeout') - ->once() - ->andReturn(1); + ->with('swoole_websocket.ping_timeout') + ->once() + ->andReturn(1); $expectedPayload = json_encode([ 'sid' => 'payload', 'upgrades' => ['websocket'], 'pingInterval' => 1, - 'pingTimeout' => 1 + 'pingTimeout' => 1, ]); $expectedPayload = '97:0' . $expectedPayload . '2:40'; @@ -95,21 +97,20 @@ public function testUpgradePayload() public function testReject() { - $request = m::mock(Request::class); - $this->mockMethod('response', function () { $response = m::mock('response'); $response->shouldReceive('json') - ->once() - ->with([ - 'code' => 3, - 'message' => 'Bad request' - ], 400); + ->once() + ->with([ + 'code' => 3, + 'message' => 'Bad request', + ], 400); + return $response; }); $controller = new SocketIOController; - $controller->reject($request); + $controller->reject(); } protected function mockMethod($name, \Closure $function, $namespace = null) diff --git a/tests/SocketIO/WebsocketHandlerTest.php b/tests/SocketIO/WebsocketHandlerTest.php index 09575f6f..6db1e612 100644 --- a/tests/SocketIO/WebsocketHandlerTest.php +++ b/tests/SocketIO/WebsocketHandlerTest.php @@ -2,12 +2,13 @@ namespace SwooleTW\Http\Tests\SocketIO; -use Mockery as m; -use Swoole\Websocket\Frame; use Illuminate\Http\Request; -use SwooleTW\Http\Tests\TestCase; use Illuminate\Support\Facades\App; use Illuminate\Support\Facades\Config; +use Mockery as m; +use Swoole\Websocket\Frame; +use SwooleTW\Http\Server\Facades\Server; +use SwooleTW\Http\Tests\TestCase; use SwooleTW\Http\Websocket\SocketIO\WebsocketHandler; class WebsocketHandlerTest extends TestCase @@ -17,33 +18,34 @@ public function testOnOpen() $fd = 1; $request = m::mock(Request::class); $request->shouldReceive('input') - ->with('sid') - ->once() - ->andReturn(false); + ->with('sid') + ->once() + ->andReturn(false); Config::shouldReceive('get') - ->with('swoole_websocket.ping_interval') - ->once(); + ->with('swoole_websocket.ping_interval') + ->once(); Config::shouldReceive('get') - ->with('swoole_websocket.ping_timeout') - ->once(); + ->with('swoole_websocket.ping_timeout') + ->once(); $jsonEncode = false; $this->mockMethod('json_encode', function () use (&$jsonEncode) { $jsonEncode = true; + return '{foo: "bar"}'; }, 'SwooleTW\Http\Websocket\SocketIO'); App::shouldReceive('make') - ->with('swoole.server') - ->twice() - ->andReturnSelf(); + ->with(Server::class) + ->twice() + ->andReturnSelf(); App::shouldReceive('push') - ->with($fd, '0{foo: "bar"}') - ->once(); + ->with($fd, '0{foo: "bar"}') + ->once(); App::shouldReceive('push') - ->with($fd, '40') - ->once(); + ->with($fd, '40') + ->once(); $handler = new WebsocketHandler; $this->assertTrue($handler->onOpen($fd, $request)); @@ -55,9 +57,9 @@ public function testOnOpenWithFalseReturn() $fd = 1; $request = m::mock(Request::class); $request->shouldReceive('input') - ->with('sid') - ->once() - ->andReturn(true); + ->with('sid') + ->once() + ->andReturn(true); $handler = new WebsocketHandler; $this->assertFalse($handler->onOpen($fd, $request)); diff --git a/tests/Table/TableTest.php b/tests/Table/TableTest.php index 35efc296..fa32dbed 100644 --- a/tests/Table/TableTest.php +++ b/tests/Table/TableTest.php @@ -4,8 +4,8 @@ use Mockery as m; use Swoole\Table; -use SwooleTW\Http\Tests\TestCase; use SwooleTW\Http\Table\SwooleTable; +use SwooleTW\Http\Tests\TestCase; class TableTest extends TestCase { diff --git a/tests/Task/FakeJob.php b/tests/Task/FakeJob.php index 49b6ab5f..272b658b 100644 --- a/tests/Task/FakeJob.php +++ b/tests/Task/FakeJob.php @@ -3,9 +3,9 @@ namespace SwooleTW\Http\Tests\Task; use Illuminate\Bus\Queueable; -use Illuminate\Queue\SerializesModels; -use Illuminate\Queue\InteractsWithQueue; use Illuminate\Contracts\Queue\ShouldQueue; +use Illuminate\Queue\InteractsWithQueue; +use Illuminate\Queue\SerializesModels; class FakeJob implements ShouldQueue { diff --git a/tests/Task/QueueFactoryTest.php b/tests/Task/QueueFactoryTest.php new file mode 100644 index 00000000..66cae40b --- /dev/null +++ b/tests/Task/QueueFactoryTest.php @@ -0,0 +1,62 @@ +mockEnv('Laravel\Lumen'); + } + + public function testItHasNeededStubByVersion() + { + $version = FW::version(); + + $search = version_compare($version, '5.7', '>=') ? '5.7' : '5.6'; + $stub = QueueFactory::stub($version); + + $this->assertTrue(Str::contains($stub, $search)); + } + + public function testItCanCompareNeededStubByVersion() + { + $version = '5.6'; + $search = '5.7'; + + $stub = QueueFactory::stub($version); + + $this->assertNotTrue(Str::contains($stub, $search)); + } + + public function testItCanMakeNeededQueueByVersion() + { + $version = FW::version(); + + $server = $this->getServer(); + $queue = QueueFactory::make($server, $version); + + $this->assertInstanceOf(QueueFactory::QUEUE_CLASS, $queue); + } + + protected function getServer() + { + $server = m::mock('server'); + $server->taskworker = false; + $server->master_pid = -1; + + return $server; + } +} \ No newline at end of file diff --git a/tests/Task/SwooleJobTest.php b/tests/Task/SwooleJobTest.php index 7171e1ff..2f2ffaca 100644 --- a/tests/Task/SwooleJobTest.php +++ b/tests/Task/SwooleJobTest.php @@ -2,10 +2,10 @@ namespace SwooleTW\Http\Tests\Task; -use Mockery as m; -use SwooleTW\Http\Tests\TestCase; use Illuminate\Container\Container; +use Mockery as m; use SwooleTW\Http\Task\SwooleTaskJob; +use SwooleTW\Http\Tests\TestCase; class SwooleJobTest extends TestCase { diff --git a/tests/Task/SwooleQueueTest.php b/tests/Task/SwooleQueueTest.php index fecbc734..cbff7344 100644 --- a/tests/Task/SwooleQueueTest.php +++ b/tests/Task/SwooleQueueTest.php @@ -3,16 +3,16 @@ namespace SwooleTW\Http\Tests\Task; use Mockery as m; +use SwooleTW\Http\Helpers\FW; +use SwooleTW\Http\Task\QueueFactory; use SwooleTW\Http\Tests\TestCase; -use SwooleTW\Http\Task\SwooleTaskQueue; class SwooleQueueTest extends TestCase { public function testPushProperlyPushesJobOntoSwoole() { $server = $this->getServer(); - - $queue = new SwooleTaskQueue($server); + $queue = QueueFactory::make($server, FW::version()); $server->shouldReceive('task')->once(); $queue->push(new FakeJob); } diff --git a/tests/TestCase.php b/tests/TestCase.php index 9d2ff71a..28f32301 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -2,12 +2,12 @@ namespace SwooleTW\Http\Tests; +use Illuminate\Support\Facades\Facade; use Mockery as m; use phpmock\Mock; use phpmock\MockBuilder; -use SwooleTW\Http\Coroutine\Context; -use Illuminate\Support\Facades\Facade; use PHPUnit\Framework\TestCase as BaseTestCase; +use SwooleTW\Http\Coroutine\Context; class TestCase extends BaseTestCase { @@ -26,7 +26,7 @@ public function tearDown() protected function mockMethod($name, \Closure $function, $namespace = null) { - $builder = new MockBuilder(); + $builder = new MockBuilder; $builder->setNamespace($namespace) ->setName($name) ->setFunction($function); @@ -34,4 +34,15 @@ protected function mockMethod($name, \Closure $function, $namespace = null) $mock = $builder->build(); $mock->enable(); } + + protected function mockEnv(string $namespace, array $variables = []) + { + $this->mockMethod('env', function ($key, $value = null) use ($variables) { + if (array_key_exists($key, $variables)) { + return $variables[$key]; + } + + return null; + }, $namespace); + } } diff --git a/tests/Transformers/RequestTest.php b/tests/Transformers/RequestTest.php index 226ffa40..82adf818 100644 --- a/tests/Transformers/RequestTest.php +++ b/tests/Transformers/RequestTest.php @@ -2,11 +2,11 @@ namespace SwooleTW\Http\Tests\Transformers; +use Illuminate\Http\Request as IlluminateRequest; use Mockery as m; +use Swoole\Http\Request as SwooleRequest; use SwooleTW\Http\Tests\TestCase; use SwooleTW\Http\Transformers\Request; -use Swoole\Http\Request as SwooleRequest; -use Illuminate\Http\Request as IlluminateRequest; class RequestTest extends TestCase { @@ -34,25 +34,24 @@ public function testHandleStatic() $fileSize = false; $this->mockMethod('filesize', function () use (&$fileSize) { $fileSize = true; + return 1; }); - $mimeContentType = false; - $this->mockMethod('mime_content_type', function () use (&$mimeContentType) { - $mimeContentType = true; - return 'foo'; + $this->mockMethod('pathinfo', function () { + return 'css?id=bfaf14972de9d89ae8fc'; }); $response = m::mock('response'); $response->shouldReceive('status') - ->with(200) - ->once(); + ->with(200) + ->once(); $response->shouldReceive('header') - ->with('Content-Type', 'foo') - ->once(); + ->with('Content-Type', 'text/css') + ->once(); $response->shouldReceive('sendfile') - ->with('/foo.bar') - ->once(); + ->with('/foo.bar') + ->once(); Request::handleStatic(new SwooleRequestStub, $response, '/'); @@ -66,7 +65,7 @@ public function testHandleStaticWithBlackList() $request->server['request_uri'] = 'foo.php'; $result = Request::handleStatic($request, null, '/'); - $this->assertNull($result); + $this->assertFalse($result); } public function testHandleStaticWithNoneFile() @@ -74,11 +73,12 @@ public function testHandleStaticWithNoneFile() $isFile = false; $this->mockMethod('is_file', function () use (&$isFile) { $isFile = true; + return false; }); $result = Request::handleStatic(new SwooleRequestStub, null, '/'); - $this->assertNull($result); + $this->assertFalse($result); $this->assertTrue($isFile); } @@ -91,17 +91,23 @@ protected function mockMethod($name, \Closure $function, $namespace = null) class SwooleRequestStub extends SwooleRequest { public $get = []; + public $post = []; + public $header = ['foo' => 'bar']; + public $server = [ 'HTTP_CONTENT_LENGTH' => 0, 'CONTENT_LENGTH' => 0, 'HTTP_CONTENT_TYPE' => null, 'CONTENT_TYPE' => null, - 'request_uri' => 'foo.bar' + 'request_uri' => 'foo.bar', ]; + public $cookie = []; + public $files = []; + public $fd = 1; function rawContent() diff --git a/tests/Transformers/ResponseTest.php b/tests/Transformers/ResponseTest.php index d25084e5..41df5999 100644 --- a/tests/Transformers/ResponseTest.php +++ b/tests/Transformers/ResponseTest.php @@ -2,10 +2,10 @@ namespace SwooleTW\Http\Tests\Transformers; +use Illuminate\Http\Response as IlluminateResponse; +use Swoole\Http\Response as SwooleResponse; use SwooleTW\Http\Tests\TestCase; use SwooleTW\Http\Transformers\Response; -use Swoole\Http\Response as SwooleResponse; -use Illuminate\Http\Response as IlluminateResponse; class ResponseTest extends TestCase { diff --git a/tests/Websocket/HandShakeHandlerTest.php b/tests/Websocket/HandShakeHandlerTest.php new file mode 100644 index 00000000..abb5956c --- /dev/null +++ b/tests/Websocket/HandShakeHandlerTest.php @@ -0,0 +1,71 @@ +header['sec-websocket-key'] = 'Bet8DkPFq9ZxvIBvPcNy1A=='; + + $response = m::mock(Response::class); + $response->shouldReceive('header')->withAnyArgs()->times(4)->andReturnSelf(); + $response->shouldReceive('status')->with(101)->once()->andReturnSelf(); + $response->shouldReceive('end')->withAnyArgs()->once()->andReturnSelf(); + + $handler = new HandShakeHandler; + + // act + $actual = $handler->handle($request, $response); + + // assert + $this->assertTrue($actual); + } + + public function testHandleReturnFalse() + { + // arrange + $request = m::mock(Request::class); + $request->header['sec-websocket-key'] = 'test'; + + $response = m::mock(Response::class); + $response->shouldReceive('end')->withAnyArgs()->once()->andReturnSelf(); + + $handler = new HandShakeHandler; + + // act + $actual = $handler->handle($request, $response); + + // assert + $this->assertFalse($actual); + } + + public function testHandleWithSecWebsocketProtocol() + { + // arrange + $request = m::mock(Request::class); + $request->header['sec-websocket-key'] = 'Bet8DkPFq9ZxvIBvPcNy1A=='; + $request->header['sec-websocket-protocol'] = 'graphql-ws'; + + $response = m::mock(Response::class); + $response->shouldReceive('header')->withAnyArgs()->times(5)->andReturnSelf(); + $response->shouldReceive('status')->with(101)->once()->andReturnSelf(); + $response->shouldReceive('end')->withAnyArgs()->once()->andReturnSelf(); + + $handler = new HandShakeHandler; + + // act + $actual = $handler->handle($request, $response); + + // assert + $this->assertTrue($actual); + } +} diff --git a/tests/Websocket/Middleware/AuthenticateTest.php b/tests/Websocket/Middleware/AuthenticateTest.php index 95abf43a..d7dfa5be 100644 --- a/tests/Websocket/Middleware/AuthenticateTest.php +++ b/tests/Websocket/Middleware/AuthenticateTest.php @@ -2,23 +2,26 @@ namespace SwooleTW\Http\Tests\Websocket\Middleware; -use Mockery as m; +use Illuminate\Contracts\Auth\Factory as Auth; use Illuminate\Http\Request; +use Mockery as m; use SwooleTW\Http\Tests\TestCase; -use Illuminate\Contracts\Auth\Factory as Auth; use SwooleTW\Http\Websocket\Middleware\Authenticate; class AuthenticateTest extends TestCase { public function testAuthenticate() { - $auth = m::mock(Auth::class); - $auth->shouldReceive('authenticate') - ->once() - ->andReturn('user'); - $request = m::mock(Request::class); $request->shouldReceive('setUserResolver') + ->once(); + + $auth = m::mock(Auth::class); + $auth->shouldReceive('authenticate') + ->once() + ->andReturn('user'); + $auth->shouldReceive('setRequest') + ->with($request) ->once(); $middleware = new Authenticate($auth); diff --git a/tests/Websocket/Middleware/DecrpytCookiesTest.php b/tests/Websocket/Middleware/DecrpytCookiesTest.php index e9f8913d..8c0347b0 100644 --- a/tests/Websocket/Middleware/DecrpytCookiesTest.php +++ b/tests/Websocket/Middleware/DecrpytCookiesTest.php @@ -2,12 +2,12 @@ namespace SwooleTW\Http\Tests\Websocket\Middleware; -use Mockery as m; +use Illuminate\Contracts\Encryption\Encrypter as EncrypterContract; use Illuminate\Http\Request; +use Mockery as m; use SwooleTW\Http\Tests\TestCase; -use Symfony\Component\HttpFoundation\ParameterBag; use SwooleTW\Http\Websocket\Middleware\DecryptCookies; -use Illuminate\Contracts\Encryption\Encrypter as EncrypterContract; +use Symfony\Component\HttpFoundation\ParameterBag; class DecrpytCookiesTest extends TestCase { @@ -27,17 +27,17 @@ public function testDecrypt() 'foo' => 'bar', 'seafood' => 'sasaya', 'aaa' => [ - 'bbb' => 'ccc' - ] + 'bbb' => 'ccc', + ], ]); $encrypter = m::mock(EncrypterContract::class); $encrypter->shouldReceive('decrypt') - ->once() - ->with('sasaya', false); + ->once() + ->with('sasaya', false); $encrypter->shouldReceive('decrypt') - ->once() - ->with('ccc', false); + ->once() + ->with('ccc', false); $middleware = $this->getMiddleware($encrypter); $middleware->disableFor('foo'); diff --git a/tests/Websocket/Middleware/StartSessionTest.php b/tests/Websocket/Middleware/StartSessionTest.php index ac95c708..0ab4c8e4 100644 --- a/tests/Websocket/Middleware/StartSessionTest.php +++ b/tests/Websocket/Middleware/StartSessionTest.php @@ -2,11 +2,7 @@ namespace SwooleTW\Http\Tests\Websocket\Middleware; -use Mockery as m; -use Illuminate\Http\Request; use SwooleTW\Http\Tests\TestCase; -use Illuminate\Session\SessionManager; -use SwooleTW\Http\Websocket\Middleware\StartSession; class StartSessionTest extends TestCase { diff --git a/tests/Websocket/PusherTest.php b/tests/Websocket/PusherTest.php new file mode 100644 index 00000000..1913592c --- /dev/null +++ b/tests/Websocket/PusherTest.php @@ -0,0 +1,149 @@ + 1, + 'sender' => 3, + 'fds' => [1, 2], + 'broadcast' => true, + 'assigned' => true, + 'event' => 'event', + 'message' => 'message' + ]; + $pusher = Pusher::make($data, null); + + $this->assertInstanceOf(Pusher::class, $pusher); + $this->assertSame($data['opcode'], $pusher->getOpCode()); + $this->assertSame($data['sender'], $pusher->getSender()); + $this->assertSame($data['fds'], $pusher->getDescriptors()); + $this->assertSame($data['opcode'], $pusher->getOpCode()); + $this->assertSame($data['broadcast'], $pusher->isBroadCast()); + $this->assertSame($data['assigned'], $pusher->isAssigned()); + $this->assertSame($data['event'], $pusher->getEvent()); + $this->assertSame($data['message'], $pusher->getMessage()); + } + + public function testAddDescriptor() + { + $pusher = Pusher::make([ + 'opcode' => 1, + 'sender' => 3, + 'fds' => [1, 2], + 'broadcast' => true, + 'assigned' => true, + 'event' => 'event', + 'message' => 'message' + ], null); + + $pusher->addDescriptor(3); + $this->assertSame([1, 2, 3], $pusher->getDescriptors()); + + $pusher->addDescriptor(1); + $this->assertSame([1, 2, 3], $pusher->getDescriptors()); + } + + public function testAddDescriptors() + { + $pusher = Pusher::make([ + 'opcode' => 1, + 'sender' => 3, + 'fds' => [1, 2], + 'broadcast' => true, + 'assigned' => true, + 'event' => 'event', + 'message' => 'message' + ], null); + + $pusher->addDescriptors([3]); + $this->assertSame([1, 2, 3], $pusher->getDescriptors()); + + $pusher->addDescriptors([1, 4]); + $this->assertSame([1, 2, 3, 4], $pusher->getDescriptors()); + } + + public function testHasDescriptor() + { + $pusher = Pusher::make([ + 'opcode' => 1, + 'sender' => 3, + 'fds' => [1, 2], + 'broadcast' => true, + 'assigned' => true, + 'event' => 'event', + 'message' => 'message' + ], null); + + $this->assertTrue($pusher->hasDescriptor(1)); + $this->assertTrue($pusher->hasDescriptor(2)); + $this->assertFalse($pusher->hasDescriptor(3)); + } + + public function testShouldBroadcast() + { + $pusher = Pusher::make([ + 'opcode' => 1, + 'sender' => 1, + 'fds' => [], + 'broadcast' => true, + 'assigned' => false, + 'event' => 'event', + 'message' => 'message' + ], null); + + $this->assertTrue($pusher->shouldBroadcast()); + } + + public function testShouldPushToDescriptor() + { + $server = m::mock(Server::class); + $server->shouldReceive('isEstablished') + ->with($fd = 1) + ->times(3) + ->andReturn(true); + + $pusher = Pusher::make([ + 'opcode' => 1, + 'sender' => 3, + 'fds' => [], + 'broadcast' => true, + 'assigned' => false, + 'event' => 'event', + 'message' => 'message' + ], $server); + + $this->assertTrue($pusher->shouldPushToDescriptor($fd)); + + $pusher = Pusher::make([ + 'opcode' => 1, + 'sender' => 1, + 'fds' => [], + 'broadcast' => true, + 'assigned' => false, + 'event' => 'event', + 'message' => 'message' + ], $server); + + $this->assertFalse($pusher->shouldPushToDescriptor($fd)); + + $pusher = Pusher::make([ + 'opcode' => 1, + 'sender' => 1, + 'fds' => [], + 'broadcast' => false, + 'assigned' => false, + 'event' => 'event', + 'message' => 'message' + ], $server); + + $this->assertTrue($pusher->shouldPushToDescriptor($fd)); + } +} diff --git a/tests/Websocket/RedisRoomTest.php b/tests/Websocket/RedisRoomTest.php index 8acf0c24..eeda14c2 100644 --- a/tests/Websocket/RedisRoomTest.php +++ b/tests/Websocket/RedisRoomTest.php @@ -3,8 +3,8 @@ namespace SwooleTW\Http\Tests\Websocket; use Mockery as m; -use SwooleTW\Http\Tests\TestCase; use Predis\Client as RedisClient; +use SwooleTW\Http\Tests\TestCase; use SwooleTW\Http\Websocket\Rooms\RedisRoom; class RedisRoomTest extends TestCase @@ -13,11 +13,11 @@ public function testPrepare() { $redis = $this->getRedis(); $redis->shouldReceive('keys') - ->once() - ->andReturn($keys = ['foo', 'bar']); + ->once() + ->andReturn($keys = ['foo', 'bar']); $redis->shouldReceive('del') - ->with($keys) - ->once(); + ->with($keys) + ->once(); $redisRoom = new RedisRoom([]); $redisRoom->prepare($redis); @@ -38,7 +38,7 @@ public function testAddValue() { $redis = $this->getRedis(); $redis->shouldReceive('pipeline') - ->once(); + ->once(); $redisRoom = $this->getRedisRoom($redis); $redisRoom->addValue(1, ['foo', 'bar'], 'fds'); @@ -48,7 +48,7 @@ public function testAddAll() { $redis = $this->getRedis(); $redis->shouldReceive('pipeline') - ->times(3); + ->times(3); $redisRoom = $this->getRedisRoom($redis); $redisRoom->add(1, ['foo', 'bar']); @@ -58,7 +58,7 @@ public function testAdd() { $redis = $this->getRedis(); $redis->shouldReceive('pipeline') - ->twice(); + ->twice(); $redisRoom = $this->getRedisRoom($redis); $redisRoom->add(1, 'foo'); @@ -68,7 +68,7 @@ public function testDeleteAll() { $redis = $this->getRedis(); $redis->shouldReceive('pipeline') - ->times(3); + ->times(3); $redisRoom = $this->getRedisRoom($redis); $redisRoom->delete(1, ['foo', 'bar']); @@ -78,7 +78,7 @@ public function testDelete() { $redis = $this->getRedis(); $redis->shouldReceive('pipeline') - ->twice(); + ->twice(); $redisRoom = $this->getRedisRoom($redis); $redisRoom->delete(1, 'foo'); @@ -88,8 +88,8 @@ public function testGetRooms() { $redis = $this->getRedis(); $redis->shouldReceive('smembers') - ->with('swoole:fds:1') - ->once(); + ->with('swoole:fds:1') + ->once(); $redisRoom = $this->getRedisRoom($redis); $redisRoom->getRooms(1); @@ -99,8 +99,8 @@ public function testGetClients() { $redis = $this->getRedis(); $redis->shouldReceive('smembers') - ->with('swoole:rooms:foo') - ->once(); + ->with('swoole:rooms:foo') + ->once(); $redisRoom = $this->getRedisRoom($redis); $redisRoom->getClients('foo'); diff --git a/tests/Websocket/SimpleParserTest.php b/tests/Websocket/SimpleParserTest.php index 2ef763cd..f55f28e1 100644 --- a/tests/Websocket/SimpleParserTest.php +++ b/tests/Websocket/SimpleParserTest.php @@ -4,7 +4,6 @@ use Mockery as m; use Swoole\Websocket\Frame; -use Swoole\Websocket\Server; use SwooleTW\Http\Tests\TestCase; use SwooleTW\Http\Websocket\SimpleParser; @@ -18,7 +17,7 @@ public function testEncode() $parser = new SimpleParser; $this->assertSame(json_encode([ 'event' => $event, - 'data' => $data + 'data' => $data, ]), $parser->encode($event, $data)); } @@ -26,7 +25,7 @@ public function testDecode() { $payload = json_encode($data = [ 'event' => 'foo', - 'data' => 'bar' + 'data' => 'bar', ]); $frame = m::mock(Frame::class); $frame->data = $payload; diff --git a/tests/Websocket/TableRoomTest.php b/tests/Websocket/TableRoomTest.php index 4eaf412c..26a6813b 100644 --- a/tests/Websocket/TableRoomTest.php +++ b/tests/Websocket/TableRoomTest.php @@ -4,6 +4,7 @@ use Swoole\Table; use SwooleTW\Http\Tests\TestCase; +use SwooleTW\Http\Websocket\Rooms\RoomContract; use SwooleTW\Http\Websocket\Rooms\TableRoom; class TableRoomTest extends TestCase @@ -16,7 +17,7 @@ public function setUp() 'room_rows' => 4096, 'room_size' => 2048, 'client_rows' => 8192, - 'client_size' => 2048 + 'client_size' => 2048, ]; $this->tableRoom = new TableRoom($config); $this->tableRoom->prepare(); @@ -47,7 +48,7 @@ public function testInvalidTableName() public function testSetValue() { - $this->tableRoom->setValue($key = 1, $value = ['foo', 'bar'], $table = 'fds'); + $this->tableRoom->setValue($key = 1, $value = ['foo', 'bar'], $table = RoomContract::DESCRIPTORS_KEY); $this->assertSame($value, $this->tableRoom->getValue($key, $table)); } @@ -56,17 +57,17 @@ public function testAddAll() { $this->tableRoom->add($key = 1, $values = ['foo', 'bar']); - $this->assertSame($values, $this->tableRoom->getValue($key, $table = 'fds')); - $this->assertSame([$key], $this->tableRoom->getValue('foo', 'rooms')); - $this->assertSame([$key], $this->tableRoom->getValue('bar', 'rooms')); + $this->assertSame($values, $this->tableRoom->getValue($key, $table = RoomContract::DESCRIPTORS_KEY)); + $this->assertSame([$key], $this->tableRoom->getValue('foo', RoomContract::ROOMS_KEY)); + $this->assertSame([$key], $this->tableRoom->getValue('bar', RoomContract::ROOMS_KEY)); } public function testAdd() { $this->tableRoom->add($key = 1, $value = 'foo'); - $this->assertSame([$value], $this->tableRoom->getValue($key, $table = 'fds')); - $this->assertSame([$key], $this->tableRoom->getValue($value, 'rooms')); + $this->assertSame([$value], $this->tableRoom->getValue($key, $table = RoomContract::DESCRIPTORS_KEY)); + $this->assertSame([$key], $this->tableRoom->getValue($value, RoomContract::ROOMS_KEY)); } public function testDeleteAll() @@ -74,9 +75,9 @@ public function testDeleteAll() $this->tableRoom->add($key = 1, $values = ['foo', 'bar']); $this->tableRoom->delete($key); - $this->assertSame([], $this->tableRoom->getValue($key, $table = 'fds')); - $this->assertSame([], $this->tableRoom->getValue('foo', 'rooms')); - $this->assertSame([], $this->tableRoom->getValue('bar', 'rooms')); + $this->assertSame([], $this->tableRoom->getValue($key, $table = RoomContract::DESCRIPTORS_KEY)); + $this->assertSame([], $this->tableRoom->getValue('foo', RoomContract::ROOMS_KEY)); + $this->assertSame([], $this->tableRoom->getValue('bar', RoomContract::ROOMS_KEY)); } public function testDelete() @@ -84,9 +85,9 @@ public function testDelete() $this->tableRoom->add($key = 1, $values = ['foo', 'bar']); $this->tableRoom->delete($key, 'foo'); - $this->assertSame(['bar'], $this->tableRoom->getValue($key, $table = 'fds')); - $this->assertSame([], $this->tableRoom->getValue('foo', 'rooms')); - $this->assertSame([$key], $this->tableRoom->getValue('bar', 'rooms')); + $this->assertSame(['bar'], $this->tableRoom->getValue($key, $table = RoomContract::DESCRIPTORS_KEY)); + $this->assertSame([], $this->tableRoom->getValue('foo', RoomContract::ROOMS_KEY)); + $this->assertSame([$key], $this->tableRoom->getValue('bar', RoomContract::ROOMS_KEY)); } public function testGetRooms() @@ -94,7 +95,7 @@ public function testGetRooms() $this->tableRoom->add($key = 1, $values = ['foo', 'bar']); $this->assertSame( - $this->tableRoom->getValue($key, $table = 'fds'), + $this->tableRoom->getValue($key, $table = RoomContract::DESCRIPTORS_KEY), $this->tableRoom->getRooms($key) ); } @@ -106,7 +107,7 @@ public function testGetClients() $this->tableRoom->add($keys[1], $room); $this->assertSame( - $this->tableRoom->getValue($room, $table = 'rooms'), + $this->tableRoom->getValue($room, $table = RoomContract::ROOMS_KEY), $this->tableRoom->getClients($room) ); } diff --git a/tests/Websocket/WebsocketTest.php b/tests/Websocket/WebsocketTest.php index 91dfceab..9f1c5631 100644 --- a/tests/Websocket/WebsocketTest.php +++ b/tests/Websocket/WebsocketTest.php @@ -6,11 +6,13 @@ use Illuminate\Http\Request; use InvalidArgumentException; use Illuminate\Pipeline\Pipeline; +use SwooleTW\Http\Server\Manager; use SwooleTW\Http\Tests\TestCase; use Illuminate\Container\Container; use Illuminate\Support\Facades\App; use Illuminate\Support\Facades\Config; use SwooleTW\Http\Websocket\Websocket; +use SwooleTW\Http\Server\Facades\Server; use SwooleTW\Http\Websocket\Rooms\RoomContract; use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract; @@ -44,60 +46,60 @@ public function testJoin() { $room = m::mock(RoomContract::class); $room->shouldReceive('add') - ->with($sender = 1, $name = ['room']) - ->once(); + ->with($sender = 1, $name = ['room']) + ->once(); $websocket = $this->getWebsocket($room) - ->setSender($sender) - ->join($name); + ->setSender($sender) + ->join($name); } public function testInAlias() { $room = m::mock(RoomContract::class); $room->shouldReceive('add') - ->with($sender = 1, $name = ['room']) - ->once(); + ->with($sender = 1, $name = ['room']) + ->once(); $websocket = $this->getWebsocket($room) - ->setSender($sender) - ->in($name); + ->setSender($sender) + ->in($name); } public function testJoinAll() { $room = m::mock(RoomContract::class); $room->shouldReceive('add') - ->with($sender = 1, $names = ['room1', 'room2']) - ->once(); + ->with($sender = 1, $names = ['room1', 'room2']) + ->once(); $websocket = $this->getWebsocket($room) - ->setSender($sender) - ->join($names); + ->setSender($sender) + ->join($names); } public function testLeave() { $room = m::mock(RoomContract::class); $room->shouldReceive('delete') - ->with($sender = 1, $name = ['room']) - ->once(); + ->with($sender = 1, $name = ['room']) + ->once(); $websocket = $this->getWebsocket($room) - ->setSender($sender) - ->leave($name); + ->setSender($sender) + ->leave($name); } public function testLeaveAll() { $room = m::mock(RoomContract::class); $room->shouldReceive('delete') - ->with($sender = 1, $names = ['room1', 'room2']) - ->once(); + ->with($sender = 1, $names = ['room1', 'room2']) + ->once(); $websocket = $this->getWebsocket($room) - ->setSender($sender) - ->leave($names); + ->setSender($sender) + ->leave($names); } public function testCallbacks() @@ -119,50 +121,50 @@ public function testLoginUsing() { $user = m::mock(AuthenticatableContract::class); $user->shouldReceive('getAuthIdentifier') - ->once() - ->andReturn($id = 1); + ->once() + ->andReturn($id = 1); $room = m::mock(RoomContract::class); $room->shouldReceive('add') - ->with($sender = 1, ['uid_1']) - ->once(); + ->with($sender = 1, ['uid_1']) + ->once(); $websocket = $this->getWebsocket($room) - ->setSender($sender) - ->loginUsing($user); + ->setSender($sender) + ->loginUsing($user); } public function testLoginUsingId() { $room = m::mock(RoomContract::class); $room->shouldReceive('add') - ->with($sender = 1, ['uid_1']) - ->once(); + ->with($sender = 1, ['uid_1']) + ->once(); $websocket = $this->getWebsocket($room) - ->setSender($sender) - ->loginUsingId(1); + ->setSender($sender) + ->loginUsingId(1); } public function testToUserId() { $room = m::mock(RoomContract::class); $room->shouldReceive('getClients') - ->with('uid_1') - ->once() - ->andReturn([$uid = 1]); + ->with('uid_1') + ->once() + ->andReturn([$uid = 1]); $websocket = $this->getWebsocket($room)->toUserId($uid); $this->assertTrue(in_array($uid, $websocket->getTo())); $room->shouldReceive('getClients') - ->with('uid_2') - ->once() - ->andReturn([2]); + ->with('uid_2') + ->once() + ->andReturn([2]); $room->shouldReceive('getClients') - ->with('uid_3') - ->once() - ->andReturn([3]); + ->with('uid_3') + ->once() + ->andReturn([3]); $websocket->toUserId([2, 3]); $this->assertTrue(in_array(2, $websocket->getTo())); @@ -173,35 +175,35 @@ public function testToUser() { $user = m::mock(AuthenticatableContract::class); $user->shouldReceive('getAuthIdentifier') - ->once() - ->andReturn($uid = 1); + ->once() + ->andReturn($uid = 1); $room = m::mock(RoomContract::class); $room->shouldReceive('getClients') - ->with('uid_1') - ->once() - ->andReturn([$uid]); + ->with('uid_1') + ->once() + ->andReturn([$uid]); $websocket = $this->getWebsocket($room)->toUser($user); $this->assertTrue(in_array($uid, $websocket->getTo())); $room->shouldReceive('getClients') - ->with('uid_2') - ->once() - ->andReturn([2]); + ->with('uid_2') + ->once() + ->andReturn([2]); $room->shouldReceive('getClients') - ->with('uid_3') - ->once() - ->andReturn([3]); + ->with('uid_3') + ->once() + ->andReturn([3]); $userA = m::mock(AuthenticatableContract::class); $userA->shouldReceive('getAuthIdentifier') - ->once() - ->andReturn(2); + ->once() + ->andReturn(2); $userB = m::mock(AuthenticatableContract::class); $userB->shouldReceive('getAuthIdentifier') - ->once() - ->andReturn(3); + ->once() + ->andReturn(3); $websocket->toUser($users = [$userA, $userB]); $this->assertTrue(in_array(2, $websocket->getTo())); @@ -212,9 +214,9 @@ public function testGetUserId() { $room = m::mock(RoomContract::class); $room->shouldReceive('getRooms') - ->with($sender = 1) - ->once() - ->andReturn(['uid_1']); + ->with($sender = 1) + ->once() + ->andReturn(['uid_1']); $websocket = $this->getWebsocket($room)->setSender($sender); $this->assertEquals($sender, $websocket->getUserId()); @@ -224,12 +226,12 @@ public function testLogout() { $room = m::mock(RoomContract::class); $room->shouldReceive('getRooms') - ->with($sender = 1) - ->once() - ->andReturn(['uid_1']); + ->with($sender = 1) + ->once() + ->andReturn(['uid_1']); $room->shouldReceive('delete') - ->with($sender, $name = ['uid_1']) - ->once(); + ->with($sender, $name = ['uid_1']) + ->once(); $websocket = $this->getWebsocket($room)->setSender($sender); $websocket->logout(); @@ -239,9 +241,9 @@ public function testIsUserIdOnline() { $room = m::mock(RoomContract::class); $room->shouldReceive('getClients') - ->with('uid_1') - ->once() - ->andReturn([1]); + ->with('uid_1') + ->once() + ->andReturn([1]); $websocket = $this->getWebsocket($room); $this->assertTrue($websocket->isUserIdOnline(1)); @@ -251,8 +253,8 @@ public function testReset() { $websocket = $this->getWebsocket(); $websocket->setSender(1) - ->broadcast() - ->to('foo'); + ->broadcast() + ->to('foo'); $websocket->reset(true); @@ -264,26 +266,26 @@ public function testReset() public function testPipeline() { App::shouldReceive('call') - ->once() - ->andReturnSelf(); + ->once() + ->andReturnSelf(); $request = m::mock(Request::class); - $middlewares = ['foo', 'bar']; + $middleware = ['foo', 'bar']; $pipeline = m::mock(Pipeline::class); $pipeline->shouldReceive('send') - ->with($request) - ->once() - ->andReturnSelf(); + ->with($request) + ->once() + ->andReturnSelf(); $pipeline->shouldReceive('through') - ->with($middlewares) - ->once() - ->andReturnSelf(); + ->with($middleware) + ->once() + ->andReturnSelf(); $pipeline->shouldReceive('then') - ->once() - ->andReturn($request); + ->once() + ->andReturn($request); $websocket = $this->getWebsocket(null, $pipeline); - $websocket->middleware($middlewares); + $websocket->middleware($middleware); $websocket->on('connect', function () { return 'connect'; }); @@ -321,34 +323,85 @@ public function testEmit() $broadcast = true; $room = m::mock(RoomContract::class); $room->shouldReceive('getClients') - ->with(m::type('string')) - ->times(3) - ->andReturn([3, 4, 5]); + ->with(m::type('string')) + ->times(3) + ->andReturn([3, 4, 5]); + + $server = m::mock('server'); + $server->taskworker = false; App::shouldReceive('make') - ->with('swoole.server') - ->once() - ->andReturnSelf(); - - App::shouldReceive('task') - ->with([ - 'action' => 'push', - 'data' => [ - 'sender' => $sender, - 'fds' => [1, 2, 3, 4, 5], - 'broadcast' => $broadcast, - 'assigned' => true, - 'event' => $event = 'event', - 'message' => $data = 'data' - ] - ]) + ->with(Server::class) + ->once() + ->andReturn($server); + + $server->shouldReceive('task') + ->with([ + 'action' => 'push', + 'data' => [ + 'sender' => $sender, + 'fds' => [1, 2, 3, 4, 5], + 'broadcast' => $broadcast, + 'assigned' => true, + 'event' => $event = 'event', + 'message' => $data = 'data', + ], + ]) + ->once(); + + $websocket = $this->getWebsocket($room); + $websocket->setSender($sender) + ->to($to) + ->broadcast() + ->emit($event, $data); + + $this->assertSame([], $websocket->getTo()); + $this->assertFalse($websocket->getIsBroadcast()); + } + + public function testEmitInTaskWorker() + { + $sender = 1; + $to = [1, 2, 'a', 'b', 'c']; + $broadcast = true; + $room = m::mock(RoomContract::class); + $room->shouldReceive('getClients') + ->with(m::type('string')) + ->times(3) + ->andReturn([3, 4, 5]); + + $payload = [ + 'sender' => $sender, + 'fds' => [1, 2, 3, 4, 5], + 'broadcast' => $broadcast, + 'assigned' => true, + 'event' => $event = 'event', + 'message' => $data = 'data', + ]; + + $server = m::mock('server'); + $server->taskworker = true; + + $manager = m::mock(Manager::class); + $manager->shouldReceive('pushMessage') + ->with($server, $payload) ->once(); + App::shouldReceive('make') + ->with(Server::class) + ->once() + ->andReturn($server); + + App::shouldReceive('make') + ->with(Manager::class) + ->once() + ->andReturn($manager); + $websocket = $this->getWebsocket($room); $websocket->setSender($sender) - ->to($to) - ->broadcast() - ->emit($event, $data); + ->to($to) + ->broadcast() + ->emit($event, $data); $this->assertSame([], $websocket->getTo()); $this->assertFalse($websocket->getIsBroadcast()); @@ -359,17 +412,16 @@ public function testClose() $fd = 1; App::shouldReceive('make') - ->with('swoole.server') - ->once() - ->andReturnSelf(); + ->with(Server::class) + ->once() + ->andReturnSelf(); App::shouldReceive('close') - ->with($fd) - ->once(); + ->with($fd) + ->once(); $websocket = $this->getWebsocket(); $websocket->close($fd); - } protected function getWebsocket(RoomContract $room = null, $pipeline = null) @@ -378,8 +430,8 @@ protected function getWebsocket(RoomContract $room = null, $pipeline = null) $pipeline = $pipeline ?: m::mock(Pipeline::class); Config::shouldReceive('get') - ->once() - ->andReturn([]); + ->once() + ->andReturn([]); return new Websocket($room, $pipeline); } diff --git a/tests/fixtures/bootstrap/app.php b/tests/fixtures/bootstrap/app.php index a7ad3f4a..79e6824b 100644 --- a/tests/fixtures/bootstrap/app.php +++ b/tests/fixtures/bootstrap/app.php @@ -5,8 +5,8 @@ use Illuminate\Contracts\Container\Container; $kernel = new TestKernel; - $app = m::mock(Container::class); + $app->shouldReceive('make') ->with(Kernel::class) ->once() @@ -26,9 +26,10 @@ return $app; -class TestKernel { +class TestKernel +{ public function bootstrappers() { return []; } -} \ No newline at end of file +} diff --git a/tests/fixtures/laravel/config/swoole_http.php b/tests/fixtures/laravel/config/swoole_http.php index 17447723..70454af0 100644 --- a/tests/fixtures/laravel/config/swoole_http.php +++ b/tests/fixtures/laravel/config/swoole_http.php @@ -21,5 +21,5 @@ ], 'providers' => [ SwooleTW\Http\Tests\Fixtures\Laravel\App\Providers\TestServiceProvider::class, - ] + ], ]; diff --git a/tests/fixtures/lumen/config/swoole_http.php b/tests/fixtures/lumen/config/swoole_http.php index f9fa02d9..5a9e3c08 100644 --- a/tests/fixtures/lumen/config/swoole_http.php +++ b/tests/fixtures/lumen/config/swoole_http.php @@ -21,5 +21,5 @@ ], 'providers' => [ SwooleTW\Http\Tests\Fixtures\Lumen\App\Providers\TestServiceProvider::class, - ] + ], ];