├── .gitignore ├── changelog.md ├── composer.json ├── config ├── monitor.php ├── schedule.php ├── server.php ├── starter.php └── web.php ├── contributing.md ├── fixes ├── WorkmanFunctions.php ├── fix-symfony-file-moving.php ├── fix-symfony-file-validation.php └── fix-symfony-stdout.php ├── license.md ├── phpunit.xml ├── readme.md └── src ├── Command ├── ConfigProxy.php ├── Configs.php ├── Laraman.php └── Process.php ├── Events ├── MessageDone.php ├── MessageReceived.php ├── RequestReceived.php └── TaskReceived.php ├── Exceptions └── FileException.php ├── Http ├── Request.php └── Response.php ├── LaramanServiceProvider.php ├── Listeners ├── CleanBaseState.php ├── CleanWebState.php └── OwlAdminExtensionChanged.php ├── Process ├── Monitor.php ├── ProcessBase.php ├── Schedule.php └── Web.php ├── Server ├── LaramanApp.php ├── LaramanKernel.php ├── LaramanWorker.php └── StaticFileServer.php └── Traits ├── HasBaseState.php ├── HasCleanMode.php ├── HasLaravelApplication.php ├── HasRefreshTelescope.php ├── HasWebState.php ├── HasWorkermanBuilder.php └── HasWorkermanEvents.php /.gitignore: -------------------------------------------------------------------------------- 1 | ### Laravel template 2 | /vendor/ 3 | node_modules/ 4 | npm-debug.log 5 | yarn-error.log 6 | 7 | # Laravel 4 specific 8 | bootstrap/compiled.php 9 | app/storage/ 10 | 11 | # Laravel 5 & Lumen specific 12 | public/storage 13 | public/hot 14 | 15 | # Laravel 5 & Lumen specific with changed public path 16 | public_html/storage 17 | public_html/hot 18 | 19 | storage/*.key 20 | .env 21 | Homestead.yaml 22 | Homestead.json 23 | /.vagrant 24 | .phpunit.result.cache 25 | 26 | # dont limit version 27 | composer.lock 28 | 29 | .idea 30 | 31 | -------------------------------------------------------------------------------- /changelog.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to `Laraman` will be documented in this file. 4 | 5 | ## version 2.0.5 6 | feat:add schedule process 7 | 8 | ## version 2.0.4 9 | fix:start up script 10 | 11 | ## version 2.0.3 12 | feat:add support for owl-admin , try to detect owl-admin or dcat-admin and make auto support 13 | 14 | ## version 2.0.1 15 | fix:single process arg detect for boot 16 | 17 | ## version 2.0.0 released 18 | please force update if old version installed 19 | refactor the start engine 20 | fix upload file bug 21 | add default page feature 22 | 23 | 24 | ## version 1.0.0 released 25 | please force update if old version installed 26 | 27 | ## version 0.1.0 28 | this is prerelease version 29 | solved problem that can not start -d mode in linux 30 | 31 | ## version 0.0.7 32 | - bugfix:error count config for process number 33 | - improve:add database heartbeat config 34 | 35 | ## version 0.0.6 36 | - improve:add database heartbeat config 37 | - modify:split process config file to standalone 38 | 39 | ## version 0.0.5 40 | - move methods to traits 41 | - add event-listener mode 42 | - add dcat-admin support 43 | - fix bug when response give an unknown statue-code 44 | 45 | ## Version 0.0.4 46 | 47 | ### feat 48 | - add clean mode to support unknown app 49 | 50 | ## Version 0.0.3 51 | 52 | ### feat 53 | - Move pid_file,status_file,log_file,event_loop,stop_timeout to server.php as public config 54 | - fix and improve config init when bootstrap 55 | - add support for custom protocol 56 | - change onHttpMessage param from workerman request to laravel request 57 | 58 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "itinysun/laraman", 3 | "description": "Run laravel with workman ,1 artisan command, 10x speed up", 4 | "license": "MIT", 5 | "authors": [ 6 | { 7 | "name": "itinysun", 8 | "email": "860760361@qq.com", 9 | "homepage": "https://github.com/itinysun" 10 | } 11 | ], 12 | "homepage": "https://github.com/itinysun/laraman", 13 | "keywords": ["Laravel", "Laraman"], 14 | "require": { 15 | "php": ">=8.0", 16 | "workerman/workerman": "^4.0.4 || ^5.0.0", 17 | "laravel/framework": "^9.0 || ^10.0" 18 | }, 19 | "require-dev": { 20 | 21 | }, 22 | "autoload": { 23 | "psr-4": { 24 | "Itinysun\\Laraman\\": "src/" 25 | } 26 | }, 27 | "suggest": { 28 | "ext-event": "For better performance. " 29 | }, 30 | "autoload-dev": { 31 | "psr-4": { 32 | "Itinysun\\Laraman\\Tests\\": "tests" 33 | } 34 | }, 35 | "extra": { 36 | "laravel": { 37 | "providers": [ 38 | "Itinysun\\Laraman\\LaramanServiceProvider" 39 | ] 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /config/monitor.php: -------------------------------------------------------------------------------- 1 | [ 9 | * 'handler'=>class name, 必须,写进程类名。必须继承 ProcessBase 类 10 | * 'options'= [ 进程类构造函数的参数,用于进程内部使用 11 | * 'events'=>[event name =>array(listeners)] 可选,订阅事件。 12 | * 'clearMode'=>bool , 可选,默认false,是否开启洁癖模式 13 | * ], 14 | * 'workerman'=>array config 可选,构造 worker时的参数,参考workerman官方手册,如果是全局属性,请在server中配置 15 | 16 | * ] 17 | * 18 | */ 19 | return [ 20 | 'handler' => Monitor::class, 21 | 'workerman'=>[ 22 | 'reloadable' => false, 23 | ], 24 | 'options' => [ 25 | // Monitor these directories 26 | 'monitorDir' => [ 27 | app_path(), 28 | config_path(), 29 | base_path() . '/packages', 30 | base_path() . '/resources', 31 | base_path() . '/routes', 32 | base_path() . '/.env', 33 | ], 34 | // Files with these suffixes will be monitored 35 | 'monitorExtensions' => [ 36 | 'php', 'html', 'htm', 'env' 37 | ], 38 | //是否启用文件监控 需要非demon模式 39 | 'enable_file_monitor' => true, 40 | //是否启用占用内存监控 需要linux系统 41 | 'enable_memory_monitor' => true, 42 | ] 43 | ]; 44 | -------------------------------------------------------------------------------- /config/schedule.php: -------------------------------------------------------------------------------- 1 | \Itinysun\Laraman\Process\Schedule::class, 4 | 'workerman'=>[ 5 | 'reloadable' => false, 6 | ], 7 | 'options' => [ 8 | 'interval'=>60, 9 | 'timeout'=>60 10 | ] 11 | ]; 12 | -------------------------------------------------------------------------------- /config/server.php: -------------------------------------------------------------------------------- 1 | [ 12 | 'web','monitor' 13 | ], 14 | 15 | //是否支持端口复用,如果你需要不同process共用1个端口,请设置为true 16 | 'reusePort' => false, 17 | 18 | //本程序运行目录,需要有读写权限,默认放入laravel的存储路径 19 | 'runtime_path' => storage_path('laraman'), 20 | 21 | /* 22 | * 以下是 worker 全局唯一设定 23 | * @link https://www.workerman.net/doc/workerman/worker/pid-file.html 24 | */ 25 | 'pid_file' => storage_path('laraman') . '/web.pid', 26 | 'status_file' => storage_path('laraman'). '/web.status', 27 | 'stdout_file' => storage_path('laraman') . '/web_stdout.log', 28 | 'log_file' => storage_path('laraman'). '/web.log', 29 | 30 | 'event_loop' => '', 31 | 32 | /* 33 | * 平滑结束超时时间,单位为秒。 34 | * 当进程收到结束、重启信号后,等待当前任务(如果有)执行完毕的最长时间 35 | * 当进程的reloadable启用时需要配置此选项 36 | */ 37 | 'stop_timeout' => 2, 38 | 39 | /* 40 | * 以下是 connection 全局唯一设定 41 | */ 42 | 'max_package_size' => 10 * 1024 * 1024, 43 | 44 | 45 | //是否以守护模式运行,windows无效 等效 -d 46 | 'daemonize' => false, 47 | 48 | ]; 49 | -------------------------------------------------------------------------------- /config/starter.php: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | getMessage() . "\n"; 42 | echo "异常代码:" . $e->getCode() . "\n"; 43 | echo "异常文件:" . $e->getFile() . "\n"; 44 | echo "异常行号:" . $e->getLine() . "\n"; 45 | echo "异常追踪:" . $e->getTraceAsString() . "\n"; 46 | } 47 | -------------------------------------------------------------------------------- /config/web.php: -------------------------------------------------------------------------------- 1 | [ 15 | * 'handler'=>class name, 必须,写进程类名。必须继承 ProcessBase 类 16 | * 'options'= [ 进程类构造函数的参数,用于进程内部使用 17 | * 'events'=>[event name =>array(listeners)] 可选,订阅事件。 18 | * 'clearMode'=>bool , 可选,默认false,是否开启洁癖模式 19 | * ], 20 | * 'workerman'=>array config 可选,构造 worker时的参数,参考workerman官方手册,如果是全局属性,请在server中配置 21 | 22 | * ] 23 | * 24 | */ 25 | return [ 26 | 'workerman' => [ 27 | 28 | 'listen' => env('LARAMAN_WEB_LISTEN', 'http://127.0.0.1:8000'), 29 | 30 | 'transport' => 'tcp', 31 | 32 | 'context' => [], 33 | 34 | 'name' => 'laraman', 35 | 36 | /* 37 | * process需要的子进程数量,windows无效 38 | * if not set or in windows,value should always be 1 39 | * if set to empty thing , value should be 4 times of cpu_count() 40 | * */ 41 | 'count' => env('LARAMAN_WEB_WORKERS', 0), 42 | 43 | //是否支持平滑重启,如果是持续性任务,请设置为false 44 | 'reloadable' => true, 45 | 46 | 47 | //运行命令使用的用户及组,windows无效 48 | 'user' => '', 49 | 'group' => '', 50 | ], 51 | 'options' => [ 52 | /* 53 | * 洁癖模式 54 | * 为了兼容未知内容,可以开启洁癖模式,除了laravel原生自带的服务,所有其他服务均被标记为scoped,然后在每次请求后自动销毁 55 | * 如果有应用是在laravel启动时进行动态加载一些参数到服务中,可能因服务重新启动导致这些参数丢失,例如owl-admin 56 | */ 57 | 'clearMode' => false, 58 | 59 | /* 60 | * 数据库心跳 61 | * 单位为秒,0 为禁用心跳 62 | * laravel有数据重连机制,所以如果没有出现问题,可以不用打开这个 63 | * */ 64 | 'db_heartbeat_interval'=>0, 65 | 66 | /* 67 | * 事件绑定 68 | * 可以进行自定义处理,请尽量不要抛出异常 69 | */ 70 | 'events' => [ 71 | /* 72 | * 接受到请求后的事件,适用于WEB进程 73 | * 这会在接收到HTTP请求,进行处理之前触发 74 | * 已经按照octance进行了兼容,已自动兼容owl-admin和dcat-admin 75 | */ 76 | RequestReceived::class => [ 77 | CleanBaseState::class, 78 | CleanWebState::class, 79 | ], 80 | //兼容octance保留待用 81 | TaskReceived::class => [ 82 | 83 | ], 84 | /* 85 | * 对于非WEB请求,请使用如下两个事件 86 | */ 87 | MessageReceived::class=>[ 88 | 89 | ], 90 | MessageDone::class=>[ 91 | 92 | ], 93 | 94 | ], 95 | /*静态文件配置*/ 96 | 'static_file' => [ 97 | //是否启用静态文件服务器,如果不启用,无法访问 css\js\img 等静态文件 98 | 'enable' => true, 99 | /* 100 | * 允许访问的路径,可以添加多个。 101 | * 注意,此选项非常 非常 非常 重要。请确保这些目录不会包含敏感信息 102 | */ 103 | 'allowed' => [ 104 | public_path() 105 | ], 106 | //是否支持获取php文件结果,如果启用则获取运行后结果,如果不启用则返回错误 107 | 'support_php' => false, 108 | 109 | /* 110 | * 默认页面文件,当访问一个路径时,会尝试查找下面的默认页面文件 111 | * 请按照从上到下优先级来写 112 | * 如果不设置也不会列出目录 113 | * 如果是php文件,必须开启上面的support_php 114 | */ 115 | 'defaultPage'=>[ 116 | 'index.html', 117 | //'index.php', 118 | ] 119 | ] 120 | ], 121 | 'handler' => Web::class 122 | ]; 123 | -------------------------------------------------------------------------------- /contributing.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Contributions are welcome and will be fully credited. 4 | 5 | Contributions are accepted via Pull Requests on [Github](https://github.com/itinysun/laraman). 6 | 7 | 8 | ## Pull Requests 9 | 10 | - **Add tests!** - Your patch won't be accepted if it doesn't have tests. 11 | 12 | - **Document any change in behaviour** - Make sure the `readme.md` and any other relevant documentation are kept up-to-date. 13 | 14 | 15 | - **One pull request per feature** - If you want to do more than one thing, send multiple pull requests. 16 | 17 | 18 | 19 | **Happy coding**! 20 | -------------------------------------------------------------------------------- /fixes/WorkmanFunctions.php: -------------------------------------------------------------------------------- 1 | $v) { 36 | $keyWords = explode('-', $key); 37 | foreach ($keyWords as &$k) 38 | $k = ucwords($k); 39 | $newKey = implode('-', $keyWords); 40 | $arr[$newKey] = $v; 41 | } 42 | return $arr; 43 | } 44 | 45 | function initWorkerConfig(array $config): void 46 | { 47 | $staticPropertyMap = [ 48 | 'pid_file', 49 | 'status_file', 50 | 'log_file' 51 | ]; 52 | 53 | foreach ($staticPropertyMap as $property) { 54 | try { 55 | $path = $config[$property] ?? []; 56 | if (!empty($path)) { 57 | $dir = dirname($path); 58 | make_dir($dir); 59 | } 60 | } catch (\Throwable $e) { 61 | echo('Failed to create runtime logs directory. Please check the permission.'); 62 | throw $e; 63 | } 64 | } 65 | 66 | Worker::$logFile = $config['log_file'] ?? ''; 67 | 68 | TcpConnection::$defaultMaxPackageSize = $config['max_package_size'] ?? 10 * 1024 * 1024; 69 | 70 | 71 | if (property_exists(Worker::class, 'stopTimeout')) { 72 | Worker::$stopTimeout = $config['stop_timeout'] ?? 2; 73 | } 74 | 75 | if (!isWindows()) { 76 | Worker::$onMasterReload = function () { 77 | if (function_exists('opcache_get_status') && function_exists('opcache_invalidate')) { 78 | if ($status = \opcache_get_status()) { 79 | if (isset($status['scripts']) && $scripts = $status['scripts']) { 80 | foreach (array_keys($scripts) as $file) { 81 | \opcache_invalidate($file, true); 82 | } 83 | } 84 | } 85 | } 86 | }; 87 | 88 | Worker::$pidFile = $config['pid_file'] ?? ''; 89 | Worker::$eventLoopClass = $config['event_loop'] ?? ''; 90 | if (property_exists(Worker::class, 'statusFile')) { 91 | Worker::$statusFile = $config['status_file'] ?? ''; 92 | } 93 | if (property_exists(Worker::class, 'stopTimeout')) { 94 | Worker::$stopTimeout = $config['stop_timeout'] ?? 2; 95 | } 96 | } 97 | } 98 | 99 | 100 | /** 101 | * 事件绑定 102 | * @param $worker 103 | * @param $class 104 | * @throws ReflectionException 105 | */ 106 | function bindWorkerEvents($worker, $class): void 107 | { 108 | $callbackMap = [ 109 | 'onConnect', 110 | 'onClose', 111 | 'onError', 112 | 'onBufferFull', 113 | 'onBufferDrain', 114 | 'onWorkerStop', 115 | 'onWebSocketConnect', 116 | 'onWorkerReload' 117 | ]; 118 | 119 | $methods = getWorkerCallBacks($class); 120 | 121 | foreach ($callbackMap as $callback) { 122 | if (in_array($callback, $methods)) { 123 | $worker->$callback = [$class, '_' . $callback]; 124 | } 125 | } 126 | if (in_array('onHttpMessage', $methods) || in_array('onTextMessage', $methods) || in_array('onCustomMessage', $methods)) { 127 | $worker->onMessage = [$class, '_onMessage']; 128 | } 129 | call_user_func([$class, '_onWorkerStart'], $worker); 130 | } 131 | 132 | /** 133 | * 获取类型自有的原生事件方法 134 | * @param $class 135 | * @return array 136 | */ 137 | function getWorkerCallBacks($class): array 138 | { 139 | try { 140 | $ref = new ReflectionClass($class); 141 | $methods = $ref->getMethods(); 142 | $result = []; 143 | foreach ($methods as $m) { 144 | if ($m->class == $ref->name && str_starts_with($m->name, 'on')) { 145 | $result[] = $m->name; 146 | } 147 | } 148 | return $result; 149 | } catch (Exception $e) { 150 | return []; 151 | } 152 | } 153 | 154 | /** 155 | * Start worker 156 | * @param string $configName 157 | * @param string|null $processName 158 | * @throws Exception 159 | */ 160 | function startProcessWithName(string $configName, string $processName = null): void 161 | { 162 | if (!$processName) 163 | $processName = $configName; 164 | 165 | $config = Configs::get($configName); 166 | 167 | if (empty($config)) 168 | throw new Exception('process config not found for ' . $configName); 169 | 170 | if (!class_exists($config['handler'])) { 171 | throw new Exception("process error: class {$config['handler']} not exists"); 172 | } 173 | 174 | $worker = call_user_func([$config['handler'], 'buildWorker'], $configName, $processName); 175 | 176 | $worker->onWorkerStart = function ($worker) use ($config) { 177 | 178 | if (Arr::has($config, 'workerman.listen')) { 179 | register_shutdown_function(function ($startTime) { 180 | if (time() - $startTime <= 0.1) { 181 | sleep(1); 182 | } 183 | }, time()); 184 | } 185 | 186 | $instance = new $config['handler']($config['options'] ?? []); 187 | bindWorkerEvents($worker, $instance); 188 | }; 189 | } 190 | 191 | function cpu_count(): int 192 | { 193 | // Windows does not support the number of processes setting. 194 | if (isWindows()) { 195 | return 1; 196 | } 197 | $count = 4; 198 | if (\is_callable('shell_exec')) { 199 | if (\strtolower(PHP_OS) === 'darwin') { 200 | $count = (int)\shell_exec('sysctl -n machdep.cpu.core_count'); 201 | } else { 202 | $count = (int)\shell_exec('nproc'); 203 | } 204 | } 205 | return $count > 0 ? $count : 4; 206 | } 207 | 208 | -------------------------------------------------------------------------------- /fixes/fix-symfony-file-moving.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Console\Output; 13 | 14 | use Exception; 15 | use Itinysun\Laraman\Server\LaramanWorker; 16 | use Symfony\Component\Console\Formatter\OutputFormatterInterface; 17 | use Itinysun\Laraman\Command\Configs; 18 | 19 | /** 20 | * ConsoleOutput is the default class for all CLI output. It uses STDOUT and STDERR. 21 | * 22 | * This class is a convenient wrapper around `StreamOutput` for both STDOUT and STDERR. 23 | * 24 | * $output = new ConsoleOutput(); 25 | * 26 | * This is equivalent to: 27 | * 28 | * $output = new StreamOutput(fopen('php://stdout', 'w')); 29 | * $stdErr = new StreamOutput(fopen('php://stderr', 'w')); 30 | * 31 | * @author Fabien Potencier 32 | */ 33 | class ConsoleOutputFix extends StreamOutput implements ConsoleOutputInterface 34 | { 35 | private OutputInterface $stderr; 36 | private array $consoleSectionOutputs = []; 37 | 38 | private static bool|null $daemonize = null; 39 | 40 | /** 41 | * @param int $verbosity The verbosity level (one of the VERBOSITY constants in OutputInterface) 42 | * @param bool|null $decorated Whether to decorate messages (null for auto-guessing) 43 | * @param OutputFormatterInterface|null $formatter Output formatter instance (null to use default OutputFormatter) 44 | */ 45 | public function __construct(int $verbosity = self::VERBOSITY_NORMAL, bool $decorated = null, OutputFormatterInterface $formatter = null) 46 | { 47 | parent::__construct($this->openOutputStream(), $verbosity, $decorated, $formatter); 48 | 49 | if (null === $formatter) { 50 | // for BC reasons, stdErr has it own Formatter only when user don't inject a specific formatter. 51 | $this->stderr = new StreamOutput($this->openErrorStream(), $verbosity, $decorated); 52 | 53 | return; 54 | } 55 | 56 | $actualDecorated = $this->isDecorated(); 57 | $this->stderr = new StreamOutput($this->openErrorStream(), $verbosity, $decorated, $this->getFormatter()); 58 | 59 | if (null === $decorated) { 60 | $this->setDecorated($actualDecorated && $this->stderr->isDecorated()); 61 | } 62 | } 63 | 64 | /** 65 | * Creates a new output section. 66 | */ 67 | public function section(): ConsoleSectionOutput 68 | { 69 | return new ConsoleSectionOutput($this->getStream(), $this->consoleSectionOutputs, $this->getVerbosity(), $this->isDecorated(), $this->getFormatter()); 70 | } 71 | 72 | public function setDecorated(bool $decorated) 73 | { 74 | parent::setDecorated($decorated); 75 | $this->stderr->setDecorated($decorated); 76 | } 77 | 78 | public function setFormatter(OutputFormatterInterface $formatter) 79 | { 80 | parent::setFormatter($formatter); 81 | $this->stderr->setFormatter($formatter); 82 | } 83 | 84 | public function setVerbosity(int $level) 85 | { 86 | parent::setVerbosity($level); 87 | $this->stderr->setVerbosity($level); 88 | } 89 | 90 | public function getErrorOutput(): OutputInterface 91 | { 92 | return $this->stderr; 93 | } 94 | 95 | public function setErrorOutput(OutputInterface $error) 96 | { 97 | $this->stderr = $error; 98 | } 99 | 100 | /** 101 | * Returns true if current environment supports writing console output to 102 | * STDOUT. 103 | */ 104 | protected function hasStdoutSupport(): bool 105 | { 106 | return !LaramanWorker::$daemonize; 107 | } 108 | 109 | 110 | /** 111 | * @return resource 112 | * @throws Exception 113 | */ 114 | private function openOutputStream() 115 | { 116 | if (!$this->hasStdoutSupport()) { 117 | return fopen(Configs::runtimePath('laravel-out.log'), 'a'); 118 | } 119 | 120 | // Use STDOUT when possible to prevent from opening too many file descriptors 121 | return \defined('STDOUT') ? \STDOUT : (@fopen('php://stdout', 'w') ?: fopen('php://output', 'w')); 122 | } 123 | 124 | /** 125 | * @return resource 126 | * @throws Exception 127 | */ 128 | private function openErrorStream() 129 | { 130 | if (!$this->hasStdoutSupport()) { 131 | return fopen(Configs::runtimePath('laravel-err.log'), 'a'); 132 | } 133 | 134 | // Use STDERR when possible to prevent from opening too many file descriptors 135 | return \defined('STDERR') ? \STDERR : (@fopen('php://stderr', 'w') ?: fopen('php://output', 'w')); 136 | } 137 | } -------------------------------------------------------------------------------- /license.md: -------------------------------------------------------------------------------- 1 | # The license 2 | 3 | Copyright (c) itinysun <860760361@qq.com> 4 | 5 | ...Add your license text here... -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 14 | ./tests/ 15 | 16 | 17 | 18 | 19 | src/ 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Laraman 2 | 3 | [![Latest Version on Packagist][ico-version]][link-packagist] 4 | [![Total Downloads][ico-downloads]][link-downloads] 5 | 6 | 7 | Run laravel with workman , 1 artisan command, 10x speed up 8 | 9 | v2.0.6 released ! 10 | 11 | _support dcat/admin and owl-admin now_ 12 | 13 | 14 | ## Installation 15 | 16 | Via Composer 17 | 18 | ``` bash 19 | 20 | # install package 21 | composer require itinysun/laraman 22 | 23 | 24 | # install publish file 25 | php artisan vendor:publish --tag=laraman.install 26 | 27 | # update publish as needed 28 | 29 | php artisan vendor:publish --tag=laraman.install --force 30 | ``` 31 | 32 | ## Usage 33 | 34 | ```php 35 | 36 | //run 37 | php laraman 38 | 39 | //run a custom process 40 | php larman process {process name} 41 | 42 | 43 | //config/laraman/server.php 44 | //this is for auto start process name,web for inner build web server , 45 | // monitor for hot reload after edit ,only enable under debug mode 46 | // see process config in process.php 47 | 'processes'=>[ 48 | 'web','monitor' 49 | ] 50 | 51 | 52 | 53 | 54 | ``` 55 | ## how to write a custom process 56 | create a new class extend Itinysun\Laraman\Process 57 | create a new config file in config/laraman/ 58 | if it needs auto start , add config name in server.php 59 | 60 | 61 | 62 | ## Change log 63 | 64 | Please see the [changelog](changelog.md) for more information on what has changed recently. 65 | 66 | 67 | 68 | ## Contributing 69 | 70 | Please see [contributing.md](contributing.md) for details and a todolist. 71 | 72 | ## Security 73 | 74 | If you discover any security related issues, please email 860760361@qq.com instead of using the issue tracker. 75 | 76 | ## Credits 77 | 78 | - [itinysun][link-author] 79 | - [All Contributors][link-contributors] 80 | 81 | ## License 82 | 83 | MIT 84 | 85 | [ico-version]: https://img.shields.io/packagist/v/itinysun/laraman.svg?style=flat-square 86 | [ico-downloads]: https://img.shields.io/packagist/dt/itinysun/laraman.svg?style=flat-square 87 | [ico-travis]: https://img.shields.io/travis/itinysun/laraman/master.svg?style=flat-square 88 | [ico-styleci]: https://styleci.io/repos/12345678/shield 89 | 90 | [link-packagist]: https://packagist.org/packages/itinysun/laraman 91 | [link-downloads]: https://packagist.org/packages/itinysun/laraman 92 | [link-travis]: https://travis-ci.org/itinysun/laraman 93 | [link-styleci]: https://styleci.io/repos/12345678 94 | [link-author]: https://github.com/itinysun 95 | [link-contributors]: ../../contributors 96 | -------------------------------------------------------------------------------- /src/Command/ConfigProxy.php: -------------------------------------------------------------------------------- 1 | checkAllFilesChange()) { 53 | $status = proc_get_status($resource); 54 | $pid = $status['pid']; 55 | shell_exec("taskkill /F /T /PID $pid"); 56 | proc_close($resource); 57 | $resource = self::open_processes($processFiles); 58 | } 59 | } 60 | } else { 61 | /*仅当配置文件设置为true时才主动配置此选项,这样在用户手动输入-d时也可生效*/ 62 | if($config['daemonize']){ 63 | LaramanWorker::$daemonize=true; 64 | } 65 | foreach ($processes as $process) { 66 | startProcessWithName($process); 67 | } 68 | } 69 | 70 | LaramanWorker::runAll(); 71 | return 1; 72 | } 73 | 74 | protected static function open_processes($processFiles) 75 | { 76 | $cmd = '"' . PHP_BINARY . '" ' . implode(' ', $processFiles); 77 | $descriptors = [STDIN, STDOUT, STDOUT]; 78 | $resource = proc_open($cmd, $descriptors, $pipes, null, null, ['bypass_shell' => true]); 79 | if (!$resource) { 80 | exit("Can not execute $cmd\r\n"); 81 | } 82 | return $resource; 83 | } 84 | 85 | /** 86 | * @throws Exception 87 | */ 88 | protected static function buildBootstrapWindows($processName): string 89 | { 90 | $basePath = Configs::getBasePath(); 91 | $fileContent = << 10 | * @copyright walkor 11 | * @link http://www.workerman.net/ 12 | * @license http://www.opensource.org/licenses/mit-license.php MIT License 13 | */ 14 | 15 | namespace Itinysun\Laraman\Exceptions; 16 | 17 | use RuntimeException; 18 | 19 | /** 20 | * Class FileException 21 | * @package Webman\Exception 22 | */ 23 | class FileException extends RuntimeException 24 | { 25 | } 26 | -------------------------------------------------------------------------------- /src/Http/Request.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright walkor 11 | * @link http://www.workerman.net/ 12 | * @license http://www.opensource.org/licenses/mit-license.php MIT License 13 | */ 14 | 15 | namespace Itinysun\Laraman\Http; 16 | use Symfony\Component\HttpFoundation\File\UploadedFile; 17 | use Workerman\Protocols\Http\Request as WorkmanRequest; 18 | 19 | /** 20 | * Class Request 21 | * @package Webman\Http 22 | */ 23 | class Request extends \Symfony\Component\HttpFoundation\Request 24 | { 25 | public static function createFromWorkmanRequest(WorkmanRequest $workmanRequest): \Illuminate\Http\Request 26 | { 27 | return new \Illuminate\Http\Request($workmanRequest->get(),$workmanRequest->post(),[],$workmanRequest->cookie(),self::resolveFiles($workmanRequest),self::resolveServerParams($workmanRequest),$workmanRequest->rawBody()); 28 | } 29 | protected static function resolveServerParams(WorkmanRequest $workmanRequest): array 30 | { 31 | $server = []; 32 | foreach ($workmanRequest->header() as $key=>$v){ 33 | $server['HTTP_'.strtoupper($key)]=$v; 34 | } 35 | $server['REQUEST_METHOD']=$workmanRequest->method(); 36 | $server['DOCUMENT_ROOT']=public_path(); 37 | $server['REMOTE_ADDR']=$workmanRequest->connection->getRemoteIp(); 38 | $server['REMOTE_PORT']=$workmanRequest->connection->getRemotePort(); 39 | $server['SERVER_SOFTWARE']='laraman'; 40 | $server['SERVER_PROTOCOL']='HTTP/'.$workmanRequest->protocolVersion(); 41 | $server['SERVER_NAME']=$workmanRequest->host(); 42 | $server['SERVER_PORT']=substr($server['HTTP_HOST'],strpos($server['HTTP_HOST'],':')); 43 | $server['REQUEST_URI']=$workmanRequest->path().'?'.$workmanRequest->queryString(); 44 | $server['SCRIPT_NAME']='/laraman'; 45 | $server['SCRIPT_FILENAME']='/laraman'; 46 | $server['PHP_SELF']='laraman'; 47 | $server['REQUEST_TIME_FLOAT']=microtime(true); 48 | $server['REQUEST_TIME']=time(); 49 | $server['QUERY_STRING']=$workmanRequest->queryString(); 50 | return $server; 51 | } 52 | 53 | protected static function resolveFiles(WorkmanRequest $workmanRequest): array 54 | { 55 | $parameters=[]; 56 | foreach ($workmanRequest->file() as $name => $file){ 57 | $parameters[$name]=new UploadedFile($file['tmp_name'],$file['name'],$file['type'],$file['error']); 58 | } 59 | return $parameters; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Http/Response.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright walkor 11 | * @link http://www.workerman.net/ 12 | * @license http://www.opensource.org/licenses/mit-license.php MIT License 13 | */ 14 | 15 | namespace Itinysun\Laraman\Http; 16 | 17 | use Symfony\Component\HttpFoundation\BinaryFileResponse; 18 | use Throwable; 19 | 20 | use function filemtime; 21 | use function gmdate; 22 | 23 | /** 24 | * Class Response 25 | * @package Webman\Http 26 | */ 27 | class Response extends \Workerman\Protocols\Http\Response 28 | { 29 | /** 30 | * @var Throwable 31 | */ 32 | protected $exception = null; 33 | 34 | /** 35 | * File 36 | * @param string $file 37 | * @return $this 38 | */ 39 | public function file(string $file): Response 40 | { 41 | if ($this->notModifiedSince($file)) { 42 | return $this->withStatus(304); 43 | } 44 | return $this->withFile($file); 45 | } 46 | 47 | /** 48 | * Download 49 | * @param string $file 50 | * @param string $downloadName 51 | * @return $this 52 | */ 53 | public function download(string $file, string $downloadName = ''): Response 54 | { 55 | $this->withFile($file); 56 | if ($downloadName) { 57 | $this->header('Content-Disposition', "attachment; filename=\"$downloadName\""); 58 | } 59 | return $this; 60 | } 61 | 62 | /** 63 | * NotModifiedSince 64 | * @param string $file 65 | * @return bool 66 | */ 67 | protected function notModifiedSince(string $file): bool 68 | { 69 | $ifModifiedSince = request()->header('if-modified-since'); 70 | if ($ifModifiedSince === null || !($mtime = filemtime($file))) { 71 | return false; 72 | } 73 | return $ifModifiedSince === gmdate('D, d M Y H:i:s', $mtime) . ' GMT'; 74 | } 75 | 76 | /** 77 | * Exception 78 | * @param Throwable|null $exception 79 | * @return Throwable|null 80 | */ 81 | public function exception(Throwable $exception = null): ?Throwable 82 | { 83 | if ($exception) { 84 | $this->exception = $exception; 85 | } 86 | return $this->exception; 87 | } 88 | 89 | public static function fromLaravelResponse(mixed $response): static 90 | { 91 | $status = $response->getStatusCode(); 92 | $headers = $response->headers->all(); 93 | $reason = static::$_phrases[$status] ?? 'unknown error'; 94 | $resp = new static($status,marshalHeaders($headers),$response->getContent()); 95 | $resp->_reason=$reason; 96 | if($response instanceof BinaryFileResponse && null!==$response->getFile()){ 97 | $resp->withFile($response->getFile()); 98 | } 99 | return $resp; 100 | } 101 | 102 | 103 | 104 | } 105 | -------------------------------------------------------------------------------- /src/LaramanServiceProvider.php: -------------------------------------------------------------------------------- 1 | app->runningInConsole()) { 21 | $this->bootForConsole(); 22 | 23 | /* 24 | * 框架兼容处理 25 | */ 26 | if(class_exists(\Slowlyo\OwlAdmin\Admin::class)){ 27 | Event::listen(\Slowlyo\OwlAdmin\Events\ExtensionChanged::class, OwlAdminExtensionChanged::class); 28 | } 29 | if(class_exists(\Dcat\Admin\Admin::class)){ 30 | Event::listen(RequestReceived::class,\Dcat\Admin\Octane\Listeners\FlushAdminState::class); 31 | } 32 | } 33 | } 34 | 35 | /** 36 | * Register any package services. 37 | * 38 | * @return void 39 | */ 40 | public function register(): void 41 | { 42 | $this->commands([ConfigProxy::class]); 43 | } 44 | 45 | /** 46 | * Console-specific booting. 47 | * 48 | * @return void 49 | */ 50 | protected function bootForConsole(): void 51 | { 52 | // Publishing the configuration file. 53 | $this->publishes([ 54 | __DIR__.'/../config/web.php' => config_path('laraman/web.php'), 55 | __DIR__.'/../config/monitor.php' => config_path('laraman/monitor.php'), 56 | __DIR__.'/../config/server.php' => config_path('laraman/server.php'), 57 | __DIR__.'/../config/schedule.php' => config_path('laraman/schedule.php'), 58 | __DIR__ . '/../config/starter.php' =>base_path('laraman') 59 | ],'laraman.install'); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Listeners/CleanBaseState.php: -------------------------------------------------------------------------------- 1 | app->cleanBaseState(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/Listeners/CleanWebState.php: -------------------------------------------------------------------------------- 1 | app->cleanWebState(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/Listeners/OwlAdminExtensionChanged.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright walkor 11 | * @link http://www.workerman.net/ 12 | * @license http://www.opensource.org/licenses/mit-license.php MIT License 13 | */ 14 | 15 | namespace Itinysun\Laraman\Process; 16 | 17 | use FilesystemIterator; 18 | use Itinysun\Laraman\Command\Configs; 19 | use RecursiveDirectoryIterator; 20 | use RecursiveIteratorIterator; 21 | use SplFileInfo; 22 | use Workerman\Timer; 23 | use Itinysun\Laraman\Server\LaramanWorker as Worker; 24 | 25 | /** 26 | * Class FileMonitor 27 | * @package process 28 | */ 29 | class Monitor extends ProcessBase 30 | { 31 | /** 32 | * @var array 33 | */ 34 | protected array $paths = []; 35 | 36 | /** 37 | * @var array 38 | */ 39 | protected array $extensions = []; 40 | 41 | /** 42 | * @var string 43 | */ 44 | public static string $lockFile = ''; 45 | 46 | /** 47 | * Pause monitor 48 | * @return void 49 | */ 50 | public static function pause(): void 51 | { 52 | file_put_contents(static::$lockFile, time()); 53 | } 54 | 55 | /** 56 | * Resume monitor 57 | * @return void 58 | */ 59 | public static function resume(): void 60 | { 61 | clearstatcache(); 62 | if (is_file(static::$lockFile)) { 63 | unlink(static::$lockFile); 64 | } 65 | } 66 | 67 | /** 68 | * Whether monitor is paused 69 | * @return bool 70 | */ 71 | public static function isPaused(): bool 72 | { 73 | clearstatcache(); 74 | return file_exists(static::$lockFile); 75 | } 76 | 77 | /** 78 | * FileMonitor constructor. 79 | * @param array $options 80 | */ 81 | public function __construct(array $options = []) 82 | { 83 | parent::__construct($options); 84 | 85 | static::$lockFile=Configs::getBasePath('monitor.lock'); 86 | 87 | static::resume(); 88 | $this->paths = (array)$options['monitorDir']; 89 | $this->extensions = $options['monitorExtensions']; 90 | if (!Worker::getAllWorkers()) { 91 | return; 92 | } 93 | $disableFunctions = explode(',', ini_get('disable_functions')); 94 | if (in_array('exec', $disableFunctions, true)) { 95 | echo "\nMonitor file change turned off because exec() has been disabled by disable_functions setting in " . PHP_CONFIG_FILE_PATH . "/php.ini\n"; 96 | } else { 97 | if (!isWindows() && ($options['enable_file_monitor'] ?? true)) { 98 | Timer::add(1, function () { 99 | $this->checkAllFilesChange(); 100 | }); 101 | } 102 | } 103 | 104 | $memoryLimit = $this->getMemoryLimit($options['memory_limit'] ?? null); 105 | if (!isWindows() && $memoryLimit && ($options['enable_memory_monitor'] ?? true)) { 106 | Timer::add(60, [$this, 'checkMemory'], [$memoryLimit]); 107 | } 108 | } 109 | 110 | /** 111 | * @param $monitorDir 112 | * @return bool 113 | */ 114 | public function checkFilesChange($monitorDir): bool 115 | { 116 | static $lastMtime, $tooManyFilesCheck; 117 | if (!$lastMtime) { 118 | $lastMtime = time(); 119 | } 120 | clearstatcache(); 121 | if (!is_dir($monitorDir)) { 122 | if (!is_file($monitorDir)) { 123 | return false; 124 | } 125 | $iterator = [new SplFileInfo($monitorDir)]; 126 | } else { 127 | // recursive traversal directory 128 | $dirIterator = new RecursiveDirectoryIterator($monitorDir, FilesystemIterator::SKIP_DOTS | FilesystemIterator::FOLLOW_SYMLINKS); 129 | $iterator = new RecursiveIteratorIterator($dirIterator); 130 | } 131 | $count = 0; 132 | foreach ($iterator as $file) { 133 | $count ++; 134 | /** var SplFileInfo $file */ 135 | if (is_dir($file->getRealPath())) { 136 | continue; 137 | } 138 | // check mtime 139 | if ($lastMtime < $file->getMTime() && in_array($file->getExtension(), $this->extensions, true)) { 140 | $var = 0; 141 | /* 142 | * 检查是否有语法错误 143 | * 有错误var为-1,无错误为0 144 | * @see https://www.php.net/manual/zh/features.commandline.options.php 145 | * */ 146 | exec('"'.PHP_BINARY . '" -l ' . $file, $out, $var); 147 | $lastMtime = $file->getMTime(); 148 | if ($var) { 149 | /*如果有语法错误,跳过本次重启检查*/ 150 | continue; 151 | } 152 | echo $file . " update and reload\n"; 153 | // send SIGUSR1 signal to master process for reload 154 | if (!isWindows()) { 155 | posix_kill(posix_getppid(), SIGUSR1); 156 | } else { 157 | return true; 158 | } 159 | break; 160 | } 161 | } 162 | if (!$tooManyFilesCheck && $count > 1000) { 163 | echo "Monitor: There are too many files ($count files) in $monitorDir which makes file monitoring very slow\n"; 164 | $tooManyFilesCheck = 1; 165 | } 166 | return false; 167 | } 168 | 169 | /** 170 | * @return bool 171 | */ 172 | public function checkAllFilesChange(): bool 173 | { 174 | if (static::isPaused()) { 175 | return false; 176 | } 177 | 178 | foreach ($this->paths as $path) { 179 | if ($this->checkFilesChange($path)) { 180 | return true; 181 | } 182 | } 183 | 184 | return false; 185 | } 186 | 187 | /** 188 | * @param $memoryLimit 189 | * @return void 190 | */ 191 | public function checkMemory($memoryLimit): void 192 | { 193 | if (static::isPaused() || $memoryLimit <= 0) { 194 | return; 195 | } 196 | $ppid = posix_getppid(); 197 | $childrenFile = "/proc/$ppid/task/$ppid/children"; 198 | if (!is_file($childrenFile) || !($children = file_get_contents($childrenFile))) { 199 | return; 200 | } 201 | foreach (explode(' ', $children) as $pid) { 202 | $pid = (int)$pid; 203 | $statusFile = "/proc/$pid/status"; 204 | if (!is_file($statusFile) || !($status = file_get_contents($statusFile))) { 205 | continue; 206 | } 207 | $mem = 0; 208 | if (preg_match('/VmRSS\s*?:\s*?(\d+?)\s*?kB/', $status, $match)) { 209 | $mem = $match[1]; 210 | } 211 | $mem = (int)($mem / 1024); 212 | if ($mem >= $memoryLimit) { 213 | posix_kill($pid, SIGINT); 214 | } 215 | } 216 | } 217 | 218 | /** 219 | * Get memory limit 220 | * @param $memoryLimit 221 | * @return float 222 | */ 223 | protected function getMemoryLimit($memoryLimit): float 224 | { 225 | if ($memoryLimit === 0) { 226 | return 0; 227 | } 228 | $usePhpIni = false; 229 | if (!$memoryLimit) { 230 | $memoryLimit = ini_get('memory_limit'); 231 | $usePhpIni = true; 232 | } 233 | 234 | if ($memoryLimit == -1) { 235 | return 0; 236 | } 237 | $unit = strtolower($memoryLimit[strlen($memoryLimit) - 1]); 238 | if ($unit === 'g') { 239 | $memoryLimit = 1024 * (int)$memoryLimit; 240 | } else if ($unit === 'm') { 241 | $memoryLimit = (int)$memoryLimit; 242 | } else if ($unit === 'k') { 243 | $memoryLimit = ((int)$memoryLimit / 1024); 244 | } else { 245 | $memoryLimit = ((int)$memoryLimit / (1024 * 1024)); 246 | } 247 | if ($memoryLimit < 30) { 248 | $memoryLimit = 30; 249 | } 250 | if ($usePhpIni) { 251 | $memoryLimit = (int)(0.8 * $memoryLimit); 252 | } 253 | return $memoryLimit; 254 | } 255 | } 256 | -------------------------------------------------------------------------------- /src/Process/ProcessBase.php: -------------------------------------------------------------------------------- 1 | options = $params; 38 | } 39 | public function _onMessage(TcpConnection $connection, $data): void{ 40 | MessageReceived::dispatch($connection,$data); 41 | $this->onMessage($connection,$data); 42 | MessageDone::dispatch($connection,$data); 43 | $this->checkRestart(); 44 | } 45 | public function _onWorkerStart(Worker $worker): void{ 46 | $this->onWorkerStart($worker); 47 | } 48 | 49 | public function _onWorkerReload(Worker $worker): void 50 | { 51 | $this->worker = $worker; 52 | $this->onWorkerReload($worker); 53 | } 54 | 55 | public function _onConnect(TcpConnection $connection): void 56 | { 57 | $this->onConnect($connection); 58 | } 59 | 60 | 61 | public function _onClose(TcpConnection $connection): void 62 | { 63 | $this->onClose($connection); 64 | } 65 | 66 | public function _onBufferFull(TcpConnection $connection): void 67 | { 68 | $this->onBufferFull($connection); 69 | } 70 | 71 | public function _onBufferDrain(TcpConnection $connection): void 72 | { 73 | $this->onBufferDrain($connection); 74 | } 75 | 76 | 77 | /** 78 | * @return void 79 | */ 80 | protected function fixUploadedFile(): void 81 | { 82 | $fixesDir = dirname(__DIR__) . '../../fixes'; 83 | if (!function_exists('\\Symfony\\Component\\HttpFoundation\\File\\is_uploaded_file')) { 84 | require $fixesDir . '/fix-symfony-file-validation.php'; 85 | } 86 | if (!function_exists('\\Symfony\\Component\\HttpFoundation\\File\\move_uploaded_file')) { 87 | require $fixesDir . '/fix-symfony-file-moving.php'; 88 | } 89 | } 90 | 91 | /** 92 | * 检查框架是否需要重启则,请避免在高并发请求使用 93 | * @return void 94 | */ 95 | protected function checkRestart(): void 96 | { 97 | if(LaramanWorker::$needRestart){ 98 | LaramanWorker::$needRestart=false; 99 | LaramanWorker::stopAll(); 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/Process/Schedule.php: -------------------------------------------------------------------------------- 1 | id); 22 | } 23 | } -------------------------------------------------------------------------------- /src/Process/Web.php: -------------------------------------------------------------------------------- 1 | send($connection, $result, $request); 39 | return; 40 | } 41 | } 42 | 43 | RequestReceived::dispatch($this->app, $this->app, $request); 44 | 45 | //如果启用了telescope,需要每次重置状态 46 | $this->refreshTelescope($request); 47 | 48 | //获取响应 49 | $response = $this->getResponse($request); 50 | 51 | //发送响应 52 | $this->send($connection, $response, $request); 53 | 54 | 55 | } catch (Throwable $e) { 56 | 57 | //记录异常 58 | report($e); 59 | 60 | //使用原生laravel的方式渲染异常并发送异常,请查看laravel手册 61 | $response = $this->exceptionHandler->render($request, $e); 62 | $this->send($connection, Response::fromLaravelResponse($response)->withStatus(500), $request); 63 | } 64 | } 65 | 66 | /** 67 | * OnWorkerStart. 68 | * @param Worker $worker 69 | * @return void 70 | */ 71 | protected function onWorkerStart(Worker $worker): void 72 | { 73 | //读取配置,初始化静态文件服务 74 | if (isset($this->options['static_file'])) 75 | StaticFileServer::init($this->options['static_file']); 76 | 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/Server/LaramanApp.php: -------------------------------------------------------------------------------- 1 | requestStartedAt = Carbon::now(); 19 | $this->app->instance('request', new Request()); 20 | $this->bootstrap(); 21 | $this->app->make('config'); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Server/LaramanWorker.php: -------------------------------------------------------------------------------- 1 | contains(function ($v) use ($path) { 50 | return stripos($path,$v) == 0; 51 | }); 52 | }; 53 | } 54 | } 55 | 56 | $cors_config = config('cors'); 57 | static::$cors = App::make(CorsService::class, $cors_config); 58 | static::$cors_paths = config('cors.paths', []); 59 | } 60 | 61 | public static function tryServeFile(Request $request): ?Response 62 | { 63 | $file = self::resolvePath($request->path()); 64 | if($file === null) 65 | return null; 66 | if (static::matchCorsPath($request)) { 67 | if (static::$cors->isPreflightRequest($request)) { 68 | $response = static::$cors->handlePreflightRequest($request); 69 | static::$cors->varyHeader($response, 'Access-Control-Request-Method'); 70 | return new Response(200, $response->headers->all()); 71 | } 72 | $response = new \Symfony\Component\HttpFoundation\Response(); 73 | if ($request->getMethod() === 'OPTIONS') { 74 | static::$cors->varyHeader($response, 'Access-Control-Request-Method'); 75 | } 76 | $response = static::$cors->addActualRequestHeaders($response, $request); 77 | return self::getResponse($file,$response->headers->all()); 78 | }else{ 79 | return self::getResponse($file); 80 | } 81 | } 82 | 83 | public static function resolvePath(string $path): bool|null|string 84 | { 85 | if (preg_match('/%[0-9a-f]{2}/i', $path)) { 86 | $path = urldecode($path); 87 | } 88 | $path = public_path($path); 89 | $file = realpath($path); 90 | clearstatcache($file); 91 | 92 | //尝试寻找默认页面 93 | if(is_dir($path)){ 94 | $foundDefault = false; 95 | foreach (static::$config['defaultPage'] as $page){ 96 | $defaultPageTry = $path.DIRECTORY_SEPARATOR.$page; 97 | if(file_exists($defaultPageTry)){ 98 | Log::debug('use defaultPage as static file response',compact('page','path')); 99 | $file=$defaultPageTry; 100 | $foundDefault = true; 101 | } 102 | } 103 | if(!$foundDefault){ 104 | return null; 105 | } 106 | } 107 | 108 | //尝试寻找文件 109 | if (file_exists($file)) { 110 | $checkSafePath = static::$isSafePath; 111 | if ($checkSafePath($file)) { 112 | return $file; 113 | } else { 114 | return false; 115 | } 116 | } 117 | 118 | return null; 119 | } 120 | 121 | public static function getResponse(string|bool $file, array $headers=[]): Response 122 | { 123 | if($file===false){ 124 | abort(404,'access deny',$headers); 125 | } 126 | if (pathinfo($file, PATHINFO_EXTENSION) === 'php') { 127 | if(self::$config['support_php']) 128 | return (new Response(200, $headers,self::execPhpFile($file))); 129 | else 130 | abort(403,'not supported',$headers); 131 | } 132 | return (new Response(200,$headers))->withFile($file); 133 | } 134 | 135 | 136 | /** 137 | * Get the path from the configuration to determine if the CORS service should run. 138 | * 139 | * @param Request $request 140 | * @return bool 141 | */ 142 | protected static function matchCorsPath(Request $request): bool 143 | { 144 | $paths = static::getPathsByHost($request->getHost()); 145 | 146 | foreach ($paths as $path) { 147 | if ($path !== '/') { 148 | $path = trim($path, '/'); 149 | } 150 | 151 | if ($request->fullUrlIs($path) || $request->is($path)) { 152 | return true; 153 | } 154 | } 155 | 156 | return false; 157 | } 158 | 159 | /** 160 | * Get the CORS paths for the given host. 161 | * 162 | * @param string $host 163 | * @return array 164 | */ 165 | protected static function getPathsByHost(string $host): array 166 | { 167 | 168 | if (isset(static::$cors_paths[$host])) { 169 | return static::$cors_paths[$host]; 170 | } 171 | 172 | return array_filter(static::$cors_paths, function ($path) { 173 | return is_string($path); 174 | }); 175 | } 176 | 177 | /** 178 | * ExecPhpFile. 179 | * @param string $file 180 | * @return false|string 181 | */ 182 | public static function execPhpFile(string $file): bool|string 183 | { 184 | ob_start(); 185 | // Try to include php file. 186 | try { 187 | include $file; 188 | } catch (Exception $e) { 189 | echo $e; 190 | } 191 | return ob_get_clean(); 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /src/Traits/HasBaseState.php: -------------------------------------------------------------------------------- 1 | forgetScopedInstances(); 18 | $this->flushMailer(); 19 | $this->flushNotificationChannelManager(); 20 | $this->flushDatabase(); 21 | $this->flushLogContext(); 22 | $this->flushArrayCache(); 23 | $this->flushStrCache(); 24 | $this->flushTranslatorCache(); 25 | $this->prepareScoutForNextOperation(); 26 | $this->prepareInertiaForNextOperation(); 27 | $this->prepareLivewireForNextOperation(); 28 | $this->PrepareSocialiteForNextOperation(); 29 | } 30 | 31 | /** 32 | * laravel 自带ScopedInstances模式,但是它竟然不清理Facade缓存,坑爹,咱自己实现 33 | * 而且我不明白Facade为啥要独立维护一份实例缓存呢? 34 | * laravel don't clear facade instance cache ,so we clear ourselves 35 | * @return void 36 | */ 37 | public function forgetScopedInstances(): void 38 | { 39 | foreach ($this->scopedInstances as $scoped) { 40 | unset($this->instances[$scoped]); 41 | Facade::clearResolvedInstance($scoped); 42 | } 43 | 44 | } 45 | /** 46 | * @return void 47 | */ 48 | protected function flushNotificationChannelManager(): void 49 | { 50 | if (!$this->resolved(ChannelManager::class)) { 51 | return; 52 | } 53 | 54 | with($this->make(ChannelManager::class), function ($manager) { 55 | $manager->forgetDrivers(); 56 | }); 57 | } 58 | 59 | /** 60 | * @return void 61 | */ 62 | protected function flushMailer(): void 63 | { 64 | if (!$this->resolved('mail.manager')) { 65 | return; 66 | } 67 | 68 | with($this->make('mail.manager'), function ($manager) { 69 | $manager->forgetMailers(); 70 | }); 71 | } 72 | 73 | /** 74 | * @return void 75 | */ 76 | protected function flushArrayCache(): void 77 | { 78 | if (config('cache.stores.array')) { 79 | $this->make('cache')->store('array')->flush(); 80 | } 81 | } 82 | 83 | /** 84 | * @return void 85 | */ 86 | protected function flushStrCache(): void 87 | { 88 | Str::flushCache(); 89 | } 90 | /** 91 | * @return void 92 | */ 93 | protected function prepareInertiaForNextOperation(): void 94 | { 95 | if (!$this->resolved('\Inertia\ResponseFactory')) { 96 | return; 97 | } 98 | 99 | $factory = $this->make('\Inertia\ResponseFactory::class'); 100 | 101 | if (method_exists($factory, 'flushShared')) { 102 | $factory->flushShared(); 103 | } 104 | } 105 | 106 | /** 107 | * @return void 108 | */ 109 | protected function prepareLivewireForNextOperation(): void 110 | { 111 | if (!$this->resolved('\Livewire\LivewireManager')) { 112 | return; 113 | } 114 | 115 | $manager = $this->make('\Livewire\LivewireManager'); 116 | 117 | if (method_exists($manager, 'flushState')) { 118 | $manager->flushState(); 119 | } 120 | } 121 | 122 | /** 123 | * @return void 124 | */ 125 | protected function prepareScoutForNextOperation(): void 126 | { 127 | if (!$this->resolved('\Laravel\Scout\EngineManager')) { 128 | return; 129 | } 130 | 131 | $factory = $this->make('\Laravel\Scout\EngineManager'); 132 | 133 | if (!method_exists($factory, 'forgetEngines')) { 134 | return; 135 | } 136 | 137 | $factory->forgetEngines(); 138 | } 139 | 140 | /** 141 | * @return void 142 | */ 143 | protected function PrepareSocialiteForNextOperation(): void 144 | { 145 | if (!$this->resolved('\Laravel\Socialite\Contracts\Factory')) { 146 | return; 147 | } 148 | 149 | $factory = $this->make('\Laravel\Socialite\Contracts\Factory'); 150 | 151 | if (!method_exists($factory, 'forgetDrivers')) { 152 | return; 153 | } 154 | 155 | $factory->forgetDrivers(); 156 | } 157 | /** 158 | * @return void 159 | */ 160 | protected function flushDatabase(): void 161 | { 162 | if (!$this->resolved('db')) { 163 | return; 164 | } 165 | 166 | foreach ($this->make('db')->getConnections() as $connection) { 167 | if ( 168 | method_exists($connection, 'resetTotalQueryDuration') 169 | && method_exists($connection, 'allowQueryDurationHandlersToRunAgain') 170 | ) { 171 | $connection->resetTotalQueryDuration(); 172 | $connection->allowQueryDurationHandlersToRunAgain(); 173 | } 174 | $connection->flushQueryLog(); 175 | $connection->forgetRecordModificationState(); 176 | } 177 | } 178 | 179 | /** 180 | * @return void 181 | */ 182 | protected function flushLogContext(): void 183 | { 184 | if (!$this->resolved('log')) { 185 | return; 186 | } 187 | collect($this->make('log')->getChannels()) 188 | ->map->getLogger() 189 | ->filter(function ($logger) { 190 | return $logger instanceof \Monolog\ResettableInterface; 191 | })->each->reset(); 192 | 193 | if (method_exists($this['log'], 'flushSharedContext')) { 194 | $this['log']->flushSharedContext(); 195 | } 196 | 197 | if (method_exists($this['log']->driver(), 'withoutContext')) { 198 | $this['log']->withoutContext(); 199 | } 200 | } 201 | 202 | /** 203 | * @return void 204 | */ 205 | protected function flushTranslatorCache(): void 206 | { 207 | if (!$this->resolved('translator')) { 208 | return; 209 | } 210 | 211 | $config = $this->make('config'); 212 | 213 | $translator = $this->make('translator'); 214 | 215 | if ($translator instanceof \Illuminate\Support\NamespacedItemResolver) { 216 | $translator->flushParsedKeys(); 217 | } 218 | 219 | tap($translator, function ($translator) use ($config) { 220 | $translator->setLocale($config->get('app.locale')); 221 | $translator->setFallback($config->get('app.fallback_locale')); 222 | }); 223 | 224 | /* 225 | * not very sure about what these mean 226 | * see Laravel\Octane\Listeners\FlushLocaleState; 227 | $provider = tap(new CarbonServiceProvider($event->app))->updateLocale(); 228 | 229 | collect($event->sandbox->getProviders($provider)) 230 | ->values() 231 | ->whenNotEmpty(fn ($providers) => $providers->first()->setAppGetter(fn () => $event->sandbox)); 232 | */ 233 | 234 | } 235 | } 236 | -------------------------------------------------------------------------------- /src/Traits/HasCleanMode.php: -------------------------------------------------------------------------------- 1 | cleanMode && !in_array($basename,$this->originBaseNames)){ 24 | $this->scopedInstances[] = $abstract; 25 | $shared=true; 26 | } 27 | parent::bind($abstract,$concrete,$shared); 28 | } 29 | 30 | 31 | /** 32 | * @param mixed $switch 33 | * @return void 34 | */ 35 | public function setCleanMode(mixed $switch=false): void 36 | { 37 | $this->cleanMode=boolval($switch); 38 | } 39 | 40 | /** 41 | * 框架原生的服务别名 42 | * @var array|string[] 43 | */ 44 | protected array $originBaseNames = [ 45 | 'PackageManifest', 46 | 'events', 47 | 'log', 48 | 'router', 49 | 'url', 50 | 'redirect', 51 | 'ServerRequestInterfaceMix', 52 | 'ResponseInterfacePackageManifest', 53 | 'ResponseFactory', 54 | 'CallableDispatcher', 55 | 'ControllerDispatcherevents', 56 | 'Kernel', 57 | 'logKernel', 58 | 'routerExceptionHandler', 59 | 'url', 60 | 'redirect', 61 | 'ServerRequestInterface', 62 | 'ResponseInterface', 63 | 'ResponseFactory', 64 | 'CallableDispatcher', 65 | 'ControllerDispatcher', 66 | 'Kernel', 67 | 'Kernel', 68 | 'ExceptionHandler', 69 | 'env', 70 | 'env', 71 | 'auth', 72 | 'auth.driver', 73 | 'Authenticatable', 74 | 'Gate', 75 | 'RequirePassword', 76 | 'cookie', 77 | 'auth', 78 | 'auth.driver', 79 | 'Authenticatable', 80 | 'Gate', 81 | 'RequirePassword', 82 | 'cookie', 83 | 'db.factory', 84 | 'db', 85 | 'db.connection', 86 | 'db.schema', 87 | 'db.transactions', 88 | 'Generator', 89 | 'EntityResolver', 90 | 'encrypter', 91 | 'db.factory', 92 | 'db', 93 | 'db.connection', 94 | 'filesdb.schema', 95 | 'filesystemdb.transactions', 96 | 'filesystem.diskGenerator', 97 | 'filesystem.cloudEntityResolver', 98 | 'encrypter', 99 | 'ParallelTesting', 100 | 'files', 101 | 'filesystem', 102 | 'filesystem.disk', 103 | 'filesystem.cloud', 104 | 'ParallelTesting', 105 | 'MaintenanceModeManager', 106 | 'MaintenanceMode', 107 | 'Vite', 108 | 'ChannelManager', 109 | 'MaintenanceModeManager', 110 | 'MaintenanceMode', 111 | 'Vite', 112 | 'ChannelManager', 113 | 'session', 114 | 'session.store', 115 | 'StartSession', 116 | 'view', 117 | 'view.finder', 118 | 'blade.compiler', 119 | 'view.engine.resolver', 120 | 'session', 121 | 'session.store', 122 | 'StartSession', 123 | 'view', 124 | 'view.finder', 125 | 'blade.compiler', 126 | 'view.engine.resolver', 127 | 'EntriesRepository', 128 | 'ClearableRepository', 129 | 'PrunableRepository', 130 | 'Provider', 131 | 'EntriesRepository', 132 | 'ExceptionHandlerClearableRepository', 133 | 'PrunableRepository', 134 | 'Provider', 135 | 'Flare', 136 | 'SentReports', 137 | 'ConfigManager', 138 | 'ExceptionHandler', 139 | 'IgnitionConfig', 140 | 'SolutionProviderRepository', 141 | 'Flare', 142 | 'Ignition', 143 | 'SentReports', 144 | 'ExceptionRenderer', 145 | 'ConfigManager', 146 | 'DumpRecorder', 147 | 'LogRecorder', 148 | 'QueryRecorder', 149 | 'JobRecorder', 150 | 'flare.logger', 151 | 'IgnitionConfig', 152 | 'SolutionProviderRepository', 153 | 'Ignition', 154 | 'ExceptionRenderer', 155 | 'DumpRecorder', 156 | 'LogRecorder', 157 | 'QueryRecorder', 158 | 'JobRecorder', 159 | 'flare.logger', 160 | 'cache', 161 | 'cache.store', 162 | 'cache.psr6', 163 | 'memcached.connector', 164 | 'RateLimiter', 165 | 'cache', 166 | 'cache.store', 167 | 'cache.psr6', 168 | 'memcached.connector', 169 | 'RateLimiter', 170 | 'redis', 171 | 'redis.connection', 172 | 'redis', 173 | 'redis.connection', 174 | 'MultiDumpHandler', 175 | 'MultiDumpHandler', 176 | 'queue', 177 | 'queue.connection', 178 | 'queuequeue.worker', 179 | 'queue.connectionqueue.listener', 180 | 'queue.workerqueue.failer', 181 | 'queue.listener', 182 | 'queue.failer', 183 | 184 | ]; 185 | } 186 | -------------------------------------------------------------------------------- /src/Traits/HasLaravelApplication.php: -------------------------------------------------------------------------------- 1 | worker->protocol != null) { 60 | if ($this->worker->protocol == 'Workerman\Protocols\Http') { 61 | //转换请求 62 | //$request = Request::createFromBase(\Itinysun\Laraman\Http\Request::createFromWorkmanRequest($data)); 63 | /* @var \Workerman\Protocols\Http\Request $data */ 64 | $request = \Itinysun\Laraman\Http\Request::createFromWorkmanRequest($data); 65 | 66 | $this->onHttpMessage($connection, $request); 67 | 68 | //clean files after message 69 | //销毁实例会自动删除文件 70 | unset($data); 71 | 72 | //销毁文件之后再检查重启 73 | $this->checkRestart(); 74 | return; 75 | } 76 | if (in_array($this->worker->protocol, ['Workerman\Protocols\Frame', 'Workerman\Protocols\Text', 'Workerman\Protocols\Websocket', 'Workerman\Protocols\Ws'])) 77 | $this->onTextMessage($connection, $data); 78 | else { 79 | $this->onMessage($connection, $data); 80 | } 81 | } else { 82 | $this->onMessage($connection, $data); 83 | } 84 | MessageDone::dispatch($connection, $data); 85 | } 86 | 87 | public function _onWorkerStart(Worker $worker): void 88 | { 89 | $this->worker = $worker; 90 | 91 | $this->app = new LaramanApp(Configs::getBasePath()); 92 | 93 | $this->app->setCleanMode($this->options['clearMode'] ?? false); 94 | 95 | $this->app->singleton( 96 | \Illuminate\Contracts\Http\Kernel::class, 97 | LaramanKernel::class 98 | ); 99 | $this->app->singleton( 100 | \Illuminate\Contracts\Console\Kernel::class, 101 | \App\Console\Kernel::class 102 | ); 103 | $this->app->singleton( 104 | ExceptionHandler::class, 105 | Handler::class 106 | ); 107 | 108 | $this->kernel = $this->app->make(\Illuminate\Contracts\Http\Kernel::class); 109 | 110 | $this->exceptionHandler = $this->app->make(ExceptionHandler::class); 111 | 112 | $this->fixUploadedFile(); 113 | 114 | if (isset($this->options['events']) && !empty($this->options['events'])) { 115 | foreach ($this->options['events'] as $event => $v) { 116 | foreach (Arr::wrap($v) as $listener) { 117 | Event::listen($event, $listener); 118 | } 119 | } 120 | } 121 | /* 122 | Heartbeat 123 | 数据库心跳,用来保持数据连接不断开。laravel有重连机制,虽然感觉好像没有必要,但是参考的前辈们都写了,我也加上了。 124 | 如果你觉得不需要,可以在配置文件中设置心跳间隔为0。欢迎提供反馈。 125 | */ 126 | if (isset($this->options['db_heartbeat_interval']) && $this->options['db_heartbeat_interval'] > 0) { 127 | Timer::add($this->options['db_heartbeat_interval'], function () { 128 | $connections = DB::getConnections(); 129 | if (!$connections) { 130 | return; 131 | } 132 | try { 133 | foreach ($connections as $item) { 134 | if ($item->getDriverName() == 'mysql') { 135 | $item->select('select 1', [], true); 136 | } 137 | } 138 | } catch (Throwable $e) { 139 | echo 'database heartbeat failed,maybe database has down' . "\r\n" . $e->getMessage() . "\n\r"; 140 | } 141 | 142 | }); 143 | } 144 | 145 | $this->onWorkerStart($worker); 146 | } 147 | 148 | /** 149 | * 获取运行结果,并转换为workerman格式 150 | * @param Request $request 151 | * @return Response 152 | */ 153 | public function getResponse(Request $request): Response 154 | { 155 | $response = $this->kernel->handle( 156 | $request 157 | ); 158 | $this->kernel->terminate($request, $response); 159 | return Response::fromLaravelResponse($response); 160 | } 161 | 162 | 163 | /** 164 | * 发送响应,提取自 webman 165 | * @param TcpConnection|mixed $connection 166 | * @param Response $response 167 | * @param Request $request 168 | * @return void 169 | */ 170 | protected function send(mixed $connection, Response $response, Request $request): void 171 | { 172 | $keepAlive = $request->header('connection'); 173 | if (($keepAlive === null && $request->getProtocolVersion() === '1.1') 174 | || $keepAlive === 'keep-alive' || $keepAlive === 'Keep-Alive' 175 | ) { 176 | $connection->send($response); 177 | return; 178 | } 179 | $connection->close($response); 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /src/Traits/HasRefreshTelescope.php: -------------------------------------------------------------------------------- 1 | getHost(); 38 | } 39 | 40 | /** 41 | * 提取自telescope,判断请求是否需要记录 42 | * Determine if the request is to an approved URI. 43 | * 44 | * @param Request $request 45 | * @return bool 46 | */ 47 | protected static function requestIsToApprovedUri(Request $request): bool 48 | { 49 | if (! empty($only = config('telescope.only_paths', []))) { 50 | 51 | return $request->is($only); 52 | } 53 | 54 | return ! $request->is( 55 | collect([ 56 | 'telescope-api*', 57 | 'vendor/telescope*', 58 | (config('horizon.path') ?? 'horizon').'*', 59 | 'vendor/horizon*', 60 | ]) 61 | ->merge(config('telescope.ignore_paths', [])) 62 | ->unless(is_null(config('telescope.path')), function ($paths) { 63 | return $paths->prepend(config('telescope.path').'*'); 64 | }) 65 | ->all() 66 | ); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/Traits/HasWebState.php: -------------------------------------------------------------------------------- 1 | flushQueuedCookie(); 14 | $this->flushSessionState(); 15 | $this->flushAuthenticationState(); 16 | } 17 | 18 | /** 19 | * @return void 20 | */ 21 | protected function flushAuthenticationState(): void 22 | { 23 | if ($this->resolved('auth.driver')) { 24 | $this->forgetInstance('auth.driver'); 25 | } 26 | 27 | if ($this->resolved('auth')) { 28 | with($this->make('auth'), function ($auth) { 29 | $auth->forgetGuards(); 30 | }); 31 | } 32 | } 33 | 34 | /** 35 | * @return void 36 | */ 37 | protected function flushSessionState(): void 38 | { 39 | if (!$this->resolved('session')) { 40 | return; 41 | } 42 | 43 | $driver = $this->make('session')->driver(); 44 | 45 | $driver->flush(); 46 | $driver->regenerate(); 47 | } 48 | 49 | /** 50 | * @return void 51 | */ 52 | protected function flushQueuedCookie(): void 53 | { 54 | if (!$this->resolved('cookie')) { 55 | return; 56 | } 57 | $this->make('cookie')->flushQueuedCookies(); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Traits/HasWorkermanBuilder.php: -------------------------------------------------------------------------------- 1 | 0 ? intval($config['count']) : cpu_count() * 4; 30 | } 31 | 32 | if (!$processName) 33 | $processName = $configName; 34 | 35 | 36 | $worker = new Worker($config['listen'] ?? null, $config['context'] ?? []); 37 | 38 | $propertyMap = [ 39 | 'count', 40 | 'user', 41 | 'group', 42 | 'reloadable', 43 | 'reusePort', 44 | 'transport', 45 | 'protocol', 46 | ]; 47 | 48 | $worker->name = $processName; 49 | 50 | foreach ($propertyMap as $property) { 51 | if (isset($config[$property])) { 52 | $worker->$property = $config[$property]; 53 | } 54 | } 55 | 56 | return $worker; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Traits/HasWorkermanEvents.php: -------------------------------------------------------------------------------- 1 |