├── .github ├── FUNDING.yml └── ISSUE_TEMPLATE │ └── bug_report.md ├── .gitignore ├── .scrutinizer.yml ├── .travis.yml ├── KnownIssues-CN.md ├── KnownIssues.md ├── LICENSE ├── README-CN.md ├── README.md ├── Settings-CN.md ├── Settings.md ├── bin ├── fswatch ├── inotify └── laravels ├── composer.json ├── config ├── laravels.php └── prometheus.php ├── grafana-dashboard.json ├── grafana-dashboard.png ├── logo.svg ├── phpunit.xml ├── sponsor.png ├── src ├── Components │ ├── Apollo │ │ ├── Client.php │ │ └── Process.php │ ├── HttpClient │ │ └── SimpleHttpTrait.php │ ├── MetricCollector.php │ ├── MetricCollectorInterface.php │ └── Prometheus │ │ ├── CollectorProcess.php │ │ ├── Collectors │ │ ├── HttpRequestCollector.php │ │ ├── SwooleProcessCollector.php │ │ ├── SwooleStatsCollector.php │ │ └── SystemCollector.php │ │ ├── Exporter.php │ │ ├── RequestMiddleware.php │ │ ├── ServiceProvider.php │ │ └── TimerProcessMetricsCronJob.php ├── Console │ └── Portal.php ├── Illuminate │ ├── CleanerManager.php │ ├── Cleaners │ │ ├── AuthCleaner.php │ │ ├── BaseCleaner.php │ │ ├── CleanerInterface.php │ │ ├── ConfigCleaner.php │ │ ├── ContainerCleaner.php │ │ ├── CookieCleaner.php │ │ ├── DcatAdminCleaner.php │ │ ├── JWTCleaner.php │ │ ├── LaravelAdminCleaner.php │ │ ├── MenuCleaner.php │ │ ├── RequestCleaner.php │ │ ├── SessionCleaner.php │ │ └── ZiggyCleaner.php │ ├── Laravel.php │ ├── LaravelSCommand.php │ ├── LaravelSServiceProvider.php │ ├── LaravelScheduleJob.php │ ├── LaravelTrait.php │ ├── ListPropertiesCommand.php │ ├── LogTrait.php │ └── ReflectionApp.php ├── LaravelS.php └── Swoole │ ├── Coroutine │ └── Context.php │ ├── DynamicResponse.php │ ├── Events │ ├── ServerStartInterface.php │ ├── ServerStopInterface.php │ ├── WorkerErrorInterface.php │ ├── WorkerStartInterface.php │ └── WorkerStopInterface.php │ ├── Inotify.php │ ├── InotifyTrait.php │ ├── Process │ ├── CustomProcessInterface.php │ ├── CustomProcessTrait.php │ └── ProcessTitleTrait.php │ ├── Request.php │ ├── Response.php │ ├── ResponseInterface.php │ ├── Server.php │ ├── Socket │ ├── Http.php │ ├── HttpInterface.php │ ├── PortInterface.php │ ├── TcpInterface.php │ ├── TcpSocket.php │ ├── UdpInterface.php │ ├── UdpSocket.php │ ├── WebSocket.php │ └── WebSocketInterface.php │ ├── StaticResponse.php │ ├── Task │ ├── BaseTask.php │ ├── Event.php │ ├── Listener.php │ └── Task.php │ ├── Timer │ ├── BackupCronJob.php │ ├── CheckGlobalTimerAliveCronJob.php │ ├── CronJob.php │ ├── CronJobInterface.php │ ├── RenewGlobalTimerLockCronJob.php │ └── TimerTrait.php │ └── WebSocketHandlerInterface.php └── tests ├── SimpleHttpTest.php ├── TestCase.php └── avg-qps.sh /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | #custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | 14 | custom: ['https://www.paypal.me/hhxsv5','https://www.blockchain.com/btc/address/367HnAzVTAEk8offesDhcsCQswnugsE54u','https://gitee.com/hhxsv5/laravel-s?donate=true'] 15 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: analyzing 6 | assignees: hhxsv5 7 | 8 | --- 9 | 10 | 1. Your software version (Screenshot of your startup) 11 | 12 | | Software | Version | 13 | | --------- | --------- | 14 | | PHP | TODO | 15 | | Swoole | TODO | 16 | | Laravel/Lumen | TODO | 17 | 18 | 2. Detail description about this issue(error/log) 19 | 20 | `TODO` 21 | 22 | 3. Some `reproducible` code blocks and `steps` 23 | 24 | ```PHP 25 | // TODO: your code 26 | ``` 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | vendor 2 | .idea 3 | composer.lock 4 | *.sw[a-z] 5 | coverage -------------------------------------------------------------------------------- /.scrutinizer.yml: -------------------------------------------------------------------------------- 1 | checks: 2 | php: true 3 | filter: 4 | excluded_paths: 5 | - tests/* 6 | build: 7 | environment: 8 | php: 8.2.0 9 | nodes: 10 | analysis: 11 | tests: 12 | override: 13 | - php-scrutinizer-run 14 | - phpcs-run src/* 15 | my-tests: 16 | environment: 17 | php: 18 | version: 8.2.0 19 | pecl_extensions: 20 | - swoole-4.8.13 21 | dependencies: 22 | before: 23 | - composer install 24 | tests: 25 | override: 26 | - composer test 27 | coverage: 28 | tests: 29 | override: 30 | - command: composer test 31 | coverage: 32 | file: coverage/coverage.xml 33 | format: clover 34 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | php: 4 | - 7.4 5 | - 8.0 6 | - 8.1 7 | - 8.2 8 | - 8.3 9 | 10 | before_script: 11 | - pecl install --onlyreqdeps --nobuild swoole && cd "$(pecl config-get temp_dir)/swoole" && phpize && ./configure --enable-openssl && make -j$(nproc) && make -j$(nproc) install && cd - 12 | - composer install 13 | 14 | script: composer test 15 | -------------------------------------------------------------------------------- /KnownIssues-CN.md: -------------------------------------------------------------------------------- 1 | # 常见问题 2 | 3 | ## Class swoole does not exist 4 | - 在`LaravelS`中,`Swoole`是以`cli`模式启动的`Http Server`,替代了`FPM`。 5 | - 投递任务、触发异步事件都会调用`app('swoole')`,从`Laravel容器`中获取`Swoole\http\server`实例。只有在`LaravelS`启动时,才会注入这个实例到容器中。 6 | - 所以,一旦脱离`LaravelS`,由于跨进程,以下情况,你将`无法`成功调用`app('swoole')`: 7 | - 以各种`命令行`方式运行的代码,例如Artisan命令行、PHP脚本命令行; 8 | - 运行在`FPM`/`Apache PHP Module`下的代码,查看SAPI `Log::info('PHP SAPI', [php_sapi_name()]);`。 9 | 10 | ## 使用包 [encore/laravel-admin](https://github.com/z-song/laravel-admin) 11 | > 修改`config/laravels.php`,在`cleaners`中增加`LaravelAdminCleaner`。 12 | 13 | ```php 14 | 'cleaners' => [ 15 | Hhxsv5\LaravelS\Illuminate\Cleaners\LaravelAdminCleaner::class, 16 | ], 17 | ``` 18 | 19 | ## 使用包 [jenssegers/agent](https://github.com/jenssegers/agent) 20 | > [监听系统事件](https://github.com/hhxsv5/laravel-s/blob/master/README-CN.md#%E7%B3%BB%E7%BB%9F%E4%BA%8B%E4%BB%B6) 21 | 22 | ```php 23 | // 重置Agent 24 | \Event::listen('laravels.received_request', function (\Illuminate\Http\Request $req, $app) { 25 | $app->agent->setHttpHeaders($req->server->all()); 26 | $app->agent->setUserAgent(); 27 | }); 28 | ``` 29 | 30 | ## 使用包 [barryvdh/laravel-debugbar](https://github.com/barryvdh/laravel-debugbar) 31 | > 官方不支持`cli`模式,需通过修改环境变量`APP_RUNNING_IN_CONSOLE`为非`cli`,但启用后不排除会有其他问题。 32 | 33 | `.env`中增加环境变量`APP_RUNNING_IN_CONSOLE=false`。 34 | 35 | ## 使用包 [the-control-group/voyager](https://github.com/the-control-group/voyager) 36 | > `voyager`依赖包[arrilot/laravel-widgets](https://github.com/arrilot/laravel-widgets),而其中`WidgetGroupCollection`是单例,[追加Widget](https://github.com/arrilot/laravel-widgets/blob/master/src/WidgetGroup.php#L270)会造成它们重复展示,通过重新注册ServiceProvider来重置此单例。 37 | 38 | ```php 39 | // config/laravels.php 40 | 'register_providers' => [ 41 | Arrilot\Widgets\ServiceProvider::class, 42 | ], 43 | ``` 44 | 45 | ## 使用包 [overtrue/wechat](https://github.com/overtrue/wechat) 46 | > easywechat包会出现异步通知回调失败的问题,原因是`$app['request']->getContent()`是空的,给其赋值即可。 47 | 48 | ```php 49 | //回调通知 50 | public function notify(Request $request) 51 | { 52 | $app = $this->getPayment();//获取支付实例 53 | $app['request'] = $request;//在原有代码添加这一行,将当前Request赋值给$app['request'] 54 | $response = $app->handlePaidNotify(function ($message, $fail) use($id) { 55 | //... 56 | }); 57 | return $response; 58 | } 59 | ``` 60 | 61 | 62 | ## 使用包 [laracasts/flash](https://github.com/laracasts/flash) 63 | > 常驻内存后,每次调用flash()会追加消息提醒,导致叠加展示消息提醒。有以下两个方案。 64 | 65 | 1.通过中间件在每次请求`处理前`或`处理后`重置$messages `app('flash')->clear();`。 66 | 67 | 2.每次请求处理后重新注册`FlashServiceProvider`,配置[register_providers](https://github.com/hhxsv5/laravel-s/blob/master/Settings-CN.md)。 68 | 69 | ## 使用包 [laravel/telescope](https://github.com/laravel/telescope) 70 | > 因Swoole运行在`cli`模式,导致`RequestWatcher`不能正常识别忽略的路由。 71 | 72 | 解决方案: 73 | 74 | 1.`.env`中增加环境变量`APP_RUNNING_IN_CONSOLE=false`; 75 | 76 | 2.修改代码。 77 | 78 | ```php 79 | // 修改`app/Providers/EventServiceProvider.php`, 添加下面监听代码到boot方法中 80 | // use Laravel\Telescope\Telescope; 81 | // use Illuminate\Support\Facades\Event; 82 | Event::listen('laravels.received_request', function ($request, $app) { 83 | $reflection = new \ReflectionClass(Telescope::class); 84 | $handlingApprovedRequest = $reflection->getMethod('handlingApprovedRequest'); 85 | $handlingApprovedRequest->setAccessible(true); 86 | $handlingApprovedRequest->invoke(null, $app) ? Telescope::startRecording() : Telescope::stopRecording(); 87 | }); 88 | ``` 89 | 90 | ## 单例控制器 91 | 92 | - Laravel 5.3+ 控制器是被绑定在`Router`下的`Route`中,而`Router`是单例,控制器只会被构造`一次`,所以不能在构造方法中初始化`请求级数据`,下面展示`错误的用法`。 93 | 94 | ```php 95 | namespace App\Http\Controllers; 96 | class TestController extends Controller 97 | { 98 | protected $userId; 99 | public function __construct() 100 | { 101 | // 错误的用法:因控制器只被构造一次,然后常驻于内存,所以$userId只会被赋值一次,后续请求会误读取之前请求$userId 102 | $this->userId = session('userId'); 103 | } 104 | public function testAction() 105 | { 106 | // 读取$this->userId; 107 | } 108 | } 109 | ``` 110 | 111 | - 两种解决方法(二选一) 112 | 113 | 1.避免在构造函数中初始化`请求级`的数据,应在具体`Action`中读取,这样编码风格更合理,建议这样写。 114 | 115 | ```bash 116 | # 列出你的路由中所有关联的控制器的所有属性 117 | php artisan laravels:list-properties 118 | ``` 119 | 120 | ```php 121 | namespace App\Http\Controllers; 122 | class TestController extends Controller 123 | { 124 | protected function getUserId() 125 | { 126 | return session('userId'); 127 | } 128 | public function testAction() 129 | { 130 | // 通过调用$this->getUserId()读取$userId 131 | } 132 | } 133 | ``` 134 | 135 | 2.使用`LaravelS`提供的`自动销毁控制器`机制。 136 | 137 | ```php 138 | // config/laravels.php 139 | // 将enable置为true、excluded_list置为[],则表示自动销毁所有控制器 140 | 'destroy_controllers' => [ 141 | 'enable' => true, // 启用自动销毁控制器 142 | 'excluded_list' => [ 143 | //\App\Http\Controllers\TestController::class, // 排除销毁的控制器类列表 144 | ], 145 | ], 146 | ``` 147 | 148 | ## 不能使用这些函数 149 | 150 | - `flush`/`ob_flush`/`ob_end_flush`/`ob_implicit_flush`:`swoole_http_response`不支持`flush`。 151 | 152 | - `dd()`/`exit()`/`die()`: 将导致Worker/Task/Process进程立即退出,建议通过抛异常跳出函数调用栈,[Swoole文档](https://wiki.swoole.com/wiki/page/501.html)。 153 | 154 | - `header()`/`setcookie()`/`http_response_code()`:HTTP响应只能通过Laravel/Lumen的`Response`对象。 155 | 156 | ## 不能使用的全局变量 157 | 158 | - $_GET、$_POST、$_FILES、$_COOKIE、$_REQUEST、$_SESSION、$GLOBALS,$_ENV是`可读`的,$_SERVER是`部分可读`的。 159 | 160 | ## 大小限制 161 | 162 | - `Swoole`限制了`GET`请求头的最大尺寸为`8KB`,建议`Cookie`的不要太大,不然Cookie可能解析失败。 163 | 164 | - `POST`数据或文件上传的大小受`Swoole`配置[`package_max_length`](https://wiki.swoole.com/wiki/page/301.html)限制,默认上限`2M`。 165 | 166 | - 文件上传的大小受到了[memory_limit](https://www.php.net/manual/zh/ini.core.php#ini.memory-limit)的限制,默认`128M`。 167 | 168 | ## Inotify监听文件数达到上限 169 | > `Warning: inotify_add_watch(): The user limit on the total number of inotify watches was reached` 170 | 171 | - `Linux`中`Inotify`监听文件数默认上限一般是`8192`,实际项目的文件数+目录树很可能超过此上限,进而导致后续的监听失败。 172 | 173 | - 增加此上限到`524288`:`echo fs.inotify.max_user_watches=524288 | sudo tee -a /etc/sysctl.conf && sudo sysctl -p`,注意`Docker`时需设置启用`privileged`。 174 | 175 | ## 注意include/require与(include/require)_once 176 | > 看看鸟哥这篇文章[再一次, 不要使用(include/require)_once](http://www.laruence.com/2012/09/12/2765.html) 177 | 178 | - 引入`类`、`接口`、`trait`、`函数`时使用(include/require)_once,其他情况使用include/require。 179 | 180 | - 在多进程模式下,子进程会继承父进程资源,一旦父进程引入了某个需要被执行的文件,子进程再次`require_once()`时会直接返回`true`,导致该文件执行失败。此时,你应该使用include/require。 181 | 182 | 183 | ## 对于`Swoole < 1.9.17`的环境 184 | > 开启`handle_static`后,静态资源文件将由`LaravelS`组件处理。由于PHP环境的原因,可能会导致`MimeTypeGuesser`无法正确识别`MimeType`,比如会Javascript与CSS文件会被识别为`text/plain`。 185 | 186 | 解决方案: 187 | 188 | 1.升级Swoole到`1.9.17+` 189 | 190 | 2.注册自定义MIME猜测器 191 | 192 | ```php 193 | // MyGuessMimeType.php 194 | use Symfony\Component\HttpFoundation\File\MimeType\MimeTypeGuesserInterface; 195 | class MyGuessMimeType implements MimeTypeGuesserInterface 196 | { 197 | protected static $map = [ 198 | 'js' => 'application/javascript', 199 | 'css' => 'text/css', 200 | ]; 201 | public function guess($path) 202 | { 203 | $ext = pathinfo($path, PATHINFO_EXTENSION); 204 | if (strlen($ext) > 0) { 205 | return Arr::get(self::$map, $ext); 206 | } else { 207 | return null; 208 | } 209 | } 210 | } 211 | ``` 212 | 213 | ```php 214 | // AppServiceProvider.php 215 | use Symfony\Component\HttpFoundation\File\MimeType\MimeTypeGuesser; 216 | public function boot() 217 | { 218 | MimeTypeGuesser::getInstance()->register(new MyGuessMimeType()); 219 | } 220 | ``` 221 | 222 | -------------------------------------------------------------------------------- /KnownIssues.md: -------------------------------------------------------------------------------- 1 | # Known issues 2 | 3 | ## Class swoole does not exist 4 | - In `LaravelS`, `Swoole` is `Http Server` started in `cli` mode, replacing `FPM`. 5 | - Delivering a task, triggering an asynchronous event will call `app('swoole')` and get the `Swoole\http\server` instance from the `Laravel container`. This instance is injected into the container only when `LaravelS` is started. 6 | - So, once you leave the `LaravelS`, due to the cross-process, you will be `unable` to successfully call `app('swoole')`: 7 | - The code that runs in various `command line` modes, such as the Artisan command line and the PHP script command line. 8 | - Run the code under `FPM`/`Apache PHP Module`, view SAPI `Log::info('PHP SAPI', [php_sapi_name()]);`. 9 | 10 | ## Use package [encore/laravel-admin](https://github.com/z-song/laravel-admin) 11 | > Modify `config/laravels.php` and add` LaravelAdminCleaner` in `cleaners`. 12 | 13 | ```php 14 | 'cleaners' => [ 15 | Hhxsv5\LaravelS\Illuminate\Cleaners\LaravelAdminCleaner::class, 16 | ], 17 | ``` 18 | 19 | ## Use package [jenssegers/agent](https://github.com/jenssegers/agent) 20 | > [Listen System Event](https://github.com/hhxsv5/laravel-s/blob/master/README.md#system-events) 21 | 22 | ```php 23 | // Reset Agent 24 | \Event::listen('laravels.received_request', function (\Illuminate\Http\Request $req, $app) { 25 | $app->agent->setHttpHeaders($req->server->all()); 26 | $app->agent->setUserAgent(); 27 | }); 28 | ``` 29 | 30 | ## Use package [barryvdh/laravel-debugbar](https://github.com/barryvdh/laravel-debugbar) 31 | > Not support `cli` mode officially, you need to add the environment variable `APP_RUNNING_IN_CONSOLE` to be non-cli`, but there may be some other issues. 32 | 33 | Add environment variable `APP_RUNNING_IN_CONSOLE=false` to `.env`. 34 | 35 | ## Use package [the-control-group/voyager](https://github.com/the-control-group/voyager) 36 | > `voyager` dependencies [arrilot/laravel-widgets](https://github.com/arrilot/laravel-widgets), where `WidgetGroupCollection` is a singleton, [appending widget](https://github.com/Arrilot/laravel-widgets/blob/master/src/WidgetGroup.php#L270) will cause them to repeat the display, you need to reset the singleton by re-registering the ServiceProvider. 37 | 38 | ```php 39 | // config/laravels.php 40 | 'register_providers' => [ 41 | Arrilot\Widgets\ServiceProvider::class, 42 | ], 43 | ``` 44 | 45 | ## Use package [overtrue/wechat](https://github.com/overtrue/wechat) 46 | > The asynchronous notification callback will be failing, because `$app['request']->getContent()` is empty, give it a value. 47 | 48 | ```php 49 | public function notify(Request $request) 50 | { 51 | $app = $this->getPayment();//Get payment instance 52 | $app['request'] = $request;//Add this line to the original code and assign the current request instance to $app['request'] 53 | $response = $app->handlePaidNotify(function ($message, $fail) use($id) { 54 | //... 55 | }); 56 | return $response; 57 | } 58 | ``` 59 | ## Use package [laracasts/flash](https://github.com/laracasts/flash) 60 | > Flash messages are held in memory all the time. Appending to `$messages` when call flash() every time, leads to the multiple messages. There are two solutions. 61 | 62 | 1.Reset `$messages` by middleware `app('flash')->clear();`. 63 | 64 | 2.Re-register `FlashServiceProvider` after handling request, Refer [register_providers](https://github.com/hhxsv5/laravel-s/blob/master/Settings.md). 65 | 66 | ## Use package [laravel/telescope](https://github.com/laravel/telescope) 67 | > Because Swoole is running in `cli` mode, `RequestWatcher` does not recognize the ignored route properly. 68 | 69 | Solution: 70 | 71 | 1.Add environment variable `APP_RUNNING_IN_CONSOLE=false` to `.env`; 72 | 73 | 2.Modify code. 74 | 75 | ```php 76 | // Edit file `app/Providers/EventServiceProvider.php`, add the following code into method `boot` 77 | // use Laravel\Telescope\Telescope; 78 | // use Illuminate\Support\Facades\Event; 79 | Event::listen('laravels.received_request', function ($request, $app) { 80 | $reflection = new \ReflectionClass(Telescope::class); 81 | $handlingApprovedRequest = $reflection->getMethod('handlingApprovedRequest'); 82 | $handlingApprovedRequest->setAccessible(true); 83 | $handlingApprovedRequest->invoke(null, $app) ? Telescope::startRecording() : Telescope::stopRecording(); 84 | }); 85 | ``` 86 | 87 | ## Singleton controller 88 | 89 | - Laravel 5.3+ controller is bound to `Route` under `Router`, and `Router` is a singleton, controller will only be constructed `once`, so you cannot initialize `request-level data` in the constructor, the following shows you the `wrong` usage. 90 | 91 | ```php 92 | namespace App\Http\Controllers; 93 | class TestController extends Controller 94 | { 95 | protected $userId; 96 | public function __construct() 97 | { 98 | // Wrong usage: Since the controller is only constructed once and then resident in memory, $userId will only be assigned once, and subsequent requests will be misread before requesting $userId 99 | $this->userId = session('userId'); 100 | } 101 | public function testAction() 102 | { 103 | // read $this->userId; 104 | } 105 | } 106 | ``` 107 | 108 | - Two solutions (choose one) 109 | 110 | 1.Avoid initializing `request-level` data in the constructor, which should be read in the concrete `Action`. This coding style is more reasonable, it is recommended to do so. 111 | 112 | ```bash 113 | # List all properties of all controllers related your routes. 114 | php artisan laravels:list-properties 115 | ``` 116 | 117 | ```php 118 | namespace App\Http\Controllers; 119 | class TestController extends Controller 120 | { 121 | protected function getUserId() 122 | { 123 | return session('userId'); 124 | } 125 | public function testAction() 126 | { 127 | // call $this->getUserId() to read $userId 128 | } 129 | } 130 | ``` 131 | 132 | 2.Use the `automatic destruction controller` mechanism provided by `LaravelS`. 133 | 134 | ```php 135 | // config/laravels.php 136 | // Set enable to true and exclude_list to [], which means that all controllers are automatically destroyed. 137 | 'destroy_controllers' => [ 138 | 'enable' => true, // Enable automatic destruction controller 139 | 'excluded_list' => [ 140 | //\App\Http\Controllers\TestController::class, // The excluded list of destroyed controller classes 141 | ], 142 | ], 143 | ``` 144 | 145 | ## Cannot call these functions 146 | 147 | - `flush`/`ob_flush`/`ob_end_flush`/`ob_implicit_flush`: `swoole_http_response` does not support `flush`. 148 | 149 | - `dd()`/`exit()`/`die()`: will lead to Worker/Task/Process quit right now, suggest jump out function call stack by throwing exception. 150 | 151 | - `header()`/`setcookie()`/`http_response_code()`: Make HTTP response by Laravel/Lumen `Response` only in LaravelS underlying. 152 | 153 | ## Cannot use these global variables 154 | 155 | - $_GET/$_POST/$_FILES/$_COOKIE/$_REQUEST/$_SESSION/$GLOBALS, $_ENV is `readable`, $_SERVER is `partial readable`. 156 | 157 | ## Size restriction 158 | 159 | - The max size of `GET` request's header is `8KB`, limited by `Swoole`, the big `Cookie` will lead to parse Cookie fail. 160 | 161 | - The max size of `POST` data/file is limited by `Swoole` [package_max_length](https://www.swoole.co.uk/docs/modules/swoole-server/configuration), default `2M`. 162 | 163 | - The max size of the file upload is limited by [memory_limit](https://www.php.net/manual/en/ini.core.php#ini.memory-limit), default `128M`. 164 | 165 | ## Inotify reached the watchers limit 166 | > `Warning: inotify_add_watch(): The user limit on the total number of inotify watches was reached` 167 | 168 | - Inotify limit is default `8192` for most `Linux`, but the amount of actual project may be more than it, then lead to watch fail. 169 | 170 | - Increase the amount of inotify watchers to `524288`: `echo fs.inotify.max_user_watches=524288 | sudo tee -a /etc/sysctl.conf && sudo sysctl -p`, note: you need to enable `privileged` for `Docker`. 171 | 172 | ## include/require与(include/require)_once 173 | > See Laruence's blog [Do NOT USE (include/require)_once](http://www.laruence.com/2012/09/12/2765.html) 174 | 175 | - To include the files about `class`/`interface`/`trait`/`function`, sugguest to use (include/require)_once. In other cases, use include/require. 176 | 177 | - In the multi-process mode, the child process inherits the parent process resource. Once the parent process includes a file that needs to be executed, the child process will directly return true when it uses require_once(), causing the file to fail to execute. Now, you need to use include/require. 178 | 179 | ## If `Swoole < 1.9.17` 180 | > After enabling `handle_static`, static resource files will be handled by `LaravelS`. Due to the PHP environment, `MimeTypeGuesser` may not correctly recognize `MimeType`. For example, Javascript and CSS files will be recognized as `text/plain`. 181 | 182 | Solutions: 183 | 184 | 1.Upgrade Swoole to `1.9.17+`. 185 | 186 | 2.Register a custom MIME guesser. 187 | 188 | ```php 189 | // MyGuessMimeType.php 190 | use Symfony\Component\HttpFoundation\File\MimeType\MimeTypeGuesserInterface; 191 | class MyGuessMimeType implements MimeTypeGuesserInterface 192 | { 193 | protected static $map = [ 194 | 'js' => 'application/javascript', 195 | 'css' => 'text/css', 196 | ]; 197 | public function guess($path) 198 | { 199 | $ext = pathinfo($path, PATHINFO_EXTENSION); 200 | if (strlen($ext) > 0) { 201 | return Arr::get(self::$map, $ext); 202 | } else { 203 | return null; 204 | } 205 | } 206 | } 207 | ``` 208 | 209 | ```php 210 | // AppServiceProvider.php 211 | use Symfony\Component\HttpFoundation\File\MimeType\MimeTypeGuesser; 212 | public function boot() 213 | { 214 | MimeTypeGuesser::getInstance()->register(new MyGuessMimeType()); 215 | } 216 | ``` 217 | 218 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 hhxsv5 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Settings-CN.md: -------------------------------------------------------------------------------- 1 | # LaravelS 配置项 2 | 3 | ## listen_ip 4 | > `string` 监听的IP,监听本机`127.0.0.1`(IPv4) `::1`(IPv6),监听所有地址 `0.0.0.0`(IPv4) `::`(IPv6), 默认`127.0.0.1`。 5 | 6 | ## listen_port 7 | > `int` 监听的端口,如果端口小于1024则需要`root`权限,默认 `5200`。 8 | 9 | ## socket_type 10 | > 默认`SWOOLE_SOCK_TCP`。通常情况下,无需关心这个配置。若需Nginx代理至`UnixSocket Stream`文件,则需修改为`SWOOLE_SOCK_UNIX_STREAM`,此时`listen_ip`则是`UnixSocket Stream`文件的路径。 11 | 12 | ## server 13 | > `string` 当通过LaravelS响应数据时,设置HTTP头部`Server`的值,若为空则不设置,默认 `LaravelS`。 14 | 15 | ## handle_static 16 | > `bool` 是否开启LaravelS处理静态资源(要求 `Swoole >= 1.7.21`,若`Swoole >= 1.9.17`则由Swoole自己处理),默认`false`,建议Nginx处理静态资源,LaravelS仅处理动态资源。静态资源的默认路径为`base_path('public')`,可通过修改`swoole.document_root`变更。 17 | 18 | ## laravel_base_path 19 | > `string` `Laravel/Lumen`的基础路径,默认`base_path()`,可用于配置`符号链接`。 20 | 21 | ## inotify_reload.enable 22 | > `bool` 是否开启`Inotify Reload`,用于当修改代码后实时Reload所有worker进程,依赖库[inotify](http://pecl.php.net/package/inotify),通过命令`php --ri inotify`检查是否可用,默认`false`,`建议仅开发环境开启`,[修改监听数上限](https://github.com/hhxsv5/laravel-s/blob/master/KnownIssues-CN.md#inotify%E7%9B%91%E5%90%AC%E6%96%87%E4%BB%B6%E6%95%B0%E8%BE%BE%E5%88%B0%E4%B8%8A%E9%99%90)。 23 | 24 | ## inotify_reload.watch_path 25 | > `string` `Inotify` 监控的文件路径,默认有`base_path()`。 26 | 27 | ## inotify_reload.file_types 28 | > `array` `Inotify` 监控的文件类型,默认有`.php`。 29 | 30 | ## inotify_reload.excluded_dirs 31 | > `array` `Inotify` 监控时需要排除(或忽略)的目录,默认`[]`,示例:`[base_path('vendor')]`。 32 | 33 | ## inotify_reload.log 34 | > `bool` 是否输出Reload的日志,默认`true`。 35 | 36 | ## event_handlers 37 | > `array` 配置`Swoole`的事件回调函数,key-value格式,key为事件名,value为实现了事件处理接口的类,参考[示例](https://github.com/hhxsv5/laravel-s/blob/master/README-CN.md#%E9%85%8D%E7%BD%AEswoole%E7%9A%84%E4%BA%8B%E4%BB%B6%E5%9B%9E%E8%B0%83%E5%87%BD%E6%95%B0)。 38 | 39 | ## websocket.enable 40 | > `bool` 是否启用WebSocket服务器。启用后WebSocket服务器监听的IP和端口与Http服务器相同,默认`false`。 41 | 42 | ## websocket.handler 43 | > `string` WebSocket逻辑处理的类名,需实现接口`WebSocketHandlerInterface`,参考[示例](https://github.com/hhxsv5/laravel-s/blob/master/README-CN.md#%E5%90%AF%E7%94%A8websocket%E6%9C%8D%E5%8A%A1%E5%99%A8)。 44 | 45 | ## sockets 46 | > `array` 配置`TCP/UDP`套接字列表,参考[示例](https://github.com/hhxsv5/laravel-s/blob/master/README-CN.md#%E5%A4%9A%E7%AB%AF%E5%8F%A3%E6%B7%B7%E5%90%88%E5%8D%8F%E8%AE%AE)。 47 | 48 | ## processes 49 | > `array` 配置自定义进程列表,参考[示例](https://github.com/hhxsv5/laravel-s/blob/master/README-CN.md#%E8%87%AA%E5%AE%9A%E4%B9%89%E8%BF%9B%E7%A8%8B)。 50 | 51 | ## timer 52 | > `array` 配置毫秒定时器,参考[示例](https://github.com/hhxsv5/laravel-s/blob/master/README-CN.md#%E6%AF%AB%E7%A7%92%E7%BA%A7%E5%AE%9A%E6%97%B6%E4%BB%BB%E5%8A%A1)。 53 | 54 | ## swoole_tables 55 | > `array` 定义的`swoole_table`列表,参考[示例](https://github.com/hhxsv5/laravel-s/blob/master/README-CN.md#%E4%BD%BF%E7%94%A8swoole_table)。 56 | 57 | ## cleaners 58 | > `array` `每次请求`的清理器列表,用于清理一些残留的全局变量、单例对象、静态属性,避免多次请求间数据污染。这些清理器类必须实现接口`Hhxsv5\LaravelS\Illuminate\Cleaners\CleanerInterface`。清理的顺序与数组的顺序保持一致。[这些清理器](https://github.com/hhxsv5/laravel-s/blob/master/src/Illuminate/CleanerManager.php#L31)默认已启用,无需再配置。 59 | 60 | ```php 61 | // 如果你的项目中使用到了Session、Authentication、Passport,需配置如下清理器 62 | 'cleaners' => [ 63 | Hhxsv5\LaravelS\Illuminate\Cleaners\SessionCleaner::class, 64 | Hhxsv5\LaravelS\Illuminate\Cleaners\AuthCleaner::class, 65 | ], 66 | ``` 67 | 68 | ```php 69 | // 如果你的项目中使用到了包"tymon/jwt-auth",需配置如下清理器 70 | 'cleaners' => [ 71 | Hhxsv5\LaravelS\Illuminate\Cleaners\SessionCleaner::class, 72 | Hhxsv5\LaravelS\Illuminate\Cleaners\AuthCleaner::class, 73 | Hhxsv5\LaravelS\Illuminate\Cleaners\JWTCleaner::class, 74 | ], 75 | ``` 76 | 77 | ```php 78 | // 如果你的项目中使用到了包"spatie/laravel-menu",需配置如下清理器 79 | 'cleaners' => [ 80 | Hhxsv5\LaravelS\Illuminate\Cleaners\MenuCleaner::class, 81 | ], 82 | ``` 83 | 84 | ```php 85 | // 如果你的项目中使用到了包"encore/laravel-admin",需配置如下清理器 86 | 'cleaners' => [ 87 | Hhxsv5\LaravelS\Illuminate\Cleaners\SessionCleaner::class, 88 | Hhxsv5\LaravelS\Illuminate\Cleaners\AuthCleaner::class, 89 | Hhxsv5\LaravelS\Illuminate\Cleaners\LaravelAdminCleaner::class, 90 | ], 91 | ``` 92 | 93 | ```php 94 | // 如果你的项目中使用到了包"jqhph/dcat-admin" 95 | 'cleaners' => [ 96 | Hhxsv5\LaravelS\Illuminate\Cleaners\SessionCleaner::class, 97 | Hhxsv5\LaravelS\Illuminate\Cleaners\AuthCleaner::class, 98 | Hhxsv5\LaravelS\Illuminate\Cleaners\DcatAdminCleaner::class, 99 | ], 100 | ``` 101 | 102 | ```php 103 | // 如果你的项目中使用到了包"tightenco/ziggy",解决"Ziggy is not defined" 104 | 'cleaners' => [ 105 | Hhxsv5\LaravelS\Illuminate\Cleaners\ZiggyCleaner::class, 106 | ], 107 | ``` 108 | 109 | ## register_providers 110 | > `array` `每次请求`需要重新注册的`Service Provider`列表,若存在`boot()`方法,会自动执行。一般用于清理`注册了单例的ServiceProvider`。 111 | 112 | ```php 113 | //... 114 | 'register_providers' => [ 115 | \Xxx\Yyy\XxxServiceProvider::class, 116 | ], 117 | //... 118 | ``` 119 | 120 | ## destroy_controllers 121 | > `array` 每次请求后自动销毁控制器,解决单例控制器的问题,参考[示例](https://github.com/hhxsv5/laravel-s/blob/master/KnownIssues-CN.md#%E5%8D%95%E4%BE%8B%E6%8E%A7%E5%88%B6%E5%99%A8)。 122 | 123 | ## swoole 124 | > `array` Swoole的`原始`配置项,请参考[Swoole服务器配置项](https://wiki.swoole.com/#/server/setting)。 -------------------------------------------------------------------------------- /Settings.md: -------------------------------------------------------------------------------- 1 | # LaravelS Settings 2 | 3 | ## listen_ip 4 | > `string` The listening ip, like local address `127.0.0.1`(IPv4) `::1`(IPv6), all addresses `0.0.0.0`(IPv4) `::`(IPv6), default `127.0.0.1`. 5 | 6 | ## listen_port 7 | > `int` The listening port, need `root` permission if port less than `1024`, default `5200`. 8 | 9 | ## socket_type 10 | > `int` Default `SWOOLE_SOCK_TCP`. Usually, you don’t need to care about it. Unless you want Nginx to proxy to the `UnixSocket Stream` file, you need to modify it to `SWOOLE_SOCK_UNIX_STREAM`, and `listen_ip` is the path of `UnixSocket Stream` file. 11 | 12 | ## server 13 | > `string` Set HTTP header `Server` when respond by LaravelS, default `LaravelS`. 14 | 15 | ## handle_static 16 | > `bool` Whether handle the static resource by LaravelS(Require `Swoole >= 1.7.21`, Handle by Swoole if `Swoole >= 1.9.17`), default `false`, Suggest that Nginx handles the statics and LaravelS handles the dynamics. The default path of static resource is `base_path('public')`, you can modify `swoole.document_root` to change it. 17 | 18 | ## laravel_base_path 19 | > `string` The basic path of `Laravel/Lumen`, default `base_path()`, be used for `symbolic link`. 20 | 21 | ## inotify_reload.enable 22 | > `bool` Whether enable the `Inotify Reload` to reload all worker processes when your code is modified, depend on [inotify](http://pecl.php.net/package/inotify), use `php --ri inotify` to check whether the available. default `false`, `recommend to enable in development environment only`, change [Watchers Limit](https://github.com/hhxsv5/laravel-s/blob/master/KnownIssues.md#inotify-reached-the-watchers-limit). 23 | 24 | ## inotify_reload.watch_path 25 | > `string` The file path that `Inotify` watches, default `base_path()`. 26 | 27 | ## inotify_reload.file_types 28 | > `array` The file types that `Inotify` watches, default `['.php']`. 29 | 30 | ## inotify_reload.excluded_dirs 31 | > `array` The excluded/ignored directories that `Inotify` watches, default `[]`, eg: `[base_path('vendor')]`. 32 | 33 | ## inotify_reload.log 34 | > `bool` Whether output the reload log, default `true`. 35 | 36 | ## event_handlers 37 | > `array` Configure the event callback function of `Swoole`, key-value format, key is the event name, and value is the class that implements the event processing interface, refer [Demo](https://github.com/hhxsv5/laravel-s/blob/master/README.md#configuring-the-event-callback-function-of-swoole). 38 | 39 | ## websocket.enable 40 | > `bool` Whether enable WebSocket Server. The Listening address of WebSocket Sever is the same as Http Server, default `false`. 41 | 42 | ## websocket.handler 43 | > `string` The class name for WebSocket handler, needs to implement interface `WebSocketHandlerInterface`, refer [Demo](https://github.com/hhxsv5/laravel-s/blob/master/README.md#enable-websocket-server). 44 | 45 | ## sockets 46 | > `array` The socket list for TCP/UDP, refer [Demo](https://github.com/hhxsv5/laravel-s/blob/master/README.md#multi-port-mixed-protocol). 47 | 48 | ## processes 49 | > `array` The custom process list, refer [Demo](https://github.com/hhxsv5/laravel-s/blob/master/README.md#custom-process). 50 | 51 | ## timer 52 | > `array` The millisecond timer, refer [Demo](https://github.com/hhxsv5/laravel-s/blob/master/README.md#millisecond-cron-job). 53 | 54 | ## swoole_tables 55 | > `array` The defined of `swoole_table` list, refer [Demo](https://github.com/hhxsv5/laravel-s/blob/master/README.md#use-swoole_table). 56 | 57 | ## cleaners 58 | > `array` The list of cleaners for `each request` is used to clean up some residual global variables, singleton objects, and static properties to avoid data pollution between requests, these classes must implement interface `Hhxsv5\LaravelS\Illuminate\Cleaners\CleanerInterface`. The order of cleanup is consistent with the order of the arrays. [These cleaners](https://github.com/hhxsv5/laravel-s/blob/master/src/Illuminate/CleanerManager.php#L31) enabled by default, and do not need to be configured. 59 | 60 | ```php 61 | // Need to configure the following cleaners if you use the session/authentication/passport in your project 62 | 'cleaners' => [ 63 | Hhxsv5\LaravelS\Illuminate\Cleaners\SessionCleaner::class, 64 | Hhxsv5\LaravelS\Illuminate\Cleaners\AuthCleaner::class, 65 | ], 66 | ``` 67 | 68 | ```php 69 | // Need to configure the following cleaners if you use the package "tymon/jwt-auth" in your project 70 | 'cleaners' => [ 71 | Hhxsv5\LaravelS\Illuminate\Cleaners\SessionCleaner::class, 72 | Hhxsv5\LaravelS\Illuminate\Cleaners\AuthCleaner::class, 73 | Hhxsv5\LaravelS\Illuminate\Cleaners\JWTCleaner::class, 74 | ], 75 | ``` 76 | 77 | ```php 78 | // Need to configure the following cleaners if you use the package "spatie/laravel-menu" in your project 79 | 'cleaners' => [ 80 | Hhxsv5\LaravelS\Illuminate\Cleaners\MenuCleaner::class, 81 | ], 82 | ``` 83 | 84 | ```php 85 | // Need to configure the following cleaners if you use the package "encore/laravel-admin" in your project 86 | 'cleaners' => [ 87 | Hhxsv5\LaravelS\Illuminate\Cleaners\SessionCleaner::class, 88 | Hhxsv5\LaravelS\Illuminate\Cleaners\AuthCleaner::class, 89 | Hhxsv5\LaravelS\Illuminate\Cleaners\LaravelAdminCleaner::class, 90 | ], 91 | ``` 92 | 93 | ```php 94 | // Need to configure the following cleaners if you use the package "jqhph/dcat-admin" in your project 95 | 'cleaners' => [ 96 | Hhxsv5\LaravelS\Illuminate\Cleaners\SessionCleaner::class, 97 | Hhxsv5\LaravelS\Illuminate\Cleaners\AuthCleaner::class, 98 | Hhxsv5\LaravelS\Illuminate\Cleaners\DcatAdminCleaner::class, 99 | ], 100 | ``` 101 | 102 | ```php 103 | // Need to configure the following cleaners if you use the package "tightenco/ziggy" in your project to solve "Ziggy is not defined" 104 | 'cleaners' => [ 105 | Hhxsv5\LaravelS\Illuminate\Cleaners\ZiggyCleaner::class, 106 | ], 107 | ``` 108 | 109 | ## register_providers 110 | > `array` The `Service Provider` list, will be re-registered `each request`, and run method `boot()` if it exists. Usually, be used to clear the `Service Provider` which registers `Singleton` instances. 111 | 112 | ```php 113 | //... 114 | 'register_providers' => [ 115 | \Xxx\Yyy\XxxServiceProvider::class, 116 | ], 117 | //... 118 | ``` 119 | 120 | ## destroy_controllers 121 | > `array` Automatically destroy the controllers after each request to solve the problem of the singleton controllers, refer [Demo](https://github.com/hhxsv5/laravel-s/blob/master/KnownIssues.md#singleton-controller). 122 | 123 | ## swoole 124 | > `array` Swoole's `original` configuration items, refer [Swoole Server Configuration](https://www.swoole.co.uk/docs/modules/swoole-server/configuration). -------------------------------------------------------------------------------- /bin/fswatch: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | WORK_DIR=$1 3 | if [ ! -n "${WORK_DIR}" ] ;then 4 | WORK_DIR="." 5 | fi 6 | 7 | echo "Restarting LaravelS..." 8 | ./bin/laravels restart -d -i 9 | 10 | echo "Starting fswatch..." 11 | LOCKING=0 12 | fswatch -e ".*" -i "\\.php$" -r ${WORK_DIR} | while read file 13 | do 14 | if [[ ! ${file} =~ .php$ ]] ;then 15 | continue 16 | fi 17 | if [ ${LOCKING} -eq 1 ] ;then 18 | echo "Reloading, skipped." 19 | continue 20 | fi 21 | echo "File ${file} has been modified." 22 | LOCKING=1 23 | ./bin/laravels reload 24 | LOCKING=0 25 | done 26 | exit 0 -------------------------------------------------------------------------------- /bin/inotify: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | WORK_DIR=$1 3 | if [ ! -n "${WORK_DIR}" ] ;then 4 | WORK_DIR="." 5 | fi 6 | 7 | echo "Restarting LaravelS..." 8 | ./bin/laravels restart -d -i 9 | 10 | echo "Starting inotifywait..." 11 | LOCKING=0 12 | 13 | inotifywait --event modify --event create --event move --event delete -mrq ${WORK_DIR} | while read file 14 | 15 | do 16 | if [[ ! ${file} =~ .php$ ]] ;then 17 | continue 18 | fi 19 | if [ ${LOCKING} -eq 1 ] ;then 20 | echo "Reloading, skipped." 21 | continue 22 | fi 23 | echo "File ${file} has been modified." 24 | LOCKING=1 25 | ./bin/laravels reload 26 | LOCKING=0 27 | done 28 | exit 0 -------------------------------------------------------------------------------- /bin/laravels: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | prefixes[$prefix]) === false) { 49 | $this->prefixes[$prefix] = []; 50 | } 51 | 52 | // retain the base directory for the namespace prefix 53 | if ($prepend) { 54 | array_unshift($this->prefixes[$prefix], $base_dir); 55 | } else { 56 | $this->prefixes[$prefix][] = $base_dir; 57 | } 58 | } 59 | 60 | /** 61 | * Loads the class file for a given class name. 62 | * 63 | * @param string $class The fully-qualified class name. 64 | * @return mixed The mapped file name on success, or boolean false on 65 | * failure. 66 | */ 67 | public function loadClass($class) 68 | { 69 | // the current namespace prefix 70 | $prefix = $class; 71 | 72 | // work backwards through the namespace names of the fully-qualified 73 | // class name to find a mapped file name 74 | while (false !== $pos = strrpos($prefix, '\\')) { 75 | // retain the trailing namespace separator in the prefix 76 | $prefix = substr($class, 0, $pos + 1); 77 | 78 | // the rest is the relative class name 79 | $relative_class = substr($class, $pos + 1); 80 | 81 | // try to load a mapped file for the prefix and relative class 82 | $mapped_file = $this->loadMappedFile($prefix, $relative_class); 83 | if ($mapped_file) { 84 | return $mapped_file; 85 | } 86 | 87 | // remove the trailing namespace separator for the next iteration 88 | // of strrpos() 89 | $prefix = rtrim($prefix, '\\'); 90 | } 91 | 92 | // never found a mapped file 93 | return false; 94 | } 95 | 96 | /** 97 | * Load the mapped file for a namespace prefix and relative class. 98 | * 99 | * @param string $prefix The namespace prefix. 100 | * @param string $relative_class The relative class name. 101 | * @return mixed Boolean false if no mapped file can be loaded, or the 102 | * name of the mapped file that was loaded. 103 | */ 104 | protected function loadMappedFile($prefix, $relative_class) 105 | { 106 | // are there any base directories for this namespace prefix? 107 | if (isset($this->prefixes[$prefix]) === false) { 108 | return false; 109 | } 110 | 111 | // look through base directories for this namespace prefix 112 | foreach ($this->prefixes[$prefix] as $base_dir) { 113 | // replace the namespace prefix with the base directory, 114 | // replace namespace separators with directory separators 115 | // in the relative class name, append with .php 116 | $file = $base_dir 117 | . str_replace('\\', '/', $relative_class) 118 | . '.php'; 119 | 120 | // if the mapped file exists, require it 121 | if ($this->requireFile($file)) { 122 | // yes, we're done 123 | return $file; 124 | } 125 | } 126 | 127 | // never found it 128 | return false; 129 | } 130 | 131 | /** 132 | * If a file exists, require it from the file system. 133 | * 134 | * @param string $file The file to require. 135 | * @return bool True if the file exists, false if not. 136 | */ 137 | public function requireFile($file) 138 | { 139 | if (file_exists($file)) { 140 | require $file; 141 | return true; 142 | } 143 | return false; 144 | } 145 | } 146 | 147 | $basePath = dirname(__DIR__); 148 | $loader = new Psr4Autoloader(); 149 | $loader->register(); 150 | 151 | // Register laravel-s 152 | $loader->addNamespace('Hhxsv5\LaravelS', $basePath . '/vendor/hhxsv5/laravel-s/src'); 153 | 154 | // Register laravel-s dependencies 155 | 156 | // To fix issue #364 https://github.com/hhxsv5/laravel-s/issues/364 157 | $loader->addNamespace('Symfony\Polyfill\Php80', $basePath . '/vendor/symfony/polyfill-php80'); 158 | $loader->requireFile($basePath . '/vendor/symfony/polyfill-php80/bootstrap.php'); 159 | 160 | $loader->addNamespace('Symfony\Component\Console', $basePath . '/vendor/symfony/console'); 161 | $loader->addNamespace('Symfony\Contracts\Service', $basePath . '/vendor/symfony/service-contracts'); 162 | $loader->addNamespace('Symfony\Contracts', $basePath . '/vendor/symfony/contracts'); 163 | 164 | $command = new Hhxsv5\LaravelS\Console\Portal($basePath); 165 | $input = new Symfony\Component\Console\Input\ArgvInput(); 166 | $output = new Symfony\Component\Console\Output\ConsoleOutput(); 167 | $code = $command->run($input, $output); 168 | exit($code); -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hhxsv5/laravel-s", 3 | "type": "library", 4 | "license": "MIT", 5 | "support": { 6 | "issues": "https://github.com/hhxsv5/laravel-s/issues", 7 | "source": "https://github.com/hhxsv5/laravel-s" 8 | }, 9 | "description": "🚀 LaravelS is an out-of-the-box adapter between Laravel/Lumen and Swoole.", 10 | "keywords": [ 11 | "laravels", 12 | "laravel-s", 13 | "swoole", 14 | "laravel", 15 | "lumen", 16 | "async", 17 | "coroutine", 18 | "server", 19 | "http", 20 | "websocket", 21 | "tcp", 22 | "udp", 23 | "process", 24 | "task", 25 | "timer", 26 | "inotify", 27 | "performance" 28 | ], 29 | "homepage": "https://github.com/hhxsv5/laravel-s", 30 | "authors": [ 31 | { 32 | "name": "Xie Biao", 33 | "email": "hhxsv5@sina.com" 34 | } 35 | ], 36 | "require": { 37 | "php": ">=8.2", 38 | "ext-curl": "*", 39 | "ext-json": "*", 40 | "ext-pcntl": "*", 41 | "swoole/ide-helper": "@dev", 42 | "symfony/console": ">=6.4.0" 43 | }, 44 | "suggest": { 45 | "ext-swoole": "Coroutine-based concurrency library for PHP, require >= 1.7.19.", 46 | "ext-inotify": "Inotify, used to real-time reload." 47 | }, 48 | "autoload": { 49 | "psr-4": { 50 | "Hhxsv5\\LaravelS\\": "src" 51 | } 52 | }, 53 | "extra": { 54 | "laravel": { 55 | "providers": [ 56 | "Hhxsv5\\LaravelS\\Illuminate\\LaravelSServiceProvider" 57 | ] 58 | } 59 | }, 60 | "bin": [ 61 | "bin/fswatch" 62 | ], 63 | "prefer-stable": true, 64 | "minimum-stability": "dev", 65 | "require-dev": { 66 | "phpunit/phpunit": ">=4.8.36" 67 | }, 68 | "autoload-dev": { 69 | "psr-4": { 70 | "Hhxsv5\\LaravelS\\Tests\\": "tests" 71 | } 72 | }, 73 | "scripts": { 74 | "test": "./vendor/bin/phpunit -c phpunit.xml" 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /config/laravels.php: -------------------------------------------------------------------------------- 1 | env('LARAVELS_LISTEN_IP', '127.0.0.1'), 25 | 26 | /* 27 | |-------------------------------------------------------------------------- 28 | | The port of the server 29 | |-------------------------------------------------------------------------- 30 | | 31 | | Require root privilege if port is less than 1024. 32 | | 33 | */ 34 | 35 | 'listen_port' => env('LARAVELS_LISTEN_PORT', 5200), 36 | 37 | /* 38 | |-------------------------------------------------------------------------- 39 | | The socket type of the server 40 | |-------------------------------------------------------------------------- 41 | | 42 | | Usually, you don’t need to care about it. 43 | | Unless you want Nginx to proxy to the UnixSocket Stream file, you need 44 | | to modify it to SWOOLE_SOCK_UNIX_STREAM, and listen_ip is the path of UnixSocket Stream file. 45 | | List of socket types: 46 | | SWOOLE_SOCK_TCP: TCP 47 | | SWOOLE_SOCK_TCP6: TCP IPv6 48 | | SWOOLE_SOCK_UDP: UDP 49 | | SWOOLE_SOCK_UDP6: UDP IPv6 50 | | SWOOLE_UNIX_DGRAM: Unix socket dgram 51 | | SWOOLE_UNIX_STREAM: Unix socket stream 52 | | Enable SSL: $sock_type | SWOOLE_SSL. To enable SSL, check the configuration about SSL. 53 | | https://www.swoole.co.uk/docs/modules/swoole-server-doc 54 | | https://www.swoole.co.uk/docs/modules/swoole-server/configuration 55 | | 56 | */ 57 | 58 | 'socket_type' => defined('SWOOLE_SOCK_TCP') ? SWOOLE_SOCK_TCP : 1, 59 | 60 | /* 61 | |-------------------------------------------------------------------------- 62 | | Server Name 63 | |-------------------------------------------------------------------------- 64 | | 65 | | This value represents the name of the server that will be 66 | | displayed in the header of each request. 67 | | 68 | */ 69 | 70 | 'server' => env('LARAVELS_SERVER', ''), 71 | 72 | /* 73 | |-------------------------------------------------------------------------- 74 | | Handle Static Resource 75 | |-------------------------------------------------------------------------- 76 | | 77 | | Whether handle the static resource by LaravelS(Require Swoole >= 1.7.21, Handle by Swoole if Swoole >= 1.9.17). 78 | | Suggest that Nginx handles the statics and LaravelS handles the dynamics. 79 | | The default path of static resource is base_path('public'), you can modify swoole.document_root to change it. 80 | | 81 | */ 82 | 83 | 'handle_static' => env('LARAVELS_HANDLE_STATIC', false), 84 | 85 | /* 86 | |-------------------------------------------------------------------------- 87 | | Laravel Base Path 88 | |-------------------------------------------------------------------------- 89 | | 90 | | The basic path of Laravel, default base_path(), be used for symbolic link. 91 | | 92 | */ 93 | 94 | 'laravel_base_path' => env('LARAVEL_BASE_PATH', base_path()), 95 | 96 | /* 97 | |-------------------------------------------------------------------------- 98 | | Inotify Reload 99 | |-------------------------------------------------------------------------- 100 | | 101 | | This feature requires inotify extension. 102 | | https://github.com/hhxsv5/laravel-s#automatically-reload-after-modifying-code 103 | | 104 | */ 105 | 106 | 'inotify_reload' => [ 107 | // Whether enable the Inotify Reload to reload all worker processes when your code is modified. 108 | 'enable' => env('LARAVELS_INOTIFY_RELOAD', false), 109 | 110 | // The file path that Inotify watches 111 | 'watch_path' => base_path(), 112 | 113 | // The file types that Inotify watches 114 | 'file_types' => ['.php'], 115 | 116 | // The excluded/ignored directories that Inotify watches 117 | 'excluded_dirs' => [], 118 | 119 | // Whether output the reload log 120 | 'log' => true, 121 | ], 122 | 123 | /* 124 | |-------------------------------------------------------------------------- 125 | | Swoole Event Handlers 126 | |-------------------------------------------------------------------------- 127 | | 128 | | Configure the event callback function of Swoole, key-value format, 129 | | key is the event name, and value is the class that implements the event 130 | | processing interface. 131 | | 132 | | https://github.com/hhxsv5/laravel-s#configuring-the-event-callback-function-of-swoole 133 | | 134 | */ 135 | 136 | 'event_handlers' => [], 137 | 138 | /* 139 | |-------------------------------------------------------------------------- 140 | | WebSockets 141 | |-------------------------------------------------------------------------- 142 | | 143 | | Swoole WebSocket Server settings. 144 | | 145 | | https://github.com/hhxsv5/laravel-s#enable-websocket-server 146 | | 147 | */ 148 | 149 | 'websocket' => [ 150 | 'enable' => false, 151 | // 'handler' => XxxWebSocketHandler::class, 152 | ], 153 | 154 | /* 155 | |-------------------------------------------------------------------------- 156 | | Sockets - multi-port mixed protocol 157 | |-------------------------------------------------------------------------- 158 | | 159 | | The socket(port) list for TCP/UDP. 160 | | 161 | | https://github.com/hhxsv5/laravel-s#multi-port-mixed-protocol 162 | | 163 | */ 164 | 165 | 'sockets' => [], 166 | 167 | /* 168 | |-------------------------------------------------------------------------- 169 | | Custom Process 170 | |-------------------------------------------------------------------------- 171 | | 172 | | Support developers to create custom processes for monitoring, 173 | | reporting, or other special tasks. 174 | | 175 | | https://github.com/hhxsv5/laravel-s#custom-process 176 | | 177 | */ 178 | 179 | 'processes' => [], 180 | 181 | /* 182 | |-------------------------------------------------------------------------- 183 | | Timer 184 | |-------------------------------------------------------------------------- 185 | | 186 | | Wrapper cron job base on Swoole's Millisecond Timer, replace Linux Crontab. 187 | | 188 | | https://github.com/hhxsv5/laravel-s#millisecond-cron-job 189 | | 190 | */ 191 | 192 | 'timer' => [ 193 | 'enable' => env('LARAVELS_TIMER', false), 194 | 195 | // The list of cron job 196 | 'jobs' => [ 197 | // Enable LaravelScheduleJob to run `php artisan schedule:run` every 1 minute, replace Linux Crontab 198 | // Hhxsv5\LaravelS\Illuminate\LaravelScheduleJob::class, 199 | ], 200 | 201 | // Max waiting time of reloading 202 | 'max_wait_time' => 5, 203 | 204 | // Enable the global lock to ensure that only one instance starts the timer 205 | // when deploying multiple instances. 206 | // This feature depends on Redis https://laravel.com/docs/8.x/redis 207 | 'global_lock' => false, 208 | 'global_lock_key' => config('app.name', 'Laravel'), 209 | ], 210 | 211 | /* 212 | |-------------------------------------------------------------------------- 213 | | Swoole Tables 214 | |-------------------------------------------------------------------------- 215 | | 216 | | All defined tables will be created before Swoole starting. 217 | | 218 | | https://github.com/hhxsv5/laravel-s#use-swooletable 219 | | 220 | */ 221 | 222 | 'swoole_tables' => [], 223 | 224 | /* 225 | |-------------------------------------------------------------------------- 226 | | Re-register Providers 227 | |-------------------------------------------------------------------------- 228 | | 229 | | The Service Provider list, will be re-registered each request, and run method boot() 230 | | if it exists. Usually, be used to clear the Service Provider 231 | | which registers Singleton instances. 232 | | 233 | | https://github.com/hhxsv5/laravel-s/blob/master/Settings.md#register_providers 234 | | 235 | */ 236 | 237 | 'register_providers' => [], 238 | 239 | /* 240 | |-------------------------------------------------------------------------- 241 | | Cleaners 242 | |-------------------------------------------------------------------------- 243 | | 244 | | The list of cleaners for each request is used to clean up some residual 245 | | global variables, singleton objects, and static properties to avoid 246 | | data pollution between requests. 247 | | 248 | | https://github.com/hhxsv5/laravel-s/blob/master/Settings.md#cleaners 249 | | 250 | */ 251 | 252 | 'cleaners' => [], 253 | 254 | /* 255 | |-------------------------------------------------------------------------- 256 | | Destroy Controllers 257 | |-------------------------------------------------------------------------- 258 | | 259 | | Automatically destroy the controllers after each request to solve 260 | | the problem of the singleton controllers. 261 | | 262 | | https://github.com/hhxsv5/laravel-s/blob/master/KnownIssues.md#singleton-controller 263 | | 264 | */ 265 | 266 | 'destroy_controllers' => [ 267 | 'enable' => false, 268 | 'excluded_list' => [], 269 | ], 270 | 271 | /* 272 | |-------------------------------------------------------------------------- 273 | | Swoole Settings 274 | |-------------------------------------------------------------------------- 275 | | 276 | | Swoole's original configuration items. 277 | | 278 | | More settings 279 | | Chinese https://wiki.swoole.com/#/server/setting 280 | | English https://www.swoole.co.uk/docs/modules/swoole-server/configuration 281 | | 282 | */ 283 | 284 | 'swoole' => [ 285 | 'daemonize' => env('LARAVELS_DAEMONIZE', false), 286 | 'dispatch_mode' => env('LARAVELS_DISPATCH_MODE', 3), 287 | 'worker_num' => env('LARAVELS_WORKER_NUM', 30), 288 | //'task_worker_num' => env('LARAVELS_TASK_WORKER_NUM', 10), 289 | 'task_ipc_mode' => 1, 290 | 'task_max_request' => env('LARAVELS_TASK_MAX_REQUEST', 100000), 291 | 'task_tmpdir' => @is_writable('/dev/shm/') ? '/dev/shm' : '/tmp', 292 | 'max_request' => env('LARAVELS_MAX_REQUEST', 100000), 293 | 'open_tcp_nodelay' => true, 294 | 'pid_file' => storage_path('laravels.pid'), 295 | 'log_level' => env('LARAVELS_LOG_LEVEL', 4), 296 | 'log_file' => storage_path(sprintf('logs/swoole-%s.log', date('Y-m'))), 297 | 'document_root' => base_path('public'), 298 | 'buffer_output_size' => 2 * 1024 * 1024, 299 | 'socket_buffer_size' => 8 * 1024 * 1024, 300 | 'package_max_length' => 4 * 1024 * 1024, 301 | 'reload_async' => true, 302 | 'max_wait_time' => 60, 303 | 'enable_reuse_port' => true, 304 | 'enable_coroutine' => false, 305 | 'upload_tmp_dir' => @is_writable('/dev/shm/') ? '/dev/shm' : '/tmp', 306 | 'http_compression' => env('LARAVELS_HTTP_COMPRESSION', false), 307 | ], 308 | ]; 309 | -------------------------------------------------------------------------------- /config/prometheus.php: -------------------------------------------------------------------------------- 1 | env('PROMETHEUS_ENABLE', true), 12 | 13 | /* 14 | |-------------------------------------------------------------------------- 15 | | The name of the application 16 | |-------------------------------------------------------------------------- 17 | | Default APP_NAME. 18 | */ 19 | 'application' => env('PROMETHEUS_APP_NAME', env('APP_NAME', 'Laravel')), 20 | 21 | /* 22 | |-------------------------------------------------------------------------- 23 | | The prefix of apcu keys 24 | |-------------------------------------------------------------------------- 25 | | 26 | | Cannot contain any regular expression characters. Default "prom". 27 | | 28 | */ 29 | 'apcu_key_prefix' => env('PROMETHEUS_APCU_KEY_PREFIX', 'prom'), 30 | 31 | /* 32 | |-------------------------------------------------------------------------- 33 | | The separator of apcu keys 34 | |-------------------------------------------------------------------------- 35 | | 36 | | Cannot contain any regular expression characters. Default "::". 37 | | 38 | */ 39 | 'apcu_key_separator' => env('PROMETHEUS_APCU_KEY_SEPARATOR', '::'), 40 | 41 | /* 42 | |-------------------------------------------------------------------------- 43 | | The max age(seconds) of apcu keys. 44 | |-------------------------------------------------------------------------- 45 | | 46 | | It's TTL of apcu keys. Default 259200s(3 days). 47 | | 48 | */ 49 | 'apcu_key_max_age' => env('PROMETHEUS_APCU_KEY_MAX_AGE', 259200), 50 | 51 | /* 52 | |-------------------------------------------------------------------------- 53 | | The time window(seconds) of the maximum duration metric for http_server_requests_seconds_max. 54 | |-------------------------------------------------------------------------- 55 | | 56 | | Default 60s. 57 | | 58 | */ 59 | 'max_duration_time_window' => env('PROMETHEUS_REQUEST_MAX_DURATION_TIME_WINDOW', 60), 60 | 61 | /* 62 | |-------------------------------------------------------------------------- 63 | | The ignored status codes when collecting http requests. 64 | |-------------------------------------------------------------------------- 65 | | 66 | | Default "400,404,405". 67 | | 68 | */ 69 | 'ignored_http_codes' => array_flip(explode(',', env('PROMETHEUS_IGNORED_HTTP_CODES', '400,404,405'))), 70 | 71 | /* 72 | |-------------------------------------------------------------------------- 73 | | The interval of collecting metrics. 74 | |-------------------------------------------------------------------------- 75 | | 76 | | Default 10s. 77 | | 78 | */ 79 | 'collect_metrics_interval' => env('PROMETHEUS_COLLECT_METRICS_INTERVAL', 10), 80 | ]; 81 | -------------------------------------------------------------------------------- /grafana-dashboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hhxsv5/laravel-s/895d015c288d6c412eb0dc983dcd0690b2d8c4bf/grafana-dashboard.png -------------------------------------------------------------------------------- /logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 19 | 20 | 21 | tests 22 | tests/TestCase.php 23 | 24 | 25 | 26 | 27 | tests 28 | 29 | 30 | 31 | 32 | 33 | 34 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /sponsor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hhxsv5/laravel-s/895d015c288d6c412eb0dc983dcd0690b2d8c4bf/sponsor.png -------------------------------------------------------------------------------- /src/Components/Apollo/Client.php: -------------------------------------------------------------------------------- 1 | server = $settings['server']; 31 | $this->appId = $settings['app_id']; 32 | if (isset($settings['cluster'])) { 33 | $this->cluster = $settings['cluster']; 34 | } 35 | if (isset($settings['namespaces'])) { 36 | $this->namespaces = $settings['namespaces']; 37 | } 38 | if (isset($settings['client_ip'])) { 39 | $this->clientIp = $settings['client_ip']; 40 | } else { 41 | $this->clientIp = current(swoole_get_local_ip()) ?: gethostname(); 42 | } 43 | if (isset($settings['pull_timeout'])) { 44 | $this->pullTimeout = (int)$settings['pull_timeout']; 45 | } 46 | if (isset($settings['backup_old_env'])) { 47 | $this->backupOldEnv = (bool)$settings['backup_old_env']; 48 | } 49 | } 50 | 51 | public static function putCommandOptionsToEnv(array $options) 52 | { 53 | $envs = [ 54 | 'ENABLE_APOLLO' => !empty($options['enable-apollo']), 55 | 'APOLLO_SERVER' => $options['apollo-server'], 56 | 'APOLLO_APP_ID' => $options['apollo-app-id'], 57 | 'APOLLO_CLUSTER' => $options['apollo-cluster'], 58 | 'APOLLO_NAMESPACES' => implode(',', $options['apollo-namespaces']), 59 | 'APOLLO_CLIENT_IP' => $options['apollo-client-ip'], 60 | 'APOLLO_PULL_TIMEOUT' => $options['apollo-pull-timeout'], 61 | 'APOLLO_BACKUP_OLD_ENV' => $options['apollo-backup-old-env'], 62 | ]; 63 | foreach ($envs as $key => $value) { 64 | putenv("{$key}={$value}"); 65 | } 66 | } 67 | 68 | public static function createFromEnv() 69 | { 70 | if (!getenv('APOLLO_SERVER') || !getenv('APOLLO_APP_ID')) { 71 | throw new \InvalidArgumentException('Missing environment variable APOLLO_SERVER or APOLLO_APP_ID'); 72 | } 73 | $settings = [ 74 | 'server' => getenv('APOLLO_SERVER'), 75 | 'app_id' => getenv('APOLLO_APP_ID'), 76 | 'cluster' => ($cluster = (string)getenv('APOLLO_CLUSTER')) !== '' ? $cluster : null, 77 | 'namespaces' => ($namespaces = (string)getenv('APOLLO_NAMESPACES')) !== '' ? explode(',', $namespaces) : null, 78 | 'client_ip' => ($clientIp = (string)getenv('APOLLO_CLIENT_IP')) !== '' ? $clientIp : null, 79 | 'pull_timeout' => ($pullTimeout = (int)getenv('APOLLO_PULL_TIMEOUT')) > 0 ? $pullTimeout : null, 80 | 'backup_old_env' => ($backupOldEnv = (bool)getenv('APOLLO_BACKUP_OLD_ENV')) ? $backupOldEnv : null, 81 | ]; 82 | return new static($settings); 83 | } 84 | 85 | public static function createFromCommandOptions(array $options) 86 | { 87 | if (!isset($options['apollo-server'], $options['apollo-app-id'])) { 88 | throw new \InvalidArgumentException('Missing command option apollo-server or apollo-app-id'); 89 | } 90 | $settings = [ 91 | 'server' => $options['apollo-server'], 92 | 'app_id' => $options['apollo-app-id'], 93 | 'cluster' => isset($options['apollo-cluster']) && $options['apollo-cluster'] !== '' ? $options['apollo-cluster'] : null, 94 | 'namespaces' => !empty($options['apollo-namespaces']) ? $options['apollo-namespaces'] : null, 95 | 'client_ip' => isset($options['apollo-client-ip']) && $options['apollo-client-ip'] !== '' ? $options['apollo-client-ip'] : null, 96 | 'pull_timeout' => isset($options['apollo-pull-timeout']) ? (int)$options['apollo-pull-timeout'] : null, 97 | 'backup_old_env' => isset($options['apollo-backup-old-env']) ? (bool)$options['apollo-backup-old-env'] : null, 98 | ]; 99 | return new static($settings); 100 | } 101 | 102 | public static function attachCommandOptions(Command $command) 103 | { 104 | $command->addOption('enable-apollo', null, InputOption::VALUE_NONE, 'Whether to enable Apollo component'); 105 | $command->addOption('apollo-server', null, InputOption::VALUE_OPTIONAL, 'Apollo server URL'); 106 | $command->addOption('apollo-app-id', null, InputOption::VALUE_OPTIONAL, 'Apollo APP ID'); 107 | $command->addOption('apollo-namespaces', null, InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, 'The namespace to which the APP belongs'); 108 | $command->addOption('apollo-cluster', null, InputOption::VALUE_OPTIONAL, 'The cluster to which the APP belongs'); 109 | $command->addOption('apollo-client-ip', null, InputOption::VALUE_OPTIONAL, 'IP of current instance'); 110 | $command->addOption('apollo-pull-timeout', null, InputOption::VALUE_OPTIONAL, 'Timeout time(seconds) when pulling configuration'); 111 | $command->addOption('apollo-backup-old-env', null, InputOption::VALUE_NONE, 'Whether to backup the old configuration file when updating the configuration .env file'); 112 | } 113 | 114 | public function pullBatch(array $namespaces, $withReleaseKey = false, array $options = []) 115 | { 116 | $configs = []; 117 | $uri = sprintf('%s/configs/%s/%s/', $this->server, $this->appId, $this->cluster); 118 | foreach ($namespaces as $namespace) { 119 | $url = $uri . $namespace . '?' . http_build_query([ 120 | 'releaseKey' => $withReleaseKey && isset($this->releaseKeys[$namespace]) ? $this->releaseKeys[$namespace] : null, 121 | 'ip' => $this->clientIp, 122 | ]); 123 | $timeout = isset($options['timeout']) ? $options['timeout'] : $this->pullTimeout; 124 | $response = $this->httpGet($url, compact('timeout')); 125 | if ($response['statusCode'] === 200) { 126 | $json = json_decode($response['body'], true); 127 | if (is_array($json)) { 128 | $configs[$namespace] = $json; 129 | $this->releaseKeys[$namespace] = $configs[$namespace]['releaseKey']; 130 | } 131 | } elseif ($response['statusCode'] === 304) { 132 | // ignore 304 133 | } 134 | 135 | } 136 | return $configs; 137 | } 138 | 139 | public function pullAll($withReleaseKey = false, array $options = []) 140 | { 141 | return $this->pullBatch($this->namespaces, $withReleaseKey, $options); 142 | } 143 | 144 | public function pullAllAndSave($filepath, array $options = []) 145 | { 146 | $all = $this->pullAll(false, $options); 147 | if (count($all) !== count($this->namespaces)) { 148 | $lackNamespaces = array_diff($this->namespaces, array_keys($all)); 149 | throw new \RuntimeException('Missing Apollo configurations for namespaces ' . implode(',', $lackNamespaces)); 150 | } 151 | $configs = []; 152 | foreach ($all as $namespace => $config) { 153 | $configs[] = '# Namespace: ' . $config['namespaceName']; 154 | ksort($config['configurations']); 155 | foreach ($config['configurations'] as $key => $value) { 156 | $key = preg_replace('/[^a-zA-Z0-9_.]/', '_', $key); 157 | $configs[] = sprintf('%s=%s', $key, $value); 158 | } 159 | } 160 | if (empty($configs)) { 161 | throw new \RuntimeException('Empty Apollo configuration list'); 162 | } 163 | if ($this->backupOldEnv && file_exists($filepath)) { 164 | rename($filepath, $filepath . '.' . date('YmdHis')); 165 | } 166 | $fileContent = implode(PHP_EOL, $configs); 167 | if (Context::inCoroutine()) { 168 | Coroutine::writeFile($filepath, $fileContent); 169 | } else { 170 | file_put_contents($filepath, $fileContent); 171 | } 172 | return $configs; 173 | } 174 | 175 | public function startWatchNotification(callable $callback, array $options = []) 176 | { 177 | if (!isset($options['timeout']) || $options['timeout'] < 60) { 178 | $options['timeout'] = 70; 179 | } 180 | $this->watching = true; 181 | $this->notifications = []; 182 | foreach ($this->namespaces as $namespace) { 183 | $this->notifications[$namespace] = ['namespaceName' => $namespace, 'notificationId' => -1]; 184 | } 185 | while ($this->watching) { 186 | $url = sprintf('%s/notifications/v2?%s', 187 | $this->server, 188 | http_build_query([ 189 | 'appId' => $this->appId, 190 | 'cluster' => $this->cluster, 191 | 'notifications' => json_encode(array_values($this->notifications)), 192 | ]) 193 | ); 194 | $response = $this->httpGet($url, $options); 195 | 196 | if ($response['statusCode'] === 200) { 197 | $notifications = json_decode($response['body'], true); 198 | if (empty($notifications)) { 199 | continue; 200 | } 201 | if (!empty($this->notifications) && current($this->notifications)['notificationId'] !== -1) { // Ignore the first pull 202 | $callback($notifications); 203 | } 204 | array_walk($notifications, function (&$notification) { 205 | unset($notification['messages']); 206 | }); 207 | $this->notifications = array_merge($this->notifications, array_column($notifications, null, 'namespaceName')); 208 | } elseif ($response['statusCode'] === 304) { 209 | // ignore 304 210 | } 211 | } 212 | } 213 | 214 | public function stopWatchNotification() 215 | { 216 | $this->watching = false; 217 | } 218 | } 219 | -------------------------------------------------------------------------------- /src/Components/Apollo/Process.php: -------------------------------------------------------------------------------- 1 | [ 21 | 'class' => static::class, 22 | 'redirect' => false, 23 | 'pipe' => 0, 24 | 'enable' => (bool)getenv('ENABLE_APOLLO'), 25 | ], 26 | ]; 27 | } 28 | 29 | public static function callback(Server $swoole, SwooleProcess $process) 30 | { 31 | $filename = base_path('.env'); 32 | if (isset($_ENV['_ENV'])) { 33 | $filename .= '.' . $_ENV['_ENV']; 34 | } 35 | 36 | self::$apollo = Client::createFromEnv(); 37 | self::$apollo->startWatchNotification(function (array $notifications) use ($process, $filename) { 38 | $configs = self::$apollo->pullAllAndSave($filename); 39 | app('log')->info('[ApolloProcess] Pull all configurations', $configs); 40 | Portal::runLaravelSCommand(base_path(), 'reload'); 41 | if (Context::inCoroutine()) { 42 | Coroutine::sleep(5); 43 | } else { 44 | sleep(5); 45 | } 46 | }); 47 | } 48 | 49 | public static function onReload(Server $swoole, SwooleProcess $process) 50 | { 51 | // Stop the process... 52 | self::$apollo->stopWatchNotification(); 53 | } 54 | } -------------------------------------------------------------------------------- /src/Components/HttpClient/SimpleHttpTrait.php: -------------------------------------------------------------------------------- 1 | true, 13 | CURLOPT_FOLLOWLOCATION => true, 14 | CURLOPT_RETURNTRANSFER => true, 15 | 16 | //int 17 | CURLOPT_MAXREDIRS => 3, 18 | CURLOPT_TIMEOUT => 5, 19 | CURLOPT_CONNECTTIMEOUT => 3, 20 | ]; 21 | 22 | /** 23 | * Sends a GET request and returns a array response. 24 | * @param string $url 25 | * @param array $options 26 | * @return array 27 | */ 28 | public function httpGet($url, array $options) 29 | { 30 | if (Context::inCoroutine()) { 31 | $parts = parse_url($url); 32 | $path = isset($parts['path']) ? $parts['path'] : '/'; 33 | if (isset($parts['query'])) { 34 | $path .= '?' . $parts['query']; 35 | } 36 | if (isset($parts['fragment'])) { 37 | $path .= '#' . $parts['fragment']; 38 | } 39 | $client = new CoroutineClient($parts['host'], isset($parts['port']) ? $parts['port'] : 80, isset($parts['scheme']) && $parts['scheme'] === 'https'); 40 | if (isset($options['timeout'])) { 41 | $client->set([ 42 | 'timeout' => $options['timeout'], 43 | ]); 44 | } 45 | $client->get($path); 46 | $client->close(); 47 | if ($client->errCode === 110) { 48 | return ['statusCode' => 0, 'headers' => [], 'body' => '']; 49 | } 50 | if ($client->errCode !== 0) { 51 | $msg = sprintf('Failed to send Http request(%s), errcode=%d, errmsg=%s', $url, $client->errCode, $client->errMsg); 52 | throw new \RuntimeException($msg, $client->errCode); 53 | } 54 | return ['statusCode' => $client->statusCode, 'headers' => $client->headers, 'body' => $client->body]; 55 | } 56 | 57 | $handle = curl_init(); 58 | $finalOptions = [ 59 | CURLOPT_URL => $url, 60 | CURLOPT_HTTPGET => true, 61 | ] + $this->curlOptions; 62 | if (isset($options['timeout'])) { 63 | $finalOptions[CURLOPT_TIMEOUT] = $options['timeout']; 64 | } 65 | curl_setopt_array($handle, $finalOptions); 66 | $responseStr = curl_exec($handle); 67 | $errno = curl_errno($handle); 68 | $errmsg = curl_error($handle); 69 | // Fix: curl_errno() always return 0 when fail 70 | if ($errno !== 0 || $errmsg !== '') { 71 | curl_close($handle); 72 | $msg = sprintf('Failed to send Http request(%s), errcode=%d, errmsg=%s', $url, $errno, $errmsg); 73 | throw new \RuntimeException($msg, $errno); 74 | } 75 | 76 | $headerSize = curl_getinfo($handle, CURLINFO_HEADER_SIZE); 77 | $statusCode = curl_getinfo($handle, CURLINFO_HTTP_CODE); 78 | curl_close($handle); 79 | 80 | $header = substr($responseStr, 0, $headerSize); 81 | $body = substr($responseStr, $headerSize); 82 | $lines = explode("\n", $header); 83 | array_shift($lines); // Remove status 84 | 85 | $headers = []; 86 | foreach ($lines as $part) { 87 | $middle = explode(':', $part); 88 | $key = trim($middle[0]); 89 | if ($key === '') { 90 | continue; 91 | } 92 | if (isset($headers[$key])) { 93 | $headers[$key] = (array)$headers[$key]; 94 | $headers[$key][] = isset($middle[1]) ? trim($middle[1]) : ''; 95 | } else { 96 | $headers[$key] = isset($middle[1]) ? trim($middle[1]) : ''; 97 | } 98 | } 99 | return ['statusCode' => $statusCode, 'headers' => $headers, 'body' => $body]; 100 | } 101 | } -------------------------------------------------------------------------------- /src/Components/MetricCollector.php: -------------------------------------------------------------------------------- 1 | config = $config; 12 | } 13 | } -------------------------------------------------------------------------------- /src/Components/MetricCollectorInterface.php: -------------------------------------------------------------------------------- 1 | [ 21 | 'class' => static::class, 22 | 'redirect' => false, 23 | 'pipe' => 0, 24 | 'enable' => (bool)config('prometheus.enable', true), 25 | ], 26 | ]; 27 | } 28 | 29 | public static function callback(Server $swoole, Process $process) 30 | { 31 | /**@var SwooleProcessCollector $processCollector */ 32 | $processCollector = app(SwooleProcessCollector::class); 33 | /**@var SwooleStatsCollector $swooleStatsCollector */ 34 | $swooleStatsCollector = app(SwooleStatsCollector::class); 35 | /**@var SystemCollector $systemCollector */ 36 | $systemCollector = app(SystemCollector::class); 37 | $workerNum = $swoole->setting['worker_num']; 38 | $taskWorkerNum = isset($swoole->setting['task_worker_num']) ? $swoole->setting['task_worker_num'] : 0; 39 | $totalNum = $workerNum + $taskWorkerNum - 1; 40 | $workerIds = range(0, $totalNum); 41 | $runJob = function () use ($swoole, $workerIds, $processCollector, $swooleStatsCollector, $systemCollector) { 42 | // Collect the metrics of Swoole stats() 43 | $swooleStatsCollector->collect(); 44 | 45 | // Collect the metrics of system 46 | $systemCollector->collect(); 47 | 48 | // Collect the metrics of all workers 49 | foreach ($workerIds as $workerId) { 50 | $swoole->sendMessage($processCollector, $workerId); 51 | } 52 | }; 53 | 54 | $interval = config('prometheus.collect_metrics_interval', 10) * 1000; 55 | self::$timerId = Timer::tick($interval, $runJob); 56 | } 57 | 58 | public static function onReload(Server $swoole, Process $process) 59 | { 60 | Timer::clear(self::$timerId); 61 | } 62 | 63 | public static function onStop(Server $swoole, Process $process) 64 | { 65 | Timer::clear(self::$timerId); 66 | } 67 | } -------------------------------------------------------------------------------- /src/Components/Prometheus/Collectors/HttpRequestCollector.php: -------------------------------------------------------------------------------- 1 | config['max_duration_time_window'])) { 21 | $this->config['max_duration_time_window'] = 60; 22 | } 23 | 24 | $routes = method_exists(app(), 'getRoutes') ? app()->getRoutes() : app('router')->getRoutes(); 25 | if ($routes instanceof \Illuminate\Routing\RouteCollection) { // Laravel 26 | foreach ($routes->getRoutes() as $route) { 27 | $method = $route->methods()[0]; 28 | $uri = '/' . ltrim($route->uri(), '/'); 29 | $this->routes[$method . $uri] = $uri; 30 | 31 | $action = $route->getAction(); 32 | if (is_string($action['uses'])) { // Uses 33 | $this->routesByUses[$method . $action['uses']] = $uri; 34 | } elseif ($action['uses'] instanceof Closure) { // Closure 35 | $objectId = spl_object_hash($action['uses']); 36 | $this->routesByClosure[$method . $objectId] = $uri; 37 | } 38 | } 39 | } elseif (is_array($routes)) { // Lumen 40 | $this->routes = $routes; 41 | foreach ($routes as $route) { 42 | if (isset($route['action']['uses'])) { // Uses 43 | $this->routesByUses[$route['method'] . $route['action']['uses']] = $route['uri']; 44 | } 45 | if (isset($route['action'][0]) && $route['action'][0] instanceof Closure) { // Closure 46 | $objectId = spl_object_hash($route['action'][0]); 47 | $this->routesByClosure[$route['method'] . $objectId] = $route['uri']; 48 | } 49 | } 50 | } 51 | } 52 | 53 | public function collect(array $params = []) 54 | { 55 | if (!$this->config['enable']) { 56 | return; 57 | } 58 | 59 | /**@var \Illuminate\Http\Request $request */ 60 | /**@var \Illuminate\Http\Response $response */ 61 | list($request, $response) = $params; 62 | 63 | $status = $response->getStatusCode(); 64 | if (isset($this->config['ignored_http_codes'][$status])) { 65 | // Ignore the requests. 66 | return; 67 | } 68 | 69 | $uri = $this->findRouteUri($request); 70 | $cost = (int)round((microtime(true) - $request->server('REQUEST_TIME_FLOAT')) * 1000000); // Time unit: μs 71 | 72 | // Http Request Stats 73 | $requestLabels = http_build_query([ 74 | 'method' => $request->getMethod(), 75 | 'uri' => $uri, 76 | 'status' => $status, 77 | ]); 78 | 79 | // Key Format: prefix+metric_name+metric_type+metric_labels 80 | $countKey = implode($this->config['apcu_key_separator'], [$this->config['apcu_key_prefix'], 'http_server_requests_seconds_count', 'summary', $requestLabels]); 81 | $sumKey = implode($this->config['apcu_key_separator'], [$this->config['apcu_key_prefix'], 'http_server_requests_seconds_sum', 'summary', $requestLabels]); 82 | $maxKey = implode($this->config['apcu_key_separator'], [$this->config['apcu_key_prefix'], 'http_server_requests_seconds_max', 'gauge', $requestLabels]); 83 | apcu_inc($countKey, 1, $success, $this->config['apcu_key_max_age']); 84 | apcu_inc($sumKey, $cost, $success, $this->config['apcu_key_max_age']); 85 | 86 | $round = 0; 87 | do { 88 | $round++; 89 | $lastCost = apcu_fetch($maxKey); 90 | if ($lastCost === false) { 91 | if (!apcu_add($maxKey, $cost, $this->config['max_duration_time_window'])) { 92 | continue; 93 | } 94 | break; 95 | } 96 | if ($cost <= $lastCost) { 97 | break; 98 | } 99 | if (apcu_cas($maxKey, $lastCost, $cost)) { 100 | break; 101 | } 102 | } while ($round <= self::MAX_ROUND); 103 | } 104 | 105 | protected function findRouteUri(Request $request) 106 | { 107 | $method = $request->getMethod(); 108 | $uri = $request->getPathInfo(); 109 | $key = $method . $uri; 110 | if (isset($this->routes[$key])) { 111 | return $uri; 112 | } 113 | 114 | $route = $request->route(); 115 | if ($route instanceof \Illuminate\Routing\Route) { // Laravel 116 | $uri = $route->uri(); 117 | } elseif (is_array($route)) { // Lumen 118 | if (isset($route[1]['uses'])) { 119 | $key = $method . $route[1]['uses']; 120 | if (isset($this->routesByUses[$key])) { 121 | $uri = $this->routesByUses[$key]; 122 | } 123 | } elseif (isset($route[1][0]) && $route[1][0] instanceof Closure) { 124 | $key = $method . spl_object_hash($route[1][0]); 125 | if (isset($this->routesByClosure[$key])) { 126 | $uri = $this->routesByClosure[$key]; 127 | } 128 | } 129 | } 130 | return $uri; 131 | } 132 | } -------------------------------------------------------------------------------- /src/Components/Prometheus/Collectors/SwooleProcessCollector.php: -------------------------------------------------------------------------------- 1 | $params['process_id'], 14 | 'process_type' => $params['process_type'], 15 | ]); 16 | 17 | // Memory Usage 18 | $memoryMetrics = [ 19 | [ 20 | 'name' => 'swoole_process_memory_usage', 21 | 'type' => 'gauge', 22 | 'value' => memory_get_usage(), 23 | ], 24 | [ 25 | 'name' => 'swoole_process_memory_real_usage', 26 | 'type' => 'gauge', 27 | 'value' => memory_get_usage(true), 28 | ], 29 | ]; 30 | 31 | // GC Status 32 | $gcMetrics = []; 33 | if (PHP_VERSION_ID >= 70300) { 34 | $gcStatus = gc_status(); 35 | $gcMetrics = [ 36 | [ 37 | 'name' => 'swoole_process_gc_runs', 38 | 'type' => 'gauge', 39 | 'value' => $gcStatus['runs'], 40 | ], 41 | [ 42 | 'name' => 'swoole_process_gc_collected', 43 | 'type' => 'gauge', 44 | 'value' => $gcStatus['collected'], 45 | ], 46 | [ 47 | 'name' => 'swoole_process_gc_threshold', 48 | 'type' => 'gauge', 49 | 'value' => $gcStatus['threshold'], 50 | ], 51 | [ 52 | 'name' => 'swoole_process_gc_roots', 53 | 'type' => 'gauge', 54 | 'value' => $gcStatus['roots'], 55 | ], 56 | ]; 57 | } 58 | $apcuKey = implode($this->config['apcu_key_separator'], [$this->config['apcu_key_prefix'], 'swoole_process_stats', '', $labels]); 59 | apcu_store($apcuKey, array_merge($memoryMetrics, $gcMetrics), $this->config['apcu_key_max_age']); 60 | } 61 | } -------------------------------------------------------------------------------- /src/Components/Prometheus/Collectors/SwooleStatsCollector.php: -------------------------------------------------------------------------------- 1 | stats(); 14 | // Get worker_num/task_worker_num from setting for the old Swoole. 15 | $setting = $swoole->setting; 16 | if (!isset($stats['worker_num'])) { 17 | $stats['worker_num'] = $setting['worker_num']; 18 | } 19 | if (!isset($stats['task_worker_num'])) { 20 | $stats['task_worker_num'] = isset($setting['task_worker_num']) ? $setting['task_worker_num'] : 0; 21 | } 22 | $metrics = [ 23 | [ 24 | 'name' => 'swoole_cpu_num', 25 | 'type' => 'gauge', 26 | 'value' => swoole_cpu_num(), 27 | ], 28 | [ 29 | 'name' => 'swoole_start_time', 30 | 'type' => 'gauge', 31 | 'value' => $stats['start_time'], 32 | ], 33 | [ 34 | 'name' => 'swoole_connection_num', 35 | 'type' => 'gauge', 36 | 'value' => $stats['connection_num'], 37 | ], 38 | [ 39 | 'name' => 'swoole_request_count', 40 | 'type' => 'gauge', 41 | 'value' => $stats['request_count'], 42 | ], 43 | [ 44 | 'name' => 'swoole_worker_num', 45 | 'type' => 'gauge', 46 | 'value' => $stats['worker_num'], 47 | ], 48 | [ 49 | 'name' => 'swoole_idle_worker_num', 50 | 'type' => 'gauge', 51 | 'value' => isset($stats['idle_worker_num']) ? $stats['idle_worker_num'] : 0, 52 | ], 53 | [ 54 | 'name' => 'swoole_task_worker_num', 55 | 'type' => 'gauge', 56 | 'value' => $stats['task_worker_num'], 57 | ], 58 | [ 59 | 'name' => 'swoole_task_idle_worker_num', 60 | 'type' => 'gauge', 61 | 'value' => isset($stats['task_idle_worker_num']) ? $stats['task_idle_worker_num'] : 0, 62 | ], 63 | [ 64 | 'name' => 'swoole_tasking_num', 65 | 'type' => 'gauge', 66 | 'value' => isset($stats['tasking_num']) ? $stats['tasking_num'] : 0, 67 | ], 68 | ]; 69 | $key = implode($this->config['apcu_key_separator'], [$this->config['apcu_key_prefix'], 'swoole_stats', '', '']); 70 | apcu_store($key, $metrics, $this->config['apcu_key_max_age']); 71 | } 72 | } -------------------------------------------------------------------------------- /src/Components/Prometheus/Collectors/SystemCollector.php: -------------------------------------------------------------------------------- 1 | 'system_load_average_1m', 15 | 'type' => 'gauge', 16 | 'value' => $load[0], 17 | ], 18 | [ 19 | 'name' => 'system_load_average_5m', 20 | 'type' => 'gauge', 21 | 'value' => $load[1], 22 | ], 23 | [ 24 | 'name' => 'system_load_average_15m', 25 | 'type' => 'gauge', 26 | 'value' => $load[2], 27 | ], 28 | ]; 29 | $key = implode($this->config['apcu_key_separator'], [$this->config['apcu_key_prefix'], 'system_stats', '', '']); 30 | apcu_store($key, $metrics, $this->config['apcu_key_max_age']); 31 | } 32 | } -------------------------------------------------------------------------------- /src/Components/Prometheus/Exporter.php: -------------------------------------------------------------------------------- 1 | config = $config; 14 | } 15 | 16 | public function getMetrics() 17 | { 18 | $apcSmaInfo = apcu_sma_info(true); 19 | $metrics = [ 20 | [ 21 | 'name' => 'apcu_seg_size', 22 | 'help' => '', 23 | 'type' => 'gauge', 24 | 'value' => $apcSmaInfo['seg_size'], 25 | ], 26 | [ 27 | 'name' => 'apcu_avail_mem', 28 | 'help' => '', 29 | 'type' => 'gauge', 30 | 'value' => $apcSmaInfo['avail_mem'], 31 | ], 32 | ]; 33 | foreach (new \APCuIterator('/^' . $this->config['apcu_key_prefix'] . $this->config['apcu_key_separator'] . '/') as $item) { 34 | $parts = explode($this->config['apcu_key_separator'], $item['key']); 35 | parse_str($parts[3], $labels); 36 | if (is_array($item['value'])) { 37 | foreach ($item['value'] as $metric) { 38 | $metrics[] = [ 39 | 'name' => $metric['name'], 40 | 'help' => '', 41 | 'type' => $metric['type'], 42 | 'value' => $metric['value'], 43 | 'labels' => $labels, 44 | ]; 45 | } 46 | } else { 47 | $metrics[] = [ 48 | 'name' => $parts[1], 49 | 'help' => '', 50 | 'type' => $parts[2], 51 | 'value' => $item['value'], 52 | 'labels' => $labels, 53 | ]; 54 | } 55 | } 56 | return $metrics; 57 | } 58 | 59 | public function render() 60 | { 61 | $defaultLabels = ['application' => $this->config['application']]; 62 | $metrics = $this->getMetrics(); 63 | $lines = []; 64 | foreach ($metrics as $metric) { 65 | $lines[] = "# HELP " . $metric['name'] . " {$metric['help']}"; 66 | $lines[] = "# TYPE " . $metric['name'] . " {$metric['type']}"; 67 | 68 | $metricLabels = isset($metric['labels']) ? $metric['labels'] : []; 69 | $labels = ['{']; 70 | $allLabels = array_merge($defaultLabels, $metricLabels); 71 | foreach ($allLabels as $key => $value) { 72 | $labels[] = "{$key}=\"{$value}\","; 73 | } 74 | $labels[] = '}'; 75 | $labelStr = implode('', $labels); 76 | $lines[] = $metric['name'] . "$labelStr {$metric['value']}"; 77 | } 78 | return implode("\n", $lines); 79 | } 80 | } -------------------------------------------------------------------------------- /src/Components/Prometheus/RequestMiddleware.php: -------------------------------------------------------------------------------- 1 | collector = $collector; 15 | } 16 | 17 | /** 18 | * Handle an incoming request. 19 | * 20 | * @param \Illuminate\Http\Request $request 21 | * @param \Closure $next 22 | * @return mixed 23 | */ 24 | public function handle($request, Closure $next) 25 | { 26 | return $next($request); 27 | } 28 | 29 | /** 30 | * Handle tasks after the response has been sent to the browser. 31 | * 32 | * @param \Illuminate\Http\Request $request 33 | * @param \Illuminate\Http\Response $response 34 | * @return void 35 | */ 36 | public function terminate($request, $response) 37 | { 38 | try { 39 | $this->collector->collect([$request, $response]); 40 | } catch (\Exception $e) { 41 | app('log')->error('PrometheusMiddleware: failed to collect request metrics.', ['exception' => $e]); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Components/Prometheus/ServiceProvider.php: -------------------------------------------------------------------------------- 1 | publishes([ 18 | __DIR__ . '/../../../config/prometheus.php' => base_path('config/prometheus.php'), 19 | ]); 20 | } 21 | 22 | public function register() 23 | { 24 | $this->mergeConfigFrom( 25 | __DIR__ . '/../../../config/prometheus.php', 'prometheus' 26 | ); 27 | $this->app->singleton(RequestMiddleware::class, function ($app) { 28 | return new RequestMiddleware($app->make(HttpRequestCollector::class)); 29 | }); 30 | $this->app->singleton(HttpRequestCollector::class, function ($app) { 31 | return new HttpRequestCollector($app['config']->get('prometheus')); 32 | }); 33 | $this->app->singleton(SwooleProcessCollector::class, function ($app) { 34 | return new SwooleProcessCollector($app['config']->get('prometheus')); 35 | }); 36 | $this->app->singleton(SwooleStatsCollector::class, function ($app) { 37 | return new SwooleStatsCollector($app['config']->get('prometheus')); 38 | }); 39 | $this->app->singleton(SystemCollector::class, function ($app) { 40 | return new SystemCollector($app['config']->get('prometheus')); 41 | }); 42 | $this->app->singleton(Exporter::class, function ($app) { 43 | return new Exporter($app['config']->get('prometheus')); 44 | }); 45 | } 46 | 47 | public function provides() 48 | { 49 | return [ 50 | RequestMiddleware::class, 51 | HttpRequestCollector::class, 52 | SwooleProcessCollector::class, 53 | SwooleStatsCollector::class, 54 | SystemCollector::class, 55 | Exporter::class, 56 | ]; 57 | } 58 | } -------------------------------------------------------------------------------- /src/Components/Prometheus/TimerProcessMetricsCronJob.php: -------------------------------------------------------------------------------- 1 | collect([ 25 | 'process_id' => 'timer', 26 | 'process_type' => 'timer', 27 | ]); 28 | } 29 | } -------------------------------------------------------------------------------- /src/Illuminate/CleanerManager.php: -------------------------------------------------------------------------------- 1 | currentApp = $currentApp; 65 | $this->snapshotApp = $snapshotApp; 66 | $this->reflectionApp = new ReflectionApp($this->currentApp); 67 | $this->config = $config; 68 | $this->registerCleaners(isset($this->config['cleaners']) ? $this->config['cleaners'] : []); 69 | $this->registerCleanProviders(isset($config['register_providers']) ? $config['register_providers'] : []); 70 | $this->registerCleanControllerWhiteList(isset($this->config['destroy_controllers']['excluded_list']) ? $this->config['destroy_controllers']['excluded_list'] : []); 71 | } 72 | 73 | /** 74 | * Register singleton cleaners to application container. 75 | * @param array $cleaners 76 | */ 77 | protected function registerCleaners(array $cleaners) 78 | { 79 | $this->cleaners = array_unique(array_merge($cleaners, $this->cleaners)); 80 | foreach ($this->cleaners as $class) { 81 | $this->currentApp->singleton($class, function () use ($class) { 82 | $cleaner = new $class($this->currentApp, $this->snapshotApp); 83 | if (!($cleaner instanceof BaseCleaner)) { 84 | throw new \InvalidArgumentException(sprintf( 85 | '%s must extend the abstract class %s', 86 | $cleaner, 87 | BaseCleaner::class 88 | ) 89 | ); 90 | } 91 | return $cleaner; 92 | }); 93 | } 94 | } 95 | 96 | /** 97 | * Clean app after request finished. 98 | */ 99 | public function clean() 100 | { 101 | foreach ($this->cleaners as $class) { 102 | /**@var BaseCleaner $cleaner */ 103 | $cleaner = $this->currentApp->make($class); 104 | $cleaner->clean(); 105 | } 106 | } 107 | 108 | /** 109 | * Register providers for cleaning. 110 | * 111 | * @param array providers 112 | */ 113 | protected function registerCleanProviders(array $providers = []) 114 | { 115 | $this->providers = $providers; 116 | } 117 | 118 | /** 119 | * Clean Providers. 120 | */ 121 | public function cleanProviders() 122 | { 123 | $loadedProviders = $this->reflectionApp->loadedProviders(); 124 | 125 | foreach ($this->providers as $provider) { 126 | if (class_exists($provider)) { 127 | if ($this->config['is_lumen']) { 128 | unset($loadedProviders[get_class(new $provider($this->currentApp))]); 129 | } 130 | 131 | switch ($this->reflectionApp->registerMethodParameterCount()) { 132 | case 1: 133 | $this->currentApp->register($provider); 134 | break; 135 | case 2: 136 | $this->currentApp->register($provider, true); 137 | break; 138 | case 3: 139 | $this->currentApp->register($provider, [], true); 140 | break; 141 | default: 142 | throw new \RuntimeException('The number of parameters of the register method is unknown.'); 143 | } 144 | } 145 | } 146 | 147 | if ($this->config['is_lumen']) { 148 | $this->reflectionApp->setLoadedProviders($loadedProviders); 149 | } 150 | } 151 | 152 | /** 153 | * Register white list of controllers for cleaning. 154 | * 155 | * @param array providers 156 | */ 157 | protected function registerCleanControllerWhiteList(array $controllers = []) 158 | { 159 | $controllers = array_unique($controllers); 160 | $this->whiteListControllers = array_combine($controllers, $controllers); 161 | } 162 | 163 | /** 164 | * Clean controllers. 165 | */ 166 | public function cleanControllers() 167 | { 168 | if ($this->config['is_lumen']) { 169 | return; 170 | } 171 | 172 | if (empty($this->config['destroy_controllers']['enable'])) { 173 | return; 174 | } 175 | 176 | /**@var \Illuminate\Routing\Route $route */ 177 | $route = $this->currentApp['router']->current(); 178 | if (!$route) { 179 | return; 180 | } 181 | 182 | if (isset($route->controller)) { // For Laravel 5.4+ 183 | if (empty($this->whiteListControllers) || !isset($this->whiteListControllers[get_class($route->controller)])) { 184 | unset($route->controller); 185 | } 186 | } else { 187 | $reflection = new \ReflectionClass(get_class($route)); 188 | if ($reflection->hasProperty('controller')) { // Laravel 5.3 189 | $controller = $reflection->getProperty('controller'); 190 | $controller->setAccessible(true); 191 | if (empty($this->whiteListControllers) || (($instance = $controller->getValue($route)) && !isset($this->whiteListControllers[get_class($instance)]))) { 192 | $controller->setValue($route, null); 193 | } 194 | } 195 | } 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /src/Illuminate/Cleaners/AuthCleaner.php: -------------------------------------------------------------------------------- 1 | currentApp['auth'])) { 18 | return; 19 | } 20 | $ref = new \ReflectionObject($this->currentApp['auth']); 21 | if ($ref->hasProperty('guards')) { 22 | $this->guards = $ref->getProperty('guards'); 23 | } else { 24 | $this->guards = $ref->getProperty('drivers'); 25 | } 26 | $this->guards->setAccessible(true); 27 | } 28 | 29 | public function clean() 30 | { 31 | if (!isset($this->currentApp['auth'])) { 32 | return; 33 | } 34 | $this->guards->setValue($this->currentApp['auth'], []); 35 | $this->currentApp->forgetInstance('auth.driver'); 36 | Facade::clearResolvedInstance('auth.driver'); 37 | } 38 | } -------------------------------------------------------------------------------- /src/Illuminate/Cleaners/BaseCleaner.php: -------------------------------------------------------------------------------- 1 | currentApp = $currentApp; 15 | $this->snapshotApp = $snapshotApp; 16 | } 17 | } -------------------------------------------------------------------------------- /src/Illuminate/Cleaners/CleanerInterface.php: -------------------------------------------------------------------------------- 1 | currentApp['config']->set($this->snapshotApp['config']->all()); 10 | } 11 | } -------------------------------------------------------------------------------- /src/Illuminate/Cleaners/ContainerCleaner.php: -------------------------------------------------------------------------------- 1 | Initial value 12 | 'reboundCallbacks' => [], 13 | 'currentRoute' => [], // For Lumen: fixed wrong $request->route() 14 | ]; 15 | 16 | private $cleanProperties = [ 17 | // Property => ReflectionObject 18 | ]; 19 | 20 | public function __construct(Container $currentApp, Container $snapshotApp) 21 | { 22 | parent::__construct($currentApp, $snapshotApp); 23 | $currentReflection = new \ReflectionObject($this->currentApp); 24 | $defaultValues = $currentReflection->getDefaultProperties(); 25 | foreach ($this->properties as $property => &$initValue) { 26 | if ($currentReflection->hasProperty($property)) { 27 | $this->cleanProperties[$property] = $currentReflection->getProperty($property); 28 | $this->cleanProperties[$property]->setAccessible(true); 29 | $initValue = $defaultValues[$property]; 30 | } 31 | } 32 | unset($initValue); 33 | } 34 | 35 | public function clean() 36 | { 37 | foreach ($this->cleanProperties as $property => $reflection) { 38 | $reflection->setValue($this->currentApp, $this->properties[$property]); 39 | } 40 | } 41 | } -------------------------------------------------------------------------------- /src/Illuminate/Cleaners/CookieCleaner.php: -------------------------------------------------------------------------------- 1 | currentApp['cookie'])) { 15 | return; 16 | } 17 | $ref = new \ReflectionObject($this->currentApp['cookie']); 18 | $this->queued = $ref->getProperty('queued'); 19 | $this->queued->setAccessible(true); 20 | } 21 | 22 | public function clean() 23 | { 24 | if (!isset($this->currentApp['cookie'])) { 25 | return; 26 | } 27 | $this->queued->setValue($this->currentApp['cookie'], []); 28 | } 29 | } -------------------------------------------------------------------------------- /src/Illuminate/Cleaners/DcatAdminCleaner.php: -------------------------------------------------------------------------------- 1 | instances as $instance) { 28 | $this->currentApp->forgetInstance($instance); 29 | Facade::clearResolvedInstance($instance); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Illuminate/Cleaners/JWTCleaner.php: -------------------------------------------------------------------------------- 1 | instances as $instance) { 20 | $this->currentApp->forgetInstance($instance); 21 | Facade::clearResolvedInstance($instance); 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /src/Illuminate/Cleaners/LaravelAdminCleaner.php: -------------------------------------------------------------------------------- 1 | [], 16 | 'script' => [], 17 | 'style' => [], 18 | 'css' => [], 19 | 'js' => [], 20 | 'html' => [], 21 | 'headerJs' => [], 22 | 'manifest' => 'vendor/laravel-admin/minify-manifest.json', 23 | 'manifestData' => [], 24 | 'extensions' => [], 25 | 'minifyIgnores' => [], 26 | 'metaTitle' => null, 27 | 'favicon' => null, 28 | 'bootingCallbacks' => [], 29 | 'bootedCallbacks' => [], 30 | ]; 31 | 32 | public function __construct(Container $currentApp, Container $snapshotApp) 33 | { 34 | parent::__construct($currentApp, $snapshotApp); 35 | $this->reflection = new \ReflectionClass(self::ADMIN_CLASS); 36 | } 37 | 38 | public function clean() 39 | { 40 | foreach ($this->properties as $name => $value) { 41 | if ($this->reflection->hasProperty($name)) { 42 | $property = $this->reflection->getProperty($name); 43 | if ($property->isStatic()) { 44 | if (!$property->isPublic()) { 45 | $property->setAccessible(true); 46 | } 47 | $property->setValue($value); 48 | } 49 | } 50 | } 51 | $this->currentApp->forgetInstance(self::ADMIN_CLASS); 52 | Facade::clearResolvedInstance(self::ADMIN_CLASS); 53 | } 54 | } -------------------------------------------------------------------------------- /src/Illuminate/Cleaners/MenuCleaner.php: -------------------------------------------------------------------------------- 1 | currentApp->forgetInstance('Lavary\Menu\Menu'); 12 | Facade::clearResolvedInstance('Lavary\Menu\Menu'); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/Illuminate/Cleaners/RequestCleaner.php: -------------------------------------------------------------------------------- 1 | currentApp->forgetInstance('url'); 12 | Facade::clearResolvedInstance('url'); 13 | 14 | $this->currentApp->forgetInstance('request'); 15 | Facade::clearResolvedInstance('request'); 16 | } 17 | } -------------------------------------------------------------------------------- /src/Illuminate/Cleaners/SessionCleaner.php: -------------------------------------------------------------------------------- 1 | currentApp['session'])) { 19 | return; 20 | } 21 | $ref = new \ReflectionObject($this->currentApp['session']); 22 | $this->drivers = $ref->getProperty('drivers'); 23 | $this->drivers->setAccessible(true); 24 | 25 | } 26 | 27 | public function clean() 28 | { 29 | if (!isset($this->currentApp['session'])) { 30 | return; 31 | } 32 | 33 | $this->drivers->setValue($this->currentApp['session'], []); 34 | $this->currentApp->forgetInstance('session.store'); 35 | Facade::clearResolvedInstance('session.store'); 36 | 37 | if (isset($this->currentApp['redirect'])) { 38 | /**@var Redirector $redirect */ 39 | $redirect = $this->currentApp['redirect']; 40 | $redirect->setSession($this->currentApp->make('session.store')); 41 | } 42 | } 43 | } -------------------------------------------------------------------------------- /src/Illuminate/Cleaners/ZiggyCleaner.php: -------------------------------------------------------------------------------- 1 | true, 33 | '/.htaccess' => true, 34 | '/web.config' => true, 35 | ]; 36 | /**@var array */ 37 | protected static $staticIndexList = [ 38 | 'index.html', 39 | ]; 40 | 41 | /**@var array */ 42 | private $rawGlobals = []; 43 | 44 | /**@var CleanerManager */ 45 | protected $cleanerManager; 46 | 47 | public function __construct(array $conf = []) 48 | { 49 | $this->conf = $conf; 50 | 51 | // Merge $_ENV $_SERVER 52 | $this->rawGlobals['_SERVER'] = $_SERVER + $this->conf['_SERVER']; 53 | $this->rawGlobals['_ENV'] = $_ENV + $this->conf['_ENV']; 54 | } 55 | 56 | public function prepareLaravel() 57 | { 58 | list($this->currentApp, $this->kernel) = $this->createAppKernel(); 59 | 60 | $this->reflectionApp = new ReflectionApp($this->currentApp); 61 | 62 | $this->saveSnapshot(); 63 | 64 | // Create cleaner manager 65 | $this->cleanerManager = new CleanerManager($this->currentApp, $this->snapshotApp, $this->conf); 66 | } 67 | 68 | protected function saveSnapshot() 69 | { 70 | $this->snapshotApp = clone $this->currentApp; 71 | 72 | $instances = $this->reflectionApp->instances(); 73 | 74 | foreach ($instances as $key => $value) { 75 | $this->snapshotApp->offsetSet($key, is_object($value) ? clone $value : $value); 76 | } 77 | } 78 | 79 | protected function createAppKernel() 80 | { 81 | // Register autoload 82 | self::autoload($this->conf['root_path']); 83 | 84 | // Make kernel for Laravel 85 | $app = require $this->conf['root_path'] . '/bootstrap/app.php'; 86 | $kernel = $this->conf['is_lumen'] ? null : $app->make(HttpKernel::class); 87 | 88 | // Boot 89 | if ($this->conf['is_lumen']) { 90 | $this->configureLumen($app); 91 | if (method_exists($app, 'boot')) { 92 | $app->boot(); 93 | } 94 | } else { 95 | $app->make(ConsoleKernel::class)->bootstrap(); 96 | } 97 | 98 | return [$app, $kernel]; 99 | } 100 | 101 | protected function configureLumen(Container $app) 102 | { 103 | $cfgPaths = [ 104 | // Framework default configuration 105 | $this->conf['root_path'] . '/vendor/laravel/lumen-framework/config/', 106 | // App configuration 107 | $this->conf['root_path'] . '/config/', 108 | ]; 109 | 110 | $keys = []; 111 | foreach ($cfgPaths as $cfgPath) { 112 | $configs = (array)glob($cfgPath . '*.php'); 113 | foreach ($configs as $config) { 114 | $config = substr(basename($config), 0, -4); 115 | $keys[$config] = $config; 116 | } 117 | } 118 | 119 | foreach ($keys as $key) { 120 | $app->configure($key); 121 | } 122 | } 123 | 124 | public static function autoload($rootPath) 125 | { 126 | $autoload = $rootPath . '/bootstrap/autoload.php'; 127 | if (file_exists($autoload)) { 128 | require_once $autoload; 129 | } else { 130 | require_once $rootPath . '/vendor/autoload.php'; 131 | } 132 | } 133 | 134 | public function getRawGlobals() 135 | { 136 | return $this->rawGlobals; 137 | } 138 | 139 | public function handleDynamic(IlluminateRequest $request) 140 | { 141 | ob_start(); 142 | 143 | if ($this->conf['is_lumen']) { 144 | $response = $this->currentApp->dispatch($request); 145 | if ($response instanceof SymfonyResponse) { 146 | $content = $response->getContent(); 147 | } else { 148 | $content = $response; 149 | } 150 | 151 | $this->reflectionApp->callTerminableMiddleware($response); 152 | } else { 153 | $response = $this->kernel->handle($request); 154 | $content = $response->getContent(); 155 | $this->kernel->terminate($request, $response); 156 | } 157 | 158 | // prefer content in response, secondly ob 159 | if (!($response instanceof StreamedResponse) && (string)$content === '' && ob_get_length() > 0) { 160 | $response->setContent(ob_get_contents()); 161 | } 162 | 163 | ob_end_clean(); 164 | 165 | return $response; 166 | } 167 | 168 | public function handleStatic(IlluminateRequest $request) 169 | { 170 | $uri = $request->getRequestUri(); 171 | $uri = (string)str_replace("\0", '', urldecode($uri)); 172 | if (isset(self::$staticBlackList[$uri]) || strpos($uri, '/..') !== false) { 173 | return false; 174 | } 175 | 176 | $requestFile = $this->conf['static_path'] . $uri; 177 | if (is_file($requestFile)) { 178 | return $this->createStaticResponse($requestFile, $request); 179 | } 180 | if (is_dir($requestFile)) { 181 | $indexFile = $this->lookupIndex($requestFile); 182 | if ($indexFile === false) { 183 | return false; 184 | } 185 | return $this->createStaticResponse($indexFile, $request); 186 | } 187 | return false; 188 | } 189 | 190 | protected function lookupIndex($folder) 191 | { 192 | $folder = rtrim($folder, '/') . '/'; 193 | foreach (self::$staticIndexList as $index) { 194 | $tmpFile = $folder . $index; 195 | if (is_file($tmpFile)) { 196 | return $tmpFile; 197 | } 198 | } 199 | return false; 200 | } 201 | 202 | public function createStaticResponse($requestFile, IlluminateRequest $request) 203 | { 204 | $response = new BinaryFileResponse($requestFile); 205 | $response->prepare($request); 206 | $response->isNotModified($request); 207 | return $response; 208 | } 209 | 210 | public function clean() 211 | { 212 | $this->cleanerManager->clean(); 213 | $this->cleanerManager->cleanControllers(); 214 | } 215 | 216 | public function cleanProviders() 217 | { 218 | $this->cleanerManager->cleanProviders(); 219 | } 220 | 221 | public function fireEvent($name, array $params = []) 222 | { 223 | $params[] = $this->currentApp; 224 | return method_exists($this->currentApp['events'], 'dispatch') ? 225 | $this->currentApp['events']->dispatch($name, $params) : $this->currentApp['events']->fire($name, $params); 226 | } 227 | 228 | public function bindRequest(IlluminateRequest $request) 229 | { 230 | $this->currentApp->instance('request', $request); 231 | } 232 | 233 | public function bindSwoole($swoole) 234 | { 235 | $this->currentApp->singleton('swoole', function () use ($swoole) { 236 | return $swoole; 237 | }); 238 | } 239 | 240 | public function saveSession() 241 | { 242 | if (isset($this->currentApp['session'])) { 243 | $this->currentApp['session']->save(); 244 | } 245 | } 246 | } 247 | -------------------------------------------------------------------------------- /src/Illuminate/LaravelSCommand.php: -------------------------------------------------------------------------------- 1 | handle(); 22 | } 23 | 24 | public function handle() 25 | { 26 | $action = (string)$this->argument('action'); 27 | switch ($action) { 28 | case 'publish': 29 | $this->publish(); 30 | break; 31 | case 'config': 32 | case 'info': 33 | $this->prepareConfig(); 34 | $this->showInfo(); 35 | break; 36 | default: 37 | $this->info(sprintf('Usage: [%s] ./artisan laravels publish|config|info', PHP_BINARY)); 38 | if (in_array($action, ['start', 'stop', 'restart', 'reload'], true)) { 39 | $this->error(sprintf( 40 | 'The "%s" command has been migrated to "bin/laravels", %ssee https://github.com/hhxsv5/laravel-s#run', 41 | $action, 42 | file_exists(base_path('bin/laravels')) ? '' : 'please run `php artisan laravels publish` first, ' 43 | )); 44 | } 45 | break; 46 | } 47 | } 48 | 49 | protected function isLumen() 50 | { 51 | return stripos($this->getApplication()->getVersion(), 'Lumen') !== false; 52 | } 53 | 54 | protected function loadConfig() 55 | { 56 | // Load configuration laravel.php manually for Lumen 57 | $basePath = config('laravels.laravel_base_path') ?: base_path(); 58 | if ($this->isLumen() && file_exists($basePath . '/config/laravels.php')) { 59 | $this->getLaravel()->configure('laravels'); 60 | } 61 | } 62 | 63 | protected function showInfo() 64 | { 65 | $this->showLogo(); 66 | $this->showComponents(); 67 | $this->showProtocols(); 68 | $this->comment('>>> Feedback: https://github.com/hhxsv5/laravel-s'); 69 | } 70 | 71 | protected function showLogo() 72 | { 73 | static $logo = <<info($logo); 83 | $this->info('Speed up your Laravel/Lumen'); 84 | } 85 | 86 | protected function showComponents() 87 | { 88 | $this->comment('>>> Components'); 89 | $laravelSVersion = '-'; 90 | $lockFile = base_path('composer.lock'); 91 | $cfg = file_exists($lockFile) ? json_decode(file_get_contents($lockFile), true) : []; 92 | if (isset($cfg['packages'])) { 93 | $packages = array_merge($cfg['packages'], Arr::get($cfg, 'packages-dev', [])); 94 | foreach ($packages as $package) { 95 | if (isset($package['name']) && $package['name'] === 'hhxsv5/laravel-s') { 96 | $laravelSVersion = ltrim($package['version'], 'vV'); 97 | break; 98 | } 99 | } 100 | } 101 | $this->table(['Component', 'Version'], [ 102 | [ 103 | 'PHP', 104 | PHP_VERSION, 105 | ], 106 | [ 107 | extension_loaded('openswoole') ? 'Open Swoole' : 'Swoole', 108 | SWOOLE_VERSION, 109 | ], 110 | [ 111 | 'LaravelS', 112 | $laravelSVersion, 113 | ], 114 | [ 115 | $this->getApplication()->getName() . ' [' . env('APP_ENV', config('app.env')) . ']', 116 | $this->getApplication()->getVersion(), 117 | ], 118 | ]); 119 | } 120 | 121 | protected function showProtocols() 122 | { 123 | $this->comment('>>> Protocols'); 124 | 125 | $config = unserialize((string)file_get_contents($this->getConfigPath())); 126 | $ssl = isset($config['server']['swoole']['ssl_key_file'], $config['server']['swoole']['ssl_cert_file']); 127 | $socketType = isset($config['server']['socket_type']) ? $config['server']['socket_type'] : SWOOLE_SOCK_TCP; 128 | if (in_array($socketType, [SWOOLE_SOCK_UNIX_DGRAM, SWOOLE_SOCK_UNIX_STREAM])) { 129 | $listenAt = $config['server']['listen_ip']; 130 | } else { 131 | $listenAt = sprintf('%s:%s', $config['server']['listen_ip'], $config['server']['listen_port']); 132 | } 133 | 134 | $tableRows = [ 135 | [ 136 | 'Main HTTP', 137 | 'On', 138 | $this->isLumen() ? 'Lumen Router' : 'Laravel Router', 139 | sprintf('%s://%s', $ssl ? 'https' : 'http', $listenAt), 140 | ], 141 | ]; 142 | if (!empty($config['server']['websocket']['enable'])) { 143 | $tableRows [] = [ 144 | 'Main WebSocket', 145 | 'On', 146 | $config['server']['websocket']['handler'], 147 | sprintf('%s://%s', $ssl ? 'wss' : 'ws', $listenAt), 148 | ]; 149 | } 150 | 151 | $socketTypeNames = [ 152 | SWOOLE_SOCK_TCP => 'TCP IPV4 Socket', 153 | SWOOLE_SOCK_TCP6 => 'TCP IPV6 Socket', 154 | SWOOLE_SOCK_UDP => 'UDP IPV4 Socket', 155 | SWOOLE_SOCK_UDP6 => 'TCP IPV6 Socket', 156 | SWOOLE_SOCK_UNIX_DGRAM => 'Unix Socket Dgram', 157 | SWOOLE_SOCK_UNIX_STREAM => 'Unix Socket Stream', 158 | ]; 159 | $sockets = isset($config['server']['sockets']) ? $config['server']['sockets'] : []; 160 | foreach ($sockets as $key => $socket) { 161 | if (isset($socket['enable']) && !$socket['enable']) { 162 | continue; 163 | } 164 | 165 | $name = 'Port#' . $key . ' '; 166 | $name .= isset($socketTypeNames[$socket['type']]) ? $socketTypeNames[$socket['type']] : 'Unknown socket'; 167 | $tableRows [] = [ 168 | $name, 169 | 'On', 170 | $socket['handler'], 171 | sprintf('%s:%s', $socket['host'], $socket['port']), 172 | ]; 173 | } 174 | $this->table(['Protocol', 'Status', 'Handler', 'Listen At'], $tableRows); 175 | } 176 | 177 | protected function prepareConfig() 178 | { 179 | $this->loadConfig(); 180 | 181 | $svrConf = config('laravels'); 182 | 183 | $this->preSet($svrConf); 184 | 185 | $ret = $this->preCheck($svrConf); 186 | if ($ret !== 0) { 187 | return $ret; 188 | } 189 | 190 | // Fixed $_ENV['APP_ENV'] 191 | if (isset($_SERVER['APP_ENV'])) { 192 | $_ENV['APP_ENV'] = $_SERVER['APP_ENV']; 193 | } 194 | 195 | $laravelConf = [ 196 | 'root_path' => $svrConf['laravel_base_path'], 197 | 'static_path' => $svrConf['swoole']['document_root'], 198 | 'cleaners' => array_unique((array)Arr::get($svrConf, 'cleaners', [])), 199 | 'register_providers' => array_unique((array)Arr::get($svrConf, 'register_providers', [])), 200 | 'destroy_controllers' => Arr::get($svrConf, 'destroy_controllers', []), 201 | 'is_lumen' => $this->isLumen(), 202 | '_SERVER' => $_SERVER, 203 | '_ENV' => $_ENV, 204 | ]; 205 | 206 | $config = ['server' => $svrConf, 'laravel' => $laravelConf]; 207 | return file_put_contents($this->getConfigPath(), serialize($config)) > 0 ? 0 : 1; 208 | } 209 | 210 | protected function getConfigPath() 211 | { 212 | return storage_path('laravels.conf'); 213 | } 214 | 215 | protected function preSet(array &$svrConf) 216 | { 217 | if (!isset($svrConf['enable_gzip'])) { 218 | $svrConf['enable_gzip'] = false; 219 | } 220 | if (empty($svrConf['laravel_base_path'])) { 221 | $svrConf['laravel_base_path'] = base_path(); 222 | } 223 | if (empty($svrConf['process_prefix'])) { 224 | $svrConf['process_prefix'] = trim(config('app.name', '') . ' ' . $svrConf['laravel_base_path']); 225 | } 226 | if ($this->option('ignore')) { 227 | $svrConf['ignore_check_pid'] = true; 228 | } elseif (!isset($svrConf['ignore_check_pid'])) { 229 | $svrConf['ignore_check_pid'] = false; 230 | } 231 | if (empty($svrConf['swoole']['document_root'])) { 232 | $svrConf['swoole']['document_root'] = $svrConf['laravel_base_path'] . '/public'; 233 | } 234 | if ($this->option('daemonize')) { 235 | $svrConf['swoole']['daemonize'] = true; 236 | } elseif (!isset($svrConf['swoole']['daemonize'])) { 237 | $svrConf['swoole']['daemonize'] = false; 238 | } 239 | if (empty($svrConf['swoole']['pid_file'])) { 240 | $svrConf['swoole']['pid_file'] = storage_path('laravels.pid'); 241 | } 242 | if (empty($svrConf['timer']['max_wait_time'])) { 243 | $svrConf['timer']['max_wait_time'] = 5; 244 | } 245 | 246 | // Configure TimerProcessMetricsCronJob automatically 247 | if (isset($svrConf['processes']) && !empty($svrConf['timer']['enable'])) { 248 | foreach ($svrConf['processes'] as $process) { 249 | if ($process['class'] === CollectorProcess::class && (!isset($process['enable']) || $process['enable'])) { 250 | $svrConf['timer']['jobs'][] = TimerProcessMetricsCronJob::class; 251 | break; 252 | } 253 | } 254 | } 255 | 256 | // Set X-Version 257 | $xVersion = (string)$this->option('x-version'); 258 | if ($xVersion !== '') { 259 | $_SERVER['X_VERSION'] = $_ENV['X_VERSION'] = $xVersion; 260 | } 261 | return 0; 262 | } 263 | 264 | protected function preCheck(array $svrConf) 265 | { 266 | if (!empty($svrConf['enable_gzip']) && version_compare(SWOOLE_VERSION, '4.1.0', '>=')) { 267 | $this->error('enable_gzip is DEPRECATED since Swoole 4.1.0, set http_compression of Swoole instead, http_compression is disabled by default.'); 268 | $this->info('If there is a proxy server like Nginx, suggest that enable gzip in Nginx and disable gzip in Swoole, to avoid the repeated gzip compression for response.'); 269 | return 1; 270 | } 271 | if (!empty($svrConf['events'])) { 272 | if (empty($svrConf['swoole']['task_worker_num']) || $svrConf['swoole']['task_worker_num'] <= 0) { 273 | $this->error('Asynchronous event listening needs to set task_worker_num > 0'); 274 | return 1; 275 | } 276 | } 277 | return 0; 278 | } 279 | 280 | 281 | public function publish() 282 | { 283 | $basePath = config('laravels.laravel_base_path') ?: base_path(); 284 | $configPath = $basePath . '/config/laravels.php'; 285 | $todoList = [ 286 | [ 287 | 'from' => realpath(__DIR__ . '/../../config/laravels.php'), 288 | 'to' => $configPath, 289 | 'mode' => 0644, 290 | ], 291 | [ 292 | 'from' => realpath(__DIR__ . '/../../bin/laravels'), 293 | 'to' => $basePath . '/bin/laravels', 294 | 'mode' => 0755, 295 | 'link' => true, 296 | ], 297 | [ 298 | 'from' => realpath(__DIR__ . '/../../bin/fswatch'), 299 | 'to' => $basePath . '/bin/fswatch', 300 | 'mode' => 0755, 301 | 'link' => true, 302 | ], 303 | [ 304 | 'from' => realpath(__DIR__ . '/../../bin/inotify'), 305 | 'to' => $basePath . '/bin/inotify', 306 | 'mode' => 0755, 307 | 'link' => true, 308 | ], 309 | ]; 310 | if (file_exists($configPath)) { 311 | $choice = $this->anticipate($configPath . ' already exists, do you want to override it ? Y/N', 312 | ['Y', 'N'], 313 | 'N' 314 | ); 315 | if (!$choice || strtoupper($choice) !== 'Y') { 316 | array_shift($todoList); 317 | } 318 | } 319 | 320 | foreach ($todoList as $todo) { 321 | $toDir = dirname($todo['to']); 322 | if (!is_dir($toDir) && !mkdir($toDir, 0755, true) && !is_dir($toDir)) { 323 | throw new \RuntimeException(sprintf('Directory "%s" was not created', $toDir)); 324 | } 325 | if (file_exists($todo['to'])) { 326 | unlink($todo['to']); 327 | } 328 | $operation = 'Copied'; 329 | if (empty($todo['link'])) { 330 | copy($todo['from'], $todo['to']); 331 | } elseif (@link($todo['from'], $todo['to'])) { 332 | $operation = 'Linked'; 333 | } else { 334 | copy($todo['from'], $todo['to']); 335 | } 336 | chmod($todo['to'], $todo['mode']); 337 | $this->line("{$operation} file [{$todo['from']}] To [{$todo['to']}]"); 338 | } 339 | return 0; 340 | } 341 | } 342 | -------------------------------------------------------------------------------- /src/Illuminate/LaravelSServiceProvider.php: -------------------------------------------------------------------------------- 1 | publishes([ 12 | __DIR__ . '/../../config/laravels.php' => base_path('config/laravels.php'), 13 | ]); 14 | } 15 | 16 | public function register() 17 | { 18 | $this->mergeConfigFrom( 19 | __DIR__ . '/../../config/laravels.php', 'laravels' 20 | ); 21 | 22 | $this->commands([ 23 | LaravelSCommand::class, 24 | ListPropertiesCommand::class, 25 | ]); 26 | } 27 | } -------------------------------------------------------------------------------- /src/Illuminate/LaravelScheduleJob.php: -------------------------------------------------------------------------------- 1 | > /dev/null 2>&1 &'); 25 | } 26 | } -------------------------------------------------------------------------------- /src/Illuminate/LaravelTrait.php: -------------------------------------------------------------------------------- 1 | prepareLaravel(); 13 | $laravel->bindSwoole($swoole); 14 | return $laravel; 15 | } 16 | } -------------------------------------------------------------------------------- /src/Illuminate/ListPropertiesCommand.php: -------------------------------------------------------------------------------- 1 | handle(); 24 | } 25 | 26 | /** 27 | * @throws \ReflectionException 28 | */ 29 | public function handle() 30 | { 31 | $this->outputTable(); 32 | } 33 | 34 | /** 35 | * Output all properties of all controllers as table. 36 | * 37 | * @throws \ReflectionException 38 | */ 39 | private function outputTable() 40 | { 41 | $allProperties = $this->allControllerProperties(); 42 | foreach ($allProperties as $controller => $properties) { 43 | if (empty($properties)) { 44 | continue; 45 | } 46 | 47 | $this->table( 48 | ['Controller', 'Property', 'Property Modifier'], 49 | $properties 50 | ); 51 | } 52 | } 53 | 54 | /** 55 | * Get all properties of all controllers. 56 | * 57 | * @return array 58 | * @throws \ReflectionException 59 | */ 60 | private function allControllerProperties() 61 | { 62 | $controllers = $this->allControllers(); 63 | array_walk($controllers, function (&$properties, $controller) { 64 | $properties = []; 65 | // Get parent's properties 66 | $parent = get_parent_class($controller); 67 | if ($parent) { 68 | $reflectParentController = new \ReflectionClass($parent); 69 | $parentProperties = $reflectParentController->getProperties(); 70 | foreach ($parentProperties as $property) { 71 | $properties[$property->getName()] = [ 72 | $controller => $controller, 73 | 'Property' => $property->getName(), 74 | 'Property Modifier' => $this->resolveModifiers($property), 75 | ]; 76 | } 77 | } 78 | 79 | // Get sub controller's properties, override the parent properties. 80 | $reflectController = new \ReflectionClass($controller); 81 | $subProperties = $reflectController->getProperties(); 82 | foreach ($subProperties as $property) { 83 | $properties[$property->getName()] = [ 84 | $controller => $controller, 85 | 'Property' => $property->getName(), 86 | 'Property Modifier' => $this->resolveModifiers($property), 87 | ]; 88 | } 89 | }); 90 | return $controllers; 91 | } 92 | 93 | /** 94 | * Get all controllers 95 | * 96 | * @return array 97 | * @throws \ReflectionException 98 | */ 99 | private function allControllers() 100 | { 101 | $controllers = []; 102 | $router = isset(app()->router) ? app()->router : (app()->offsetExists('router') ? app('router') : app()); 103 | $routes = $router->getRoutes(); 104 | if (is_array($routes)) { 105 | $uses = array_column(array_column($routes, 'action'), 'uses'); 106 | } else { 107 | $property = new \ReflectionProperty(get_class($routes), 'actionList'); 108 | $property->setAccessible(true); 109 | $uses = array_keys($property->getValue($routes)); 110 | } 111 | 112 | foreach ($uses as $use) { 113 | list($controller,) = explode('@', $use); 114 | $controllers[$controller] = $controller; 115 | } 116 | return $controllers; 117 | } 118 | 119 | /** 120 | * Resolve modifiers from \ReflectionProperty 121 | * 122 | * @param \ReflectionProperty $property 123 | * @return string 124 | */ 125 | private function resolveModifiers(\ReflectionProperty $property) 126 | { 127 | if ($property->isPublic()) { 128 | $modifier = 'public'; 129 | } elseif ($property->isProtected()) { 130 | $modifier = 'protected'; 131 | } elseif ($property->isPrivate()) { 132 | $modifier = 'private'; 133 | } else { 134 | $modifier = ' '; 135 | } 136 | if ($property->isStatic()) { 137 | $modifier .= ' static'; 138 | } 139 | return $modifier; 140 | } 141 | } -------------------------------------------------------------------------------- /src/Illuminate/LogTrait.php: -------------------------------------------------------------------------------- 1 | log( 13 | sprintf( 14 | 'Uncaught exception \'%s\': [%d]%s called in %s:%d%s%s', 15 | get_class($e), 16 | $e->getCode(), 17 | $e->getMessage(), 18 | $e->getFile(), 19 | $e->getLine(), 20 | PHP_EOL, 21 | $e->getTraceAsString() 22 | ), 23 | 'ERROR' 24 | ); 25 | } 26 | 27 | public function log($msg, $type = 'INFO') 28 | { 29 | $outputStyle = LaravelS::getOutputStyle(); 30 | $msg = sprintf('[%s] [%s] %s', date('Y-m-d H:i:s'), $type, $msg); 31 | if ($outputStyle) { 32 | switch (strtoupper($type)) { 33 | case 'INFO': 34 | $outputStyle->writeln("{$msg}"); 35 | break; 36 | case 'WARNING': 37 | if (!$outputStyle->getFormatter()->hasStyle('warning')) { 38 | $style = new OutputFormatterStyle('yellow'); 39 | $outputStyle->getFormatter()->setStyle('warning', $style); 40 | } 41 | $outputStyle->writeln("{$msg}"); 42 | break; 43 | case 'ERROR': 44 | $outputStyle->writeln("{$msg}"); 45 | break; 46 | case 'TRACE': 47 | default: 48 | $outputStyle->writeln($msg); 49 | break; 50 | } 51 | } else { 52 | echo $msg, PHP_EOL; 53 | } 54 | } 55 | 56 | public function trace($msg) 57 | { 58 | $this->log($msg, 'TRACE'); 59 | } 60 | 61 | public function info($msg) 62 | { 63 | $this->log($msg, 'INFO'); 64 | } 65 | 66 | public function warning($msg) 67 | { 68 | $this->log($msg, 'WARNING'); 69 | } 70 | 71 | public function error($msg) 72 | { 73 | $this->log($msg, 'ERROR'); 74 | } 75 | 76 | public function callWithCatchException(callable $callback, array $args = [], $tries = 1) 77 | { 78 | $try = 0; 79 | do { 80 | $try++; 81 | try { 82 | return call_user_func_array($callback, $args); 83 | } catch (\Exception $e) { 84 | $this->logException($e); 85 | } 86 | } while ($try < $tries); 87 | return null; 88 | } 89 | } -------------------------------------------------------------------------------- /src/Illuminate/ReflectionApp.php: -------------------------------------------------------------------------------- 1 | app = $app; 28 | 29 | $this->reflectionApp = new \ReflectionObject($app); 30 | } 31 | 32 | /** 33 | * Get all bindings from application container. 34 | * 35 | * @return array 36 | * @throws \ReflectionException 37 | */ 38 | public function instances() 39 | { 40 | $instances = $this->reflectionApp->getProperty('instances'); 41 | $instances->setAccessible(true); 42 | $instances = array_merge($this->app->getBindings(), $instances->getValue($this->app)); 43 | 44 | return $instances; 45 | } 46 | 47 | /** 48 | * Call terminable middleware of Lumen. 49 | * 50 | * @param Response $response 51 | * @throws \ReflectionException 52 | */ 53 | public function callTerminableMiddleware(Response $response) 54 | { 55 | $middleware = $this->reflectionApp->getProperty('middleware'); 56 | $middleware->setAccessible(true); 57 | 58 | if (!empty($middleware->getValue($this->app))) { 59 | $callTerminableMiddleware = $this->reflectionApp->getMethod('callTerminableMiddleware'); 60 | $callTerminableMiddleware->setAccessible(true); 61 | $callTerminableMiddleware->invoke($this->app, $response); 62 | } 63 | } 64 | 65 | /** 66 | * The parameter count of 'register' method in app container. 67 | * 68 | * @return int 69 | * @throws \ReflectionException 70 | */ 71 | public function registerMethodParameterCount() 72 | { 73 | return $this->reflectionApp->getMethod('register')->getNumberOfParameters(); 74 | } 75 | 76 | /** 77 | * Get 'loadedProviders' of application container. 78 | * 79 | * @return array 80 | * @throws \ReflectionException 81 | */ 82 | public function loadedProviders() 83 | { 84 | $loadedProviders = $this->reflectLoadedProviders(); 85 | return $loadedProviders->getValue($this->app); 86 | } 87 | 88 | /** 89 | * Set 'loadedProviders' of application container. 90 | * 91 | * @param array $loadedProviders 92 | * @throws \ReflectionException 93 | */ 94 | public function setLoadedProviders(array $loadedProviders) 95 | { 96 | $reflectLoadedProviders = $this->reflectLoadedProviders(); 97 | $reflectLoadedProviders->setValue($this->app, $loadedProviders); 98 | } 99 | 100 | /** 101 | * Get the reflect loadedProviders of application container. 102 | * 103 | * @return \ReflectionProperty 104 | * @throws \ReflectionException 105 | */ 106 | protected function reflectLoadedProviders() 107 | { 108 | $loadedProviders = $this->reflectionApp->getProperty('loadedProviders'); 109 | $loadedProviders->setAccessible(true); 110 | return $loadedProviders; 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/LaravelS.php: -------------------------------------------------------------------------------- 1 | Laravel Request 33 | * Laravel Request => Laravel handle => Laravel Response 34 | * Laravel Response => Swoole Response 35 | */ 36 | class LaravelS extends Server 37 | { 38 | /** 39 | * Fix conflicts of traits 40 | */ 41 | use InotifyTrait, LaravelTrait, LogTrait, ProcessTitleTrait, TimerTrait, CustomProcessTrait; 42 | 43 | /**@var array */ 44 | protected array $laravelConf; 45 | 46 | /**@var Laravel */ 47 | protected Laravel $laravel; 48 | 49 | /**@var Process[] */ 50 | protected static array $customProcesses = []; 51 | 52 | public function __construct(array $svrConf, array $laravelConf) 53 | { 54 | parent::__construct($svrConf); 55 | $this->laravelConf = $laravelConf; 56 | 57 | $timerConf = $this->conf['timer'] ?? []; 58 | $timerConf['process_prefix'] = $svrConf['process_prefix']; 59 | $this->addTimerProcess($this->swoole, $timerConf, $this->laravelConf); 60 | 61 | $inotifyConf = $this->conf['inotify_reload'] ?? []; 62 | if (!isset($inotifyConf['watch_path'])) { 63 | $inotifyConf['watch_path'] = $this->laravelConf['root_path']; 64 | } 65 | $inotifyConf['process_prefix'] = $svrConf['process_prefix']; 66 | $this->addInotifyProcess($this->swoole, $inotifyConf, $this->laravelConf); 67 | 68 | $processes = $this->conf['processes'] ?? []; 69 | static::$customProcesses = $this->addCustomProcesses($this->swoole, $svrConf['process_prefix'], $processes, $this->laravelConf); 70 | 71 | // Fire ServerStart event 72 | if (isset($this->conf['event_handlers']['ServerStart'])) { 73 | Laravel::autoload($this->laravelConf['root_path']); 74 | $this->fireEvent('ServerStart', ServerStartInterface::class, [$this->swoole]); 75 | } 76 | } 77 | 78 | public static function getCustomProcesses(): array 79 | { 80 | return static::$customProcesses; 81 | } 82 | 83 | protected function beforeWebSocketHandShake(SwooleRequest $request) 84 | { 85 | // Start Laravel's lifetime, then support session ...middleware. 86 | $laravelRequest = $this->convertRequest($this->laravel, $request); 87 | $this->laravel->bindRequest($laravelRequest); 88 | $this->laravel->fireEvent('laravels.received_request', [$laravelRequest]); 89 | $this->laravel->cleanProviders(); 90 | $laravelResponse = $this->laravel->handleDynamic($laravelRequest); 91 | $this->laravel->fireEvent('laravels.generated_response', [$laravelRequest, $laravelResponse]); 92 | } 93 | 94 | protected function afterWebSocketOpen(SwooleRequest $request) 95 | { 96 | // End Laravel's lifetime. 97 | $this->laravel->saveSession(); 98 | $this->laravel->clean(); 99 | } 100 | 101 | protected function triggerWebSocketEvent($event, array $params) 102 | { 103 | if ($event === 'onHandShake') { 104 | $this->beforeWebSocketHandShake($params[0]); 105 | if (!empty($this->conf['server'])) { 106 | $params[1]->header('Server', $this->conf['server']); 107 | } 108 | } 109 | 110 | parent::triggerWebSocketEvent($event, $params); 111 | 112 | switch ($event) { 113 | case 'onHandShake': 114 | if (isset($params[1]->header['Sec-Websocket-Accept'])) { 115 | // Successful handshake 116 | parent::triggerWebSocketEvent('onOpen', [$this->swoole, $params[0]]); 117 | } 118 | $this->afterWebSocketOpen($params[0]); 119 | break; 120 | case 'onOpen': 121 | $this->afterWebSocketOpen($params[1]); 122 | break; 123 | } 124 | } 125 | 126 | protected function triggerPortEvent(Port $port, $handlerClass, $event, array $params) 127 | { 128 | switch ($event) { 129 | case 'onHandShake': 130 | $this->beforeWebSocketHandShake($params[0]); 131 | case 'onRequest': 132 | if (!empty($this->conf['server'])) { 133 | $params[1]->header('Server', $this->conf['server']); 134 | } 135 | break; 136 | } 137 | 138 | parent::triggerPortEvent($port, $handlerClass, $event, $params); 139 | 140 | switch ($event) { 141 | case 'onHandShake': 142 | if (isset($params[1]->header['Sec-Websocket-Accept'])) { 143 | // Successful handshake 144 | parent::triggerPortEvent($port, $handlerClass, 'onOpen', [$this->swoole, $params[0]]); 145 | } 146 | $this->afterWebSocketOpen($params[0]); 147 | break; 148 | case 'onOpen': 149 | $this->afterWebSocketOpen($params[1]); 150 | break; 151 | } 152 | } 153 | 154 | public function onShutdown(HttpServer $server) 155 | { 156 | parent::onShutdown($server); 157 | 158 | // Fire ServerStop event 159 | if (isset($this->conf['event_handlers']['ServerStop'])) { 160 | $this->laravel = $this->initLaravel($this->laravelConf, $this->swoole); 161 | $this->fireEvent('ServerStop', ServerStopInterface::class, [$server]); 162 | } 163 | } 164 | 165 | public function onWorkerStart(HttpServer $server, $workerId) 166 | { 167 | parent::onWorkerStart($server, $workerId); 168 | 169 | // To implement gracefully reload 170 | // Delay to create Laravel 171 | // Delay to include Laravel's autoload.php 172 | $this->laravel = $this->initLaravel($this->laravelConf, $this->swoole); 173 | 174 | // Fire WorkerStart event 175 | $this->fireEvent('WorkerStart', WorkerStartInterface::class, func_get_args()); 176 | } 177 | 178 | public function onWorkerStop(HttpServer $server, $workerId) 179 | { 180 | parent::onWorkerStop($server, $workerId); 181 | 182 | // Fire WorkerStop event 183 | $this->fireEvent('WorkerStop', WorkerStopInterface::class, func_get_args()); 184 | } 185 | 186 | public function onWorkerError(HttpServer $server, $workerId, $workerPId, $exitCode, $signal) 187 | { 188 | parent::onWorkerError($server, $workerId, $workerPId, $exitCode, $signal); 189 | 190 | Laravel::autoload($this->laravelConf['root_path']); 191 | 192 | // Fire WorkerError event 193 | $this->fireEvent('WorkerError', WorkerErrorInterface::class, func_get_args()); 194 | } 195 | 196 | protected function convertRequest(Laravel $laravel, SwooleRequest $request) 197 | { 198 | $rawGlobals = $laravel->getRawGlobals(); 199 | return (new Request($request))->toIlluminateRequest($rawGlobals['_SERVER'], $rawGlobals['_ENV']); 200 | } 201 | 202 | public function onRequest(SwooleRequest $swooleRequest, SwooleResponse $swooleResponse) 203 | { 204 | try { 205 | $laravelRequest = $this->convertRequest($this->laravel, $swooleRequest); 206 | $this->laravel->bindRequest($laravelRequest); 207 | $this->laravel->fireEvent('laravels.received_request', [$laravelRequest]); 208 | $handleStaticSuccess = false; 209 | if ($this->conf['handle_static']) { 210 | // For Swoole < 1.9.17 211 | $handleStaticSuccess = $this->handleStaticResource($this->laravel, $laravelRequest, $swooleResponse); 212 | } 213 | if (!$handleStaticSuccess) { 214 | $this->handleDynamicResource($this->laravel, $laravelRequest, $swooleResponse); 215 | } 216 | } catch (\Exception $e) { 217 | $this->handleException($e, $swooleResponse); 218 | } 219 | } 220 | 221 | /** 222 | * @param \Exception $e 223 | * @param SwooleResponse $response 224 | */ 225 | protected function handleException($e, SwooleResponse $response) 226 | { 227 | $error = sprintf( 228 | 'onRequest: Uncaught exception "%s"([%d]%s) at %s:%s, %s%s', 229 | get_class($e), 230 | $e->getCode(), 231 | $e->getMessage(), 232 | $e->getFile(), 233 | $e->getLine(), 234 | PHP_EOL, 235 | $e->getTraceAsString() 236 | ); 237 | $this->error($error); 238 | try { 239 | $response->status(500); 240 | $response->end('Oops! An unexpected error occurred'); 241 | } catch (\Exception $e) { 242 | $this->logException($e); 243 | } 244 | } 245 | 246 | protected function handleStaticResource(Laravel $laravel, IlluminateRequest $laravelRequest, SwooleResponse $swooleResponse) 247 | { 248 | $laravelResponse = $laravel->handleStatic($laravelRequest); 249 | if ($laravelResponse === false) { 250 | return false; 251 | } 252 | if (!empty($this->conf['server'])) { 253 | $laravelResponse->headers->set('Server', $this->conf['server'], true); 254 | } 255 | $laravel->fireEvent('laravels.generated_response', [$laravelRequest, $laravelResponse]); 256 | $response = new StaticResponse($swooleResponse, $laravelResponse); 257 | $response->setChunkLimit($this->conf['swoole']['buffer_output_size']); 258 | $response->send($this->conf['enable_gzip']); 259 | return true; 260 | } 261 | 262 | protected function handleDynamicResource(Laravel $laravel, IlluminateRequest $laravelRequest, SwooleResponse $swooleResponse) 263 | { 264 | $laravel->cleanProviders(); 265 | $laravelResponse = $laravel->handleDynamic($laravelRequest); 266 | if (!empty($this->conf['server'])) { 267 | $laravelResponse->headers->set('Server', $this->conf['server'], true); 268 | } 269 | $laravel->fireEvent('laravels.generated_response', [$laravelRequest, $laravelResponse]); 270 | if ($laravelResponse instanceof BinaryFileResponse) { 271 | $response = new StaticResponse($swooleResponse, $laravelResponse); 272 | } else { 273 | $response = new DynamicResponse($swooleResponse, $laravelResponse); 274 | } 275 | $response->setChunkLimit($this->conf['swoole']['buffer_output_size']); 276 | $response->send($this->conf['enable_gzip']); 277 | $laravel->clean(); 278 | return true; 279 | } 280 | 281 | /**@var OutputStyle */ 282 | protected static $outputStyle; 283 | 284 | public static function setOutputStyle(OutputStyle $outputStyle) 285 | { 286 | static::$outputStyle = $outputStyle; 287 | } 288 | 289 | public static function getOutputStyle() 290 | { 291 | return static::$outputStyle; 292 | } 293 | } 294 | -------------------------------------------------------------------------------- /src/Swoole/Coroutine/Context.php: -------------------------------------------------------------------------------- 1 | 0) { 27 | self::$box[$cid][$key] = $item; 28 | } 29 | } 30 | 31 | public static function delete($key = null) 32 | { 33 | $cid = Coroutine::getCid(); 34 | if ($cid > 0) { 35 | if ($key) { 36 | unset(self::$box[$cid][$key]); 37 | } else { 38 | unset(self::$box[$cid]); 39 | } 40 | } 41 | } 42 | 43 | public static function inCoroutine() 44 | { 45 | return class_exists('Swoole\Coroutine', false) && Coroutine::getCid() > 0; 46 | } 47 | } -------------------------------------------------------------------------------- /src/Swoole/DynamicResponse.php: -------------------------------------------------------------------------------- 1 | swooleResponse->gzip(2); 16 | } else { 17 | throw new \RuntimeException('Http GZIP requires library "zlib", use "php --ri zlib" to check.'); 18 | } 19 | } 20 | 21 | public function sendContent() 22 | { 23 | if ($this->laravelResponse instanceof StreamedResponse) { 24 | ob_start(); 25 | $this->laravelResponse = $this->laravelResponse->sendContent(); 26 | $content = ob_get_clean(); 27 | } else { 28 | $content = $this->laravelResponse->getContent(); 29 | } 30 | 31 | $len = strlen($content); 32 | if ($len === 0) { 33 | $this->swooleResponse->end(); 34 | return; 35 | } 36 | 37 | if ($len > $this->chunkLimit) { 38 | for ($offset = 0, $limit = (int)(0.6 * $this->chunkLimit); $offset < $len; $offset += $limit) { 39 | $chunk = substr($content, $offset, $limit); 40 | $this->swooleResponse->write($chunk); 41 | } 42 | $this->swooleResponse->end(); 43 | } else { 44 | $this->swooleResponse->end($content); 45 | } 46 | } 47 | } -------------------------------------------------------------------------------- /src/Swoole/Events/ServerStartInterface.php: -------------------------------------------------------------------------------- 1 | fd = inotify_init(); 23 | $this->watchPath = $watchPath; 24 | $this->watchMask = $watchMask; 25 | $this->watchHandler = $watchHandler; 26 | } 27 | 28 | public function addFileType($type) 29 | { 30 | $type = '.' . trim($type, '.'); 31 | $this->fileTypes[$type] = true; 32 | } 33 | 34 | public function addFileTypes(array $types) 35 | { 36 | foreach ($types as $type) { 37 | $this->addFileType($type); 38 | } 39 | } 40 | 41 | public function addExcludedDir($dir) 42 | { 43 | $dir = realpath($dir); 44 | $this->excludedDirs[$dir] = $dir; 45 | } 46 | 47 | public function addExcludedDirs(array $dirs) 48 | { 49 | foreach ($dirs as $dir) { 50 | $this->addExcludedDir($dir); 51 | } 52 | } 53 | 54 | public function isExcluded($path) 55 | { 56 | foreach ($this->excludedDirs as $excludedDir) { 57 | if ($excludedDir === $path || strpos($path, $excludedDir . '/') === 0) { 58 | return true; 59 | } 60 | } 61 | return false; 62 | } 63 | 64 | public function watch() 65 | { 66 | $this->_watch($this->watchPath); 67 | } 68 | 69 | protected function _watch($path) 70 | { 71 | if ($this->isExcluded($path)) { 72 | return false; 73 | } 74 | $wd = inotify_add_watch($this->fd, $path, $this->watchMask); 75 | if ($wd === false) { 76 | return false; 77 | } 78 | $this->bind($wd, $path); 79 | 80 | if (is_dir($path)) { 81 | $wd = inotify_add_watch($this->fd, $path, $this->watchMask); 82 | if ($wd === false) { 83 | return false; 84 | } 85 | $this->bind($wd, $path); 86 | $files = scandir($path); 87 | foreach ($files as $file) { 88 | if ($file === '.' || $file === '..' || $this->isExcluded($file)) { 89 | continue; 90 | } 91 | $file = $path . DIRECTORY_SEPARATOR . $file; 92 | if (is_dir($file)) { 93 | $this->_watch($file); 94 | } 95 | } 96 | } 97 | return true; 98 | } 99 | 100 | protected function clearWatch() 101 | { 102 | foreach ($this->wdPath as $wd => $path) { 103 | @inotify_rm_watch($this->fd, $wd); 104 | } 105 | $this->wdPath = []; 106 | $this->pathWd = []; 107 | } 108 | 109 | protected function bind($wd, $path) 110 | { 111 | $this->pathWd[$path] = $wd; 112 | $this->wdPath[$wd] = $path; 113 | } 114 | 115 | protected function unbind($wd, $path = null) 116 | { 117 | unset($this->wdPath[$wd]); 118 | if ($path !== null) { 119 | unset($this->pathWd[$path]); 120 | } 121 | } 122 | 123 | public function start() 124 | { 125 | Event::add($this->fd, function ($fp) { 126 | $events = inotify_read($fp); 127 | foreach ($events as $event) { 128 | if ($event['mask'] == IN_IGNORED) { 129 | continue; 130 | } 131 | 132 | $fileType = strchr($event['name'], '.'); 133 | if (!isset($this->fileTypes[$fileType])) { 134 | continue; 135 | } 136 | 137 | if ($this->doing) { 138 | continue; 139 | } 140 | 141 | Timer::after(100, function () use ($event) { 142 | call_user_func_array($this->watchHandler, [$event]); 143 | $this->doing = false; 144 | }); 145 | $this->doing = true; 146 | break; 147 | } 148 | }); 149 | Event::wait(); 150 | } 151 | 152 | public function stop() 153 | { 154 | Event::del($this->fd); 155 | fclose($this->fd); 156 | } 157 | 158 | public function getWatchedFileCount() 159 | { 160 | return count($this->wdPath); 161 | } 162 | 163 | public function __destruct() 164 | { 165 | $this->stop(); 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /src/Swoole/InotifyTrait.php: -------------------------------------------------------------------------------- 1 | warning('Require extension inotify'); 19 | return false; 20 | } 21 | 22 | $fileTypes = isset($config['file_types']) ? (array)$config['file_types'] : []; 23 | if (empty($fileTypes)) { 24 | $this->warning('No file types to watch by inotify'); 25 | return false; 26 | } 27 | 28 | $callback = function () use ($config, $laravelConf) { 29 | $log = !empty($config['log']); 30 | $this->setProcessTitle(sprintf('%s laravels: inotify process', $config['process_prefix'])); 31 | $inotify = new Inotify($config['watch_path'], IN_CREATE | IN_DELETE | IN_MODIFY | IN_MOVE, 32 | function ($event) use ($log, $laravelConf) { 33 | Portal::runLaravelSCommand($laravelConf['root_path'], 'reload'); 34 | if ($log) { 35 | $action = 'file:'; 36 | switch ($event['mask']) { 37 | case IN_CREATE: 38 | $action = 'create'; 39 | break; 40 | case IN_DELETE: 41 | $action = 'delete'; 42 | break; 43 | case IN_MODIFY: 44 | $action = 'modify'; 45 | break; 46 | case IN_MOVE: 47 | $action = 'move'; 48 | break; 49 | } 50 | $this->info(sprintf('reloaded by inotify, reason: %s %s', $action, $event['name'])); 51 | } 52 | }); 53 | $inotify->addFileTypes($config['file_types']); 54 | if (empty($config['excluded_dirs'])) { 55 | $config['excluded_dirs'] = []; 56 | } 57 | $inotify->addExcludedDirs($config['excluded_dirs']); 58 | $inotify->watch(); 59 | if ($log) { 60 | $this->info(sprintf('[Inotify] watched files: %d; file types: %s; excluded directories: %s', 61 | $inotify->getWatchedFileCount(), 62 | implode(',', $config['file_types']), 63 | implode(',', $config['excluded_dirs']) 64 | ) 65 | ); 66 | } 67 | $inotify->start(); 68 | }; 69 | 70 | $process = new Process($callback, false, 0); 71 | $swoole->addProcess($process); 72 | return $process; 73 | } 74 | } -------------------------------------------------------------------------------- /src/Swoole/Process/CustomProcessInterface.php: -------------------------------------------------------------------------------- 1 | setting['pid_file']) . '/' . $this->customProcessPidFile; 15 | if (file_exists($pidfile)) { 16 | unlink($pidfile); 17 | } 18 | 19 | /**@var []Process $processList */ 20 | $processList = []; 21 | foreach ($processes as $name => $item) { 22 | if (empty($item['class'])) { 23 | throw new \InvalidArgumentException(sprintf('The class of process %s must be specified', $name)); 24 | } 25 | if (isset($item['enable']) && !$item['enable']) { 26 | continue; 27 | } 28 | $processClass = $item['class']; 29 | $restartInterval = isset($item['restart_interval']) ? (int)$item['restart_interval'] : 5; 30 | $callback = function (Process $worker) use ($pidfile, $swoole, $processPrefix, $processClass, $restartInterval, $name, $laravelConfig) { 31 | file_put_contents($pidfile, $worker->pid . "\n", FILE_APPEND | LOCK_EX); 32 | $this->initLaravel($laravelConfig, $swoole); 33 | if (!isset(class_implements($processClass)[CustomProcessInterface::class])) { 34 | throw new \InvalidArgumentException( 35 | sprintf( 36 | '%s must implement the interface %s', 37 | $processClass, 38 | CustomProcessInterface::class 39 | ) 40 | ); 41 | } 42 | /**@var CustomProcessInterface $processClass */ 43 | $this->setProcessTitle(sprintf('%s laravels: %s process', $processPrefix, $name)); 44 | 45 | Process::signal(SIGUSR1, function ($signo) use ($name, $processClass, $worker, $pidfile, $swoole) { 46 | $this->info(sprintf('Reloading %s process[PID=%d].', $name, $worker->pid)); 47 | $processClass::onReload($swoole, $worker); 48 | }); 49 | 50 | if (method_exists($processClass, 'onStop')) { 51 | Process::signal(SIGTERM, function ($signo) use ($name, $processClass, $worker, $pidfile, $swoole) { 52 | $this->info(sprintf('Stopping %s process[PID=%d].', $name, $worker->pid)); 53 | $processClass::onStop($swoole, $worker); 54 | }); 55 | } 56 | 57 | if (class_exists('Swoole\Runtime')) { 58 | \Swoole\Runtime::enableCoroutine(); 59 | } 60 | 61 | $this->callWithCatchException([$processClass, 'callback'], [$swoole, $worker]); 62 | 63 | // Avoid frequent process creation 64 | if (class_exists('Swoole\Coroutine')) { 65 | \Swoole\Coroutine::sleep($restartInterval); 66 | } else { 67 | sleep($restartInterval); 68 | } 69 | }; 70 | 71 | if (isset($item['num']) && $item['num'] > 1) { // For multiple processes 72 | for ($i = 0; $i < $item['num']; $i++) { 73 | $process = $this->makeProcess($callback, $item); 74 | $swoole->addProcess($process); 75 | $processList[$name . $i] = $process; 76 | } 77 | } else { // For single process 78 | $process = $this->makeProcess($callback, $item); 79 | $swoole->addProcess($process); 80 | $processList[$name] = $process; 81 | } 82 | } 83 | return $processList; 84 | } 85 | 86 | /** 87 | * @param callable $callback 88 | * @param array $config 89 | * @return Process 90 | */ 91 | public function makeProcess(callable $callback, array $config) 92 | { 93 | $redirect = isset($config['redirect']) ? $config['redirect'] : false; 94 | $pipe = isset($config['pipe']) ? $config['pipe'] : 0; 95 | $process = version_compare(SWOOLE_VERSION, '4.3.0', '>=') 96 | ? new Process($callback, $redirect, $pipe, class_exists('Swoole\Coroutine')) 97 | : new Process($callback, $redirect, $pipe); 98 | if (isset($config['queue'])) { 99 | if (empty($config['queue'])) { 100 | $process->useQueue(); 101 | } else { 102 | $msgKey = isset($config['queue']['msg_key']) ? $config['queue']['msg_key'] : 0; 103 | $mode = isset($config['queue']['mode']) ? $config['queue']['mode'] : 2; 104 | $capacity = isset($config['queue']['capacity']) ? $config['queue']['capacity'] : -1; 105 | $process->useQueue($msgKey, $mode, $capacity); 106 | } 107 | } 108 | 109 | return $process; 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/Swoole/Process/ProcessTitleTrait.php: -------------------------------------------------------------------------------- 1 | swooleRequest = $request; 16 | } 17 | 18 | /** 19 | * Convert SwooleRequest to IlluminateRequest 20 | * @param array $rawServer 21 | * @param array $rawEnv 22 | * @return IlluminateRequest 23 | */ 24 | public function toIlluminateRequest(array $rawServer = [], array $rawEnv = []) 25 | { 26 | $__GET = isset($this->swooleRequest->get) ? $this->swooleRequest->get : []; 27 | $__POST = isset($this->swooleRequest->post) ? $this->swooleRequest->post : []; 28 | $__COOKIE = isset($this->swooleRequest->cookie) ? $this->swooleRequest->cookie : []; 29 | $server = isset($this->swooleRequest->server) ? $this->swooleRequest->server : []; 30 | $headers = isset($this->swooleRequest->header) ? $this->swooleRequest->header : []; 31 | $__FILES = isset($this->swooleRequest->files) ? $this->swooleRequest->files : []; 32 | $__CONTENT = empty($__FILES) ? $this->swooleRequest->rawContent() : ''; // Cannot call rawContent() to avoid double the file memory when uploading a file. 33 | $_REQUEST = []; 34 | $_SESSION = []; 35 | 36 | static $headerServerMapping = [ 37 | 'x-real-ip' => 'REMOTE_ADDR', 38 | 'x-real-port' => 'REMOTE_PORT', 39 | 'server-protocol' => 'SERVER_PROTOCOL', 40 | 'server-name' => 'SERVER_NAME', 41 | 'server-addr' => 'SERVER_ADDR', 42 | 'server-port' => 'SERVER_PORT', 43 | 'scheme' => 'REQUEST_SCHEME', 44 | ]; 45 | 46 | $_ENV = $rawEnv; 47 | $_SERVER = $rawServer; 48 | foreach ($headers as $key => $value) { 49 | // Fix client && server's info 50 | if (isset($headerServerMapping[$key])) { 51 | $server[$headerServerMapping[$key]] = $value; 52 | } else { 53 | $key = str_replace('-', '_', $key); 54 | $server['http_' . $key] = $value; 55 | } 56 | } 57 | $server = array_change_key_case($server, CASE_UPPER); 58 | $_SERVER = array_merge($_SERVER, $server); 59 | if (isset($_SERVER['REQUEST_SCHEME']) && $_SERVER['REQUEST_SCHEME'] === 'https') { 60 | $_SERVER['HTTPS'] = 'on'; 61 | } 62 | 63 | // Fix REQUEST_URI with QUERY_STRING 64 | if (strpos($_SERVER['REQUEST_URI'], '?') === false 65 | && isset($_SERVER['QUERY_STRING']) 66 | && $_SERVER['QUERY_STRING'] !== '' 67 | ) { 68 | $_SERVER['REQUEST_URI'] .= '?' . $_SERVER['QUERY_STRING']; 69 | } 70 | 71 | // Fix argv & argc 72 | if (!isset($_SERVER['argv'])) { 73 | $_SERVER['argv'] = isset($GLOBALS['argv']) ? $GLOBALS['argv'] : []; 74 | $_SERVER['argc'] = isset($GLOBALS['argc']) ? $GLOBALS['argc'] : 0; 75 | } 76 | 77 | // Initialize laravel request 78 | IlluminateRequest::enableHttpMethodParameterOverride(); 79 | $request = IlluminateRequest::createFromBase(new \Symfony\Component\HttpFoundation\Request($__GET, $__POST, [], $__COOKIE, $__FILES, $_SERVER, $__CONTENT)); 80 | 81 | if (0 === strpos($request->headers->get('CONTENT_TYPE', ''), 'application/x-www-form-urlencoded') 82 | && in_array(strtoupper($request->server->get('REQUEST_METHOD', 'GET')), ['PUT', 'DELETE', 'PATCH']) 83 | ) { 84 | parse_str($request->getContent(), $data); 85 | $request->request = new ParameterBag($data); 86 | } 87 | 88 | return $request; 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/Swoole/Response.php: -------------------------------------------------------------------------------- 1 | swooleResponse = $swooleResponse; 20 | $this->laravelResponse = $laravelResponse; 21 | } 22 | 23 | public function setChunkLimit($chunkLimit) 24 | { 25 | $this->chunkLimit = $chunkLimit; 26 | } 27 | 28 | public function sendStatusCode() 29 | { 30 | $this->swooleResponse->status($this->laravelResponse->getStatusCode()); 31 | } 32 | 33 | private function getHeaders() 34 | { 35 | if (method_exists($this->laravelResponse->headers, 'allPreserveCaseWithoutCookies')) { 36 | return $this->laravelResponse->headers->allPreserveCaseWithoutCookies(); 37 | } 38 | 39 | return $this->laravelResponse->headers->allPreserveCase(); 40 | } 41 | 42 | public function sendHeaders() 43 | { 44 | $headers = $this->getHeaders(); 45 | $trailers = isset($headers['trailer']) ? $headers['trailer'] : []; 46 | 47 | foreach ($headers as $name => $values) { 48 | if (in_array($name, $trailers, true)) { 49 | continue; 50 | } 51 | if (version_compare(SWOOLE_VERSION, '4.6.0', '>=')) { 52 | $this->swooleResponse->header($name, $values); 53 | } else { 54 | foreach ($values as $value) { 55 | $this->swooleResponse->header($name, $value); 56 | } 57 | } 58 | } 59 | } 60 | 61 | public function sendTrailers() 62 | { 63 | $headers = $this->getHeaders(); 64 | $trailers = isset($headers['trailer']) ? $headers['trailer'] : []; 65 | 66 | foreach ($headers as $name => $values) { 67 | if (!in_array($name, $trailers, true)) { 68 | continue; 69 | } 70 | 71 | foreach ($values as $value) { 72 | $this->swooleResponse->trailer($name, $value); 73 | } 74 | } 75 | } 76 | 77 | public function sendCookies() 78 | { 79 | $hasIsRaw = null; 80 | /**@var \Symfony\Component\HttpFoundation\Cookie[] $cookies */ 81 | $cookies = $this->laravelResponse->headers->getCookies(); 82 | foreach ($cookies as $cookie) { 83 | if ($hasIsRaw === null) { 84 | $hasIsRaw = method_exists($cookie, 'isRaw'); 85 | } 86 | $setCookie = $hasIsRaw && $cookie->isRaw() ? 'rawcookie' : 'cookie'; 87 | $this->swooleResponse->$setCookie( 88 | $cookie->getName(), 89 | $cookie->getValue(), 90 | $cookie->getExpiresTime(), 91 | $cookie->getPath(), 92 | $cookie->getDomain(), 93 | $cookie->isSecure(), 94 | $cookie->isHttpOnly() 95 | ); 96 | } 97 | } 98 | 99 | public function send($gzip = false) 100 | { 101 | $this->sendStatusCode(); 102 | $this->sendHeaders(); 103 | $this->sendCookies(); 104 | $this->sendTrailers(); 105 | if ($gzip) { 106 | $this->gzip(); 107 | } 108 | $this->sendContent(); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/Swoole/ResponseInterface.php: -------------------------------------------------------------------------------- 1 | swoolePort = $port; 14 | } 15 | } -------------------------------------------------------------------------------- /src/Swoole/Socket/HttpInterface.php: -------------------------------------------------------------------------------- 1 | swoolePort = $port; 15 | } 16 | 17 | public function onConnect(Server $server, $fd, $reactorId) 18 | { 19 | } 20 | 21 | public function onClose(Server $server, $fd, $reactorId) 22 | { 23 | } 24 | 25 | public function onBufferFull(Server $server, $fd) 26 | { 27 | } 28 | 29 | public function onBufferEmpty(Server $server, $fd) 30 | { 31 | } 32 | 33 | abstract public function onReceive(Server $server, $fd, $reactorId, $data); 34 | } -------------------------------------------------------------------------------- /src/Swoole/Socket/UdpInterface.php: -------------------------------------------------------------------------------- 1 | swoolePort = $port; 15 | } 16 | 17 | abstract public function onPacket(Server $server, $data, array $clientInfo); 18 | } -------------------------------------------------------------------------------- /src/Swoole/Socket/WebSocket.php: -------------------------------------------------------------------------------- 1 | swoolePort = $port; 14 | } 15 | } -------------------------------------------------------------------------------- /src/Swoole/Socket/WebSocketInterface.php: -------------------------------------------------------------------------------- 1 | laravelResponse->getFile(); 22 | if (!$this->laravelResponse->headers->has('Content-Type')) { 23 | $this->swooleResponse->header('Content-Type', $file->getMimeType()); 24 | } 25 | if ($this->laravelResponse->getStatusCode() == BinaryFileResponse::HTTP_NOT_MODIFIED) { 26 | $this->swooleResponse->end(); 27 | return; 28 | } 29 | 30 | $path = $file->getPathname(); 31 | $size = filesize($path); 32 | if ($size <= 0) { 33 | $this->swooleResponse->end(); 34 | return; 35 | } 36 | 37 | // Support deleteFileAfterSend: https://github.com/symfony/http-foundation/blob/5.0/BinaryFileResponse.php#L305 38 | $reflection = new \ReflectionObject($this->laravelResponse); 39 | if ($reflection->hasProperty('deleteFileAfterSend')) { 40 | $deleteFileAfterSend = $reflection->getProperty('deleteFileAfterSend'); 41 | $deleteFileAfterSend->setAccessible(true); 42 | $deleteFile = $deleteFileAfterSend->getValue($this->laravelResponse); 43 | } else { 44 | $deleteFile = false; 45 | } 46 | 47 | if ($deleteFile) { 48 | $fp = fopen($path, 'rb'); 49 | 50 | for ($offset = 0, $limit = (int)(0.99 * $this->chunkLimit); $offset < $size; $offset += $limit) { 51 | fseek($fp, $offset, SEEK_SET); 52 | $chunk = fread($fp, $limit); 53 | $this->swooleResponse->write($chunk); 54 | } 55 | $this->swooleResponse->end(); 56 | 57 | fclose($fp); 58 | 59 | if (file_exists($path)) { 60 | unlink($path); 61 | } 62 | } else { 63 | if (version_compare(SWOOLE_VERSION, '1.7.21', '<')) { 64 | throw new \RuntimeException('sendfile() require Swoole >= 1.7.21'); 65 | } 66 | $this->swooleResponse->sendfile($path); 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/Swoole/Task/BaseTask.php: -------------------------------------------------------------------------------- 1 | delay = (int)$delay; 32 | return $this; 33 | } 34 | 35 | /** 36 | * Return the delay time. 37 | * @return int 38 | */ 39 | public function getDelay() 40 | { 41 | return $this->delay; 42 | } 43 | 44 | /** 45 | * Set the number of tries. 46 | * @param int $tries 47 | * @return $this 48 | */ 49 | public function setTries($tries) 50 | { 51 | if ($tries < 1) { 52 | throw new \InvalidArgumentException('The number of attempts must be greater than or equal to 1'); 53 | } 54 | $this->tries = (int)$tries; 55 | return $this; 56 | } 57 | 58 | /** 59 | * Get the number of tries. 60 | * @return int 61 | */ 62 | public function getTries() 63 | { 64 | return $this->tries; 65 | } 66 | 67 | /** 68 | * Deliver a task 69 | * @param mixed $task The task object 70 | * @return bool 71 | */ 72 | protected function task($task) 73 | { 74 | static $dispatch; 75 | if (!$dispatch) { 76 | $dispatch = static function ($task) { 77 | /**@var \Swoole\Http\Server $swoole */ 78 | $swoole = app('swoole'); 79 | // The worker_id of timer process is -1 80 | if ($swoole->worker_id === -1 || $swoole->taskworker) { 81 | $workerNum = isset($swoole->setting['worker_num']) ? $swoole->setting['worker_num'] : 0; 82 | $availableId = mt_rand(0, $workerNum - 1); 83 | return $swoole->sendMessage($task, $availableId); 84 | } 85 | $taskId = $swoole->task($task); 86 | return $taskId !== false; 87 | }; 88 | } 89 | 90 | if ($this->delay > 0) { 91 | Timer::after($this->delay * 1000, $dispatch, $task); 92 | return true; 93 | } 94 | 95 | return $dispatch($task); 96 | } 97 | } -------------------------------------------------------------------------------- /src/Swoole/Task/Event.php: -------------------------------------------------------------------------------- 1 | listeners; 24 | } 25 | 26 | /** 27 | * Trigger an event 28 | * @param Event $event 29 | * @return bool 30 | */ 31 | public static function fire(self $event) 32 | { 33 | return $event->task($event); 34 | } 35 | } -------------------------------------------------------------------------------- /src/Swoole/Task/Listener.php: -------------------------------------------------------------------------------- 1 | task($task); 25 | } 26 | } -------------------------------------------------------------------------------- /src/Swoole/Timer/BackupCronJob.php: -------------------------------------------------------------------------------- 1 | interval = $config[0]; 58 | } 59 | if (isset($config[1])) { 60 | $this->isImmediate = $config[1]; 61 | } 62 | } 63 | } 64 | 65 | /** 66 | * @return int 67 | */ 68 | public function interval() 69 | { 70 | return $this->interval; 71 | } 72 | 73 | /** 74 | * @return bool 75 | */ 76 | public function isImmediate() 77 | { 78 | return $this->isImmediate; 79 | } 80 | 81 | public function setTimerId($timerId) 82 | { 83 | $this->timerId = $timerId; 84 | } 85 | 86 | public function stop() 87 | { 88 | if ($this->timerId && Timer::exists($this->timerId)) { 89 | Timer::clear($this->timerId); 90 | } 91 | } 92 | 93 | public static function getGlobalTimerCacheKey() 94 | { 95 | return 'laravels:timer:' . strtolower(self::$globalTimerLockKey); 96 | } 97 | 98 | public static function getGlobalTimerLock() 99 | { 100 | /**@var \Illuminate\Redis\RedisManager $redis */ 101 | $redis = app('redis'); 102 | 103 | $key = self::getGlobalTimerCacheKey(); 104 | $value = self::getCurrentInstanceId(); 105 | $expire = self::GLOBAL_TIMER_LOCK_SECONDS; 106 | $result = $redis->set($key, $value, 'ex', $expire, 'nx'); 107 | // Compatible with Predis and PhpRedis 108 | return $result === true || ((string)$result === 'OK'); 109 | } 110 | 111 | protected static function getCurrentInstanceId() 112 | { 113 | return sprintf('%s:%d', current(swoole_get_local_ip()) ?: gethostname(), config('laravels.listen_port')); 114 | } 115 | 116 | public static function isGlobalTimerAlive() 117 | { 118 | /**@var \Redis|\RedisCluster|\Predis\Client $redis */ 119 | $redis = app('redis')->client(); // Fix: Redis exists() always returns false on cluster mode for some older versions of Laravel/Lumen, see https://github.com/illuminate/redis/commit/62ff6a06a9c91902d3baa7feda20bab5e807606f 120 | return (bool)$redis->exists(self::getGlobalTimerCacheKey()); 121 | } 122 | 123 | public static function isCurrentTimerAlive() 124 | { 125 | /**@var \Illuminate\Redis\RedisManager $redis */ 126 | $redis = app('redis'); 127 | $key = self::getGlobalTimerCacheKey(); 128 | $instanceId = $redis->get($key); 129 | return $instanceId === self::getCurrentInstanceId(); 130 | } 131 | 132 | public static function renewGlobalTimerLock($expire) 133 | { 134 | /**@var \Illuminate\Redis\RedisManager $redis */ 135 | $redis = app('redis'); 136 | return (bool)$redis->expire(self::getGlobalTimerCacheKey(), $expire); 137 | } 138 | 139 | public static function setGlobalTimerLockKey($lockKey) 140 | { 141 | self::$globalTimerLockKey = $lockKey; 142 | } 143 | 144 | public static function checkSetEnable() 145 | { 146 | if (self::isGlobalTimerAlive()) { 147 | // Reset current timer to avoid repeated execution 148 | self::setEnable(self::isCurrentTimerAlive()); 149 | } else { 150 | // Compete for timer lock 151 | self::setEnable(self::getGlobalTimerLock()); 152 | } 153 | } 154 | 155 | public static function setEnable($enable) 156 | { 157 | self::$enable = (bool)$enable; 158 | } 159 | 160 | public static function isEnable() 161 | { 162 | return self::$enable; 163 | } 164 | } -------------------------------------------------------------------------------- /src/Swoole/Timer/CronJobInterface.php: -------------------------------------------------------------------------------- 1 | setting['pid_file']) . '/' . $this->timerPidFile; 30 | file_put_contents($pidfile, $process->pid); 31 | $this->setProcessTitle(sprintf('%s laravels: timer process', $config['process_prefix'])); 32 | $this->initLaravel($laravelConfig, $swoole); 33 | 34 | // Implement global timer by Cache lock. 35 | if (!empty($config['global_lock'])) { 36 | CronJob::setGlobalTimerLockKey($config['global_lock_key']); 37 | CronJob::checkSetEnable(); 38 | } 39 | 40 | $timerIds = $this->registerTimers($config['jobs']); 41 | 42 | Process::signal(SIGUSR1, function ($signo) use ($config, $timerIds, $process) { 43 | foreach ($timerIds as $timerId) { 44 | if (Timer::exists($timerId)) { 45 | Timer::clear($timerId); 46 | } 47 | } 48 | Timer::after($config['max_wait_time'] * 1000, function () use ($process) { 49 | $process->exit(0); 50 | }); 51 | }); 52 | // For Swoole 4.6.x 53 | // Deprecated: Swoole\Event::rshutdown(): Event::wait() in shutdown function is deprecated in Unknown on line 0 54 | Event::wait(); 55 | }; 56 | 57 | $process = new Process($callback, false, 0); 58 | $swoole->addProcess($process); 59 | return $process; 60 | } 61 | 62 | public function registerTimers(array $jobs) 63 | { 64 | $timerIds = []; 65 | foreach ($jobs as $jobClass) { 66 | if (is_array($jobClass) && isset($jobClass[0])) { 67 | $job = new $jobClass[0](isset($jobClass[1]) ? $jobClass[1] : []); 68 | } else { 69 | $job = new $jobClass(); 70 | } 71 | if (!($job instanceof CronJob)) { 72 | throw new \InvalidArgumentException(sprintf( 73 | '%s must extend the abstract class %s', 74 | get_class($job), 75 | CronJob::class 76 | ) 77 | ); 78 | } 79 | if (empty($job->interval())) { 80 | throw new \InvalidArgumentException(sprintf('The interval of %s cannot be empty', get_class($job))); 81 | } 82 | $runJob = function () use ($job) { 83 | $runCallback = function () use ($job) { 84 | $this->callWithCatchException(function () use ($job) { 85 | if (($job instanceof CheckGlobalTimerAliveCronJob) || $job::isEnable()) { 86 | $job->run(); 87 | } 88 | }); 89 | }; 90 | class_exists('Swoole\Coroutine') ? \Swoole\Coroutine::create($runCallback) : $runCallback(); 91 | }; 92 | 93 | $timerId = Timer::tick($job->interval(), $runJob); 94 | $timerIds[] = $timerId; 95 | $job->setTimerId($timerId); 96 | if ($job->isImmediate()) { 97 | Timer::after(1, $runJob); 98 | } 99 | } 100 | return $timerIds; 101 | } 102 | } -------------------------------------------------------------------------------- /src/Swoole/WebSocketHandlerInterface.php: -------------------------------------------------------------------------------- 1 | httpGet('http://httpbin.org/get', ['timeout' => 3]); 14 | $this->assertIsArray($response); 15 | $this->assertArrayHasKey('body', $response); 16 | $body = $response['body']; 17 | $json = json_decode($body, true); 18 | $this->assertIsArray($json); 19 | } 20 | } -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | &1 | grep 'Requests per second' | awk '{print $4}') 13 | echo "TEST#${i}: ${qps:-"-"}" 14 | total=$(awk "BEGIN{print ${total}+${qps:-0}}") 15 | done 16 | echo "AVG QPS:" $(awk "BEGIN{printf \"%.3f\", ${total}/${rc}}") 17 | --------------------------------------------------------------------------------