commit ac95dbb1c8f6424872443b674806aeb7bfaff3cf Author: zimoyin <2556608754@qq.com> Date: Wed Apr 1 15:07:15 2026 +0800 init diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..516299c --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +/runtime +/.idea +/.vscode +/vendor +*.log +.env +/tests/tmp +/tests/.phpunit.result.cache diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..94ce154 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,18 @@ +FROM php:8.3.22-cli-alpine + +RUN mv "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini" + +RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories \ + && apk update --no-cache \ + && docker-php-source extract + +# install extensions +RUN docker-php-ext-install pdo pdo_mysql -j$(nproc) pcntl + +# enable opcache and pcntl +RUN docker-php-ext-enable opcache pcntl +RUN docker-php-source delete \ + rm -rf /var/cache/apk/* + +RUN mkdir -p /app +WORKDIR /app \ No newline at end of file diff --git a/PHPUnit.php b/PHPUnit.php new file mode 100644 index 0000000..9b7abf5 --- /dev/null +++ b/PHPUnit.php @@ -0,0 +1,49 @@ + +

webman

+ +基于workerman开发的超高性能PHP框架 + + +

学习

+ + + +
+ +

赞助商

+ +

特别赞助

+ + + + +

铂金赞助

+ + + + +
+ + +
+ +

请作者喝咖啡

+ + + +
+如果您觉得webman对您有所帮助,欢迎捐赠。 + + +
+ + +
+

LICENSE

+The webman is open-sourced software licensed under the MIT. +
+ + + + diff --git a/app/bootstrap/SqlDebug.php b/app/bootstrap/SqlDebug.php new file mode 100644 index 0000000..b8de867 --- /dev/null +++ b/app/bootstrap/SqlDebug.php @@ -0,0 +1,82 @@ +dbDebug) Db::connection()->listen(function (QueryExecuted $queryExecuted) use ($appPath) { + // 过滤掉 "select 1" 这类心跳检测SQL + if (isset($queryExecuted->sql) && $queryExecuted->sql !== "select 1") { + $bindings = $queryExecuted->bindings; + // 替换SQL中的?为实际绑定参数 + $sql = array_reduce( + $bindings, + function ($sql, $binding) { + // 处理参数类型:字符串加引号,数值/布尔直接使用,null显示为NULL + $value = match (true) { + is_string($binding) => "'{$binding}'", + is_null($binding) => 'NULL', + is_bool($binding) => $binding ? 1 : 0, + default => $binding + }; + return preg_replace('/\?/', $value, $sql, 1); + }, + $queryExecuted->sql + ); + + // 构造基础SQL日志信息 + $sqlLog = sprintf( + "%s", + $sql + ); + + // 定位产生SQL的业务文件/行号/方法 + $traces = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS); + foreach ($traces as $trace) { + if (isset($trace['file'], $trace['function']) && str_contains($trace['file'], $appPath)) { + // 格式化文件路径(去掉项目根目录,只保留相对路径) + $file = str_replace(base_path(), '', $trace['file']); + $file = ltrim($file, '/\\'); + $file = str_replace(".php", '', $file); + $file = str_replace("\\", '.', $file); + $file = str_replace("/", '.', $file); +// $file = basename($file); + + // 使用Logger::debug输出日志(会同时输出到控制台和日志文件) + Log::debug( + '' . $file . "{$trace['line']}{$trace['function']}[$queryExecuted->time ms] " . $sqlLog + ); + break; // 只打印第一个匹配的业务文件信息,避免重复输出 + } + } + } + }); + } +} \ No newline at end of file diff --git a/app/config/Config.php b/app/config/Config.php new file mode 100644 index 0000000..be07db9 --- /dev/null +++ b/app/config/Config.php @@ -0,0 +1,126 @@ + $this->logRotationTimeByDay; + } + + /** + * @var int 错误日志轮转时间默认 30 天 + */ + public int $errorLogRotationTimeByDay { + get => $this->errorLogRotationTimeByDay; + } + + public DatabaseConfig $database { + get => $this->database; + } + + public bool $dbDebug { + get => $this->dbDebug; + } + + /** + * 方式 类:方法 + * 方式 类 + * 方式 :方法 + * 允许使用正则 * 作为通配符 + * @var array 日志过滤器 + */ + public array $logFilter { + get => $this->logFilter; + } + + public int $logLevel { + get => $this->logLevel; + } + + /** + * 阻断模式 + */ + public bool $blockMode { + get => $this->blockMode; + } + + /** + * TCP Server 进程数量 + */ + public int $tcpServerProcessNum = 1 { + get => $this->tcpServerProcessNum; + } + + /** + * TCP_SERVER_PORT + */ + public int $tcpServerPort = 50000 { + get => $this->tcpServerPort; + } + + private function __construct() + { + $this->database = new DatabaseConfig(); + $this->dbDebug = self::getBoolEnv('DB_DEBUG'); + $this->logFilter = self::getStringArrayEnv('LOG_FILTER', []); + $this->logLevel = match (strtoupper(self::getStringEnv('LOG_LEVEL', 'DEBUG'))) { + 'INFO' => \Monolog\Logger::INFO, + 'WARNING' => \Monolog\Logger::WARNING, + 'ERROR' => \Monolog\Logger::ERROR, + 'ALERT' => \Monolog\Logger::ALERT, + 'EMERGENCY' => \Monolog\Logger::EMERGENCY, + 'CRITICAL' => \Monolog\Logger::CRITICAL, + 'NOTICE' => \Monolog\Logger::NOTICE, + default => \Monolog\Logger::DEBUG + }; + $this->tcpServerPort = self::getIntEnv('TCP_SERVER_PORT', 50000); + if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN' && $this->tcpServerProcessNum > 1) { + $this->tcpServerProcessNum = 1; + echo "Warning: TCP_SERVER_PROCESS_NUM set to 1 on Windows.\n"; + } + } + + private function __clone() + { + } + + public static function getBoolEnv($name, $default = false) + { + $value = getenv($name); + return empty($value) ? $default : filter_var($value, FILTER_VALIDATE_BOOLEAN); + } + + public static function getIntEnv($name, $default = 0) + { + $value = getenv($name); + return empty($value) ? $default : (int)$value; + } + + public static function getStringEnv($name, $default = null) + { + $value = getenv($name); + return empty($value) ? $default : $value; + } + + private static ?Config $instance = null; + + public static function getInstance(): Config + { + if (self::$instance === null) { + self::$instance = new self(); + } + return self::$instance; + } + + private static function getStringArrayEnv(string $string, array $array): array + { + $value = getenv($string); + return empty($value) ? $array : array_map('trim', explode(';', $value)); + } + +} \ No newline at end of file diff --git a/app/config/DatabaseConfig.php b/app/config/DatabaseConfig.php new file mode 100644 index 0000000..21f771a --- /dev/null +++ b/app/config/DatabaseConfig.php @@ -0,0 +1,48 @@ +host; + } + } + public string $username = 'root' { + get { + return $this->username; + } + } + public string $password = '' { + get { + return $this->password; + } + } + public string $database = 'opm_ectms' { + get { + return $this->database; + } + } + + public function __construct() + { + $this->host = getenv('DB_HOST'); + $this->username = getenv('DB_USER'); + $this->password = getenv('DB_PASSWORD'); + $this->database = getenv('DB_NAME'); + } + + /** + * @param $connection string|null + * @return Connection + */ + public function getConnection(?string $connection = null): Connection + { + return Db::connection($connection); + } +} \ No newline at end of file diff --git a/app/controller/IndexController.php b/app/controller/IndexController.php new file mode 100644 index 0000000..396ea80 --- /dev/null +++ b/app/controller/IndexController.php @@ -0,0 +1,42 @@ + + * { + padding: 0; + margin: 0; + } + iframe { + border: none; + overflow: scroll; + } + + +EOF; + } + + public function view(Request $request) + { + return view('index/view', ['name' => 'webman']); + } + + public function json(Request $request) + { + return json(['code' => 0, 'msg' => 'ok']); + } + +} diff --git a/app/functions.php b/app/functions.php new file mode 100644 index 0000000..5c9c58d --- /dev/null +++ b/app/functions.php @@ -0,0 +1,4 @@ + + * @copyright walkor + * @link http://www.workerman.net/ + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ + +namespace app\middleware; + +use Webman\MiddlewareInterface; +use Webman\Http\Response; +use Webman\Http\Request; + +/** + * Class StaticFile + * @package app\middleware + */ +class StaticFile implements MiddlewareInterface +{ + public function process(Request $request, callable $handler): Response + { + // Access to files beginning with. Is prohibited + if (strpos($request->path(), '/.') !== false) { + return response('

403 forbidden

', 403); + } + /** @var Response $response */ + $response = $handler($request); + // Add cross domain HTTP header + /*$response->withHeaders([ + 'Access-Control-Allow-Origin' => '*', + 'Access-Control-Allow-Credentials' => 'true', + ]);*/ + return $response; + } +} diff --git a/app/model/为保证数据隔离与方法污染请使用repo做数据查询与操作 b/app/model/为保证数据隔离与方法污染请使用repo做数据查询与操作 new file mode 100644 index 0000000..e69de29 diff --git a/app/process/Http.php b/app/process/Http.php new file mode 100644 index 0000000..f462c3a --- /dev/null +++ b/app/process/Http.php @@ -0,0 +1,10 @@ + + * @copyright walkor + * @link http://www.workerman.net/ + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ + +namespace app\process; + +use FilesystemIterator; +use RecursiveDirectoryIterator; +use RecursiveIteratorIterator; +use SplFileInfo; +use Workerman\Timer; +use Workerman\Worker; + +/** + * Class FileMonitor + * @package process + */ +class Monitor +{ + /** + * @var array + */ + protected array $paths = []; + + /** + * @var array + */ + protected array $extensions = []; + + /** + * @var array + */ + protected array $loadedFiles = []; + + /** + * @var int + */ + protected int $ppid = 0; + + /** + * Pause monitor + * @return void + */ + public static function pause(): void + { + file_put_contents(static::lockFile(), time()); + } + + /** + * Resume monitor + * @return void + */ + public static function resume(): void + { + clearstatcache(); + if (is_file(static::lockFile())) { + unlink(static::lockFile()); + } + } + + /** + * Whether monitor is paused + * @return bool + */ + public static function isPaused(): bool + { + clearstatcache(); + return file_exists(static::lockFile()); + } + + /** + * Lock file + * @return string + */ + protected static function lockFile(): string + { + return runtime_path('monitor.lock'); + } + + /** + * FileMonitor constructor. + * @param $monitorDir + * @param $monitorExtensions + * @param array $options + */ + public function __construct($monitorDir, $monitorExtensions, array $options = []) + { + $this->ppid = function_exists('posix_getppid') ? posix_getppid() : 0; + static::resume(); + $this->paths = (array)$monitorDir; + $this->extensions = $monitorExtensions; + foreach (get_included_files() as $index => $file) { + $this->loadedFiles[$file] = $index; + if (strpos($file, 'webman-framework/src/support/App.php')) { + break; + } + } + if (!Worker::getAllWorkers()) { + return; + } + $disableFunctions = explode(',', ini_get('disable_functions')); + if (in_array('exec', $disableFunctions, true)) { + echo "\nMonitor file change turned off because exec() has been disabled by disable_functions setting in " . PHP_CONFIG_FILE_PATH . "/php.ini\n"; + } else { + if ($options['enable_file_monitor'] ?? true) { + Timer::add(1, function () { + $this->checkAllFilesChange(); + }); + } + } + + $memoryLimit = $this->getMemoryLimit($options['memory_limit'] ?? null); + if ($memoryLimit && ($options['enable_memory_monitor'] ?? true)) { + Timer::add(60, [$this, 'checkMemory'], [$memoryLimit]); + } + } + + /** + * @param $monitorDir + * @return bool + */ + public function checkFilesChange($monitorDir): bool + { + static $lastMtime, $tooManyFilesCheck; + if (!$lastMtime) { + $lastMtime = time(); + } + clearstatcache(); + if (!is_dir($monitorDir)) { + if (!is_file($monitorDir)) { + return false; + } + $iterator = [new SplFileInfo($monitorDir)]; + } else { + // recursive traversal directory + $dirIterator = new RecursiveDirectoryIterator($monitorDir, FilesystemIterator::SKIP_DOTS | FilesystemIterator::FOLLOW_SYMLINKS); + $iterator = new RecursiveIteratorIterator($dirIterator); + } + $count = 0; + foreach ($iterator as $file) { + $count ++; + /** @var SplFileInfo $file */ + if (is_dir($file->getRealPath())) { + continue; + } + // check mtime + if (in_array($file->getExtension(), $this->extensions, true) && $lastMtime < $file->getMTime()) { + $lastMtime = $file->getMTime(); + if (DIRECTORY_SEPARATOR === '/' && isset($this->loadedFiles[$file->getRealPath()])) { + echo "$file updated but cannot be reloaded because only auto-loaded files support reload.\n"; + continue; + } + $var = 0; + exec('"'.PHP_BINARY . '" -l ' . $file, $out, $var); + if ($var) { + continue; + } + // send SIGUSR1 signal to master process for reload + if (DIRECTORY_SEPARATOR === '/') { + if ($masterPid = $this->getMasterPid()) { + echo $file . " updated and reload\n"; + posix_kill($masterPid, SIGUSR1); + } else { + echo "Master process has gone away and can not reload\n"; + } + return true; + } + echo $file . " updated and reload\n"; + return true; + } + } + if (!$tooManyFilesCheck && $count > 1000) { + echo "Monitor: There are too many files ($count files) in $monitorDir which makes file monitoring very slow\n"; + $tooManyFilesCheck = 1; + } + return false; + } + + /** + * @return int + */ + public function getMasterPid(): int + { + if ($this->ppid === 0) { + return 0; + } + if (function_exists('posix_kill') && !posix_kill($this->ppid, 0)) { + echo "Master process has gone away\n"; + return $this->ppid = 0; + } + if (PHP_OS_FAMILY !== 'Linux') { + return $this->ppid; + } + $cmdline = "/proc/$this->ppid/cmdline"; + if (!is_readable($cmdline) || !($content = file_get_contents($cmdline)) || (!str_contains($content, 'WorkerMan') && !str_contains($content, 'php'))) { + // Process not exist + $this->ppid = 0; + } + return $this->ppid; + } + + /** + * @return bool + */ + public function checkAllFilesChange(): bool + { + if (static::isPaused()) { + return false; + } + foreach ($this->paths as $path) { + if ($this->checkFilesChange($path)) { + return true; + } + } + return false; + } + + /** + * @param $memoryLimit + * @return void + */ + public function checkMemory($memoryLimit): void + { + if (static::isPaused() || $memoryLimit <= 0) { + return; + } + $masterPid = $this->getMasterPid(); + if ($masterPid <= 0) { + echo "Master process has gone away\n"; + return; + } + + $childrenFile = "/proc/$masterPid/task/$masterPid/children"; + if (!is_file($childrenFile) || !($children = file_get_contents($childrenFile))) { + return; + } + foreach (explode(' ', $children) as $pid) { + $pid = (int)$pid; + $statusFile = "/proc/$pid/status"; + if (!is_file($statusFile) || !($status = file_get_contents($statusFile))) { + continue; + } + $mem = 0; + if (preg_match('/VmRSS\s*?:\s*?(\d+?)\s*?kB/', $status, $match)) { + $mem = $match[1]; + } + $mem = (int)($mem / 1024); + if ($mem >= $memoryLimit) { + posix_kill($pid, SIGINT); + } + } + } + + /** + * Get memory limit + * @param $memoryLimit + * @return int + */ + protected function getMemoryLimit($memoryLimit): int + { + if ($memoryLimit === 0) { + return 0; + } + $usePhpIni = false; + if (!$memoryLimit) { + $memoryLimit = ini_get('memory_limit'); + $usePhpIni = true; + } + + if ($memoryLimit == -1) { + return 0; + } + $unit = strtolower($memoryLimit[strlen($memoryLimit) - 1]); + $memoryLimit = (int)$memoryLimit; + if ($unit === 'g') { + $memoryLimit = 1024 * $memoryLimit; + } else if ($unit === 'k') { + $memoryLimit = ($memoryLimit / 1024); + } else if ($unit === 'm') { + $memoryLimit = (int)($memoryLimit); + } else if ($unit === 't') { + $memoryLimit = (1024 * 1024 * $memoryLimit); + } else { + $memoryLimit = ($memoryLimit / (1024 * 1024)); + } + if ($memoryLimit < 50) { + $memoryLimit = 50; + } + if ($usePhpIni) { + $memoryLimit = (0.8 * $memoryLimit); + } + return (int)$memoryLimit; + } + +} diff --git a/app/process/TcpServer.php b/app/process/TcpServer.php new file mode 100644 index 0000000..817971c --- /dev/null +++ b/app/process/TcpServer.php @@ -0,0 +1,134 @@ + 连接池,key为客户端IP,value为连接对象 + */ + private static array $connections = []; + + public function __construct() + { + // 初始化 FlowMain + FlowMain::getInstance(); + } + + /** + * 获取连接池 + */ + public static function getConnections(): array + { + return self::$connections; + } + + /** + * 获取链接 + */ + public static function getConnection(string $ip): ?TcpConnection + { + return self::$connections[$ip] ?? null; + } + + /** + * 获取所有IP + */ + public static function getAllIp(): array + { + return array_keys(self::$connections); + } + + /** + * 获取所有连接 + */ + public static function getAllConnections(): array + { + return self::$connections; + } + + /** + * Worker启动时触发 + */ + public function onWorkerStart($worker): void + { + Logger::info("TcpServer started"); + } + + + /** + * 连接时触发 + */ + public function onConnect(TcpConnection $connection): void + { + self::$connections[$connection->getRemoteIp()] = $connection; + Log::info("客户端链接到主机: {$connection->getRemoteIp()}"); + } + + /** + * 接收数据时触发 + */ + public function onMessage(TcpConnection $connection, $data): void + { + $ip = $connection->getRemoteIp(); + self::$connections[$ip] = $connection; + $packet = PacketParserFactory::new($data); + $context = new PacketContext(self::getConnections(), $connection, $packet); + $this->logMessage($packet, $ip); + if ($packet->isMatched) { + $result = FlowMain::getInstance()->main($context); + $connection->send($result->getFullVoice()); + } else { + $connection->send("Packet is not matched"); + } + } + + /** + * 连接关闭时触发 + */ + public function onClose(TcpConnection $connection): void + { + unset(self::$connections[$connection->getRemoteIp()]); + Logger::info("客户端已断开连接:{$connection->getRemoteIp()}"); + } + + /** + * @param \app\net\Packet $packet + * @param string $ip + * @return void + */ + public function logMessage(\app\net\Packet $packet, string $ip): void + { + if ($packet->isMatched) { + Logger::debug(Logger::generateTextBox([ + "---------------------------- PACKET --------------------------------", + "IP Address : {$ip}", + "Packet Length: {$packet->length}", + "Hex Packet : {$packet->hexString}", + "Packet Type : {$packet->hexType->name}", + "---------------------------- DATA --------------------------------", + "Reader Info : {$packet->reader}", + "Card Info : {$packet->card}", + "Gateway Info : {$packet->gateway}", + ])); + } else { + Logger::debug("Packet is not matched"); + Logger::debug(Logger::generateTextBox([ + "---------------------------- PACKET --------------------------------", + "IP Address : {$ip}", + "Packet Length: {$packet->length}", + "Packet Type : {$packet->hexType->name}", + "---------------------------- DATA --------------------------------", + "Hex Packet : {$packet->hexString}" + ])); + } + } +} \ No newline at end of file diff --git a/app/repository/BaseRepository.php b/app/repository/BaseRepository.php new file mode 100644 index 0000000..3757859 --- /dev/null +++ b/app/repository/BaseRepository.php @@ -0,0 +1,12 @@ + trim($row) !== ''); + if (empty($validRows)) return ''; + + // 计算最长行的字符长度(核心:按实际字符数计算) + $maxContentLen = max(array_map('mb_strlen', $validRows)); + + // 格式化每行:左侧固定前缀,右侧补空格至最长长度 + $formattedRows = array_map(function ($row) use ($maxContentLen) { + $currentLen = mb_strlen($row); + $padding = $maxContentLen - $currentLen; // 精准计算需要补充的空格数 + return '| ' . $row . str_repeat(' ', max(0, $padding)) . ' |'; + }, $validRows); + + // 计算边框总宽度(格式化后最长行的长度) + $borderLen = max(array_map('strlen', $formattedRows)); + $borderLine = str_repeat($borderChar, $borderLen); + + // 空行(和内容行宽度一致) + $emptyLine = '| ' . str_repeat(' ', $maxContentLen) . ' |'; + + // 拼接文本框 + return "\n{$borderLine}\n{$emptyLine}\n" . implode("\n", $formattedRows) . "\n{$emptyLine}\n{$borderLine}"; + } + + /** + * 静态信息级别日志 + * @param string $message 日志消息 + * @param array|Throwable $context 占位符参数或异常对象 + * @param string $name 日志器名称 + */ + public static function info(string $message, array|Throwable $context = [], string $name = '__default'): void + { + self::new($name)->info($message, $context); + } + + /** + * 静态调试级别日志 + * @param string $message 日志消息 + * @param array|Throwable $context 占位符参数或异常对象 + * @param string $name 日志器名称 + */ + public static function debug(string $message, array|Throwable $context = [], string $name = '__default'): void + { + self::new($name)->debug($message, $context); + } + + /** + * 静态warn方法(warning别名) + * @param string $message 日志消息 + * @param array|Throwable $context 占位符参数或异常对象 + * @param string $name 日志器名称 + */ + public static function warn(string $message, array|Throwable $context = [], string $name = '__default'): void + { + self::new($name)->warn($message, $context); + } + + /** + * 静态warn方法(warning别名) + * @param string $message 日志消息 + * @param array|Throwable $context 占位符参数或异常对象 + * @param string $name 日志器名称 + */ + public static function warning(string $message, array|Throwable $context = [], string $name = '__default'): void + { + self::new($name)->warn($message, $context); + } + + /** + * 静态错误级别日志 + * @param string $message 日志消息 + * @param array|Throwable $context 占位符参数或异常对象 + * @param string $name 日志器名称 + */ + public static function error(string $message, array|Throwable $context = [], string $name = '__default'): void + { + self::new($name)->error($message, $context); + } + + /** + * 静态通用日志方法 + * @param string $level 日志级别 + * @param string $message 日志消息 + * @param array|Throwable $context 占位符参数或异常对象 + * @param string $name 日志器名称 + */ + public static function log(string $level, string $message, array|Throwable $context = [], string $name = '__default'): void + { + self::new($name)->log($level, $message, $context); + } +} + +/** + * 日志实例类(仅提供实例方法) + */ +class LoggerIns +{ + public $name = ''; + + /** + * 构造函数:初始化日志器名称 + * @param string $name 日志器名称,默认 '__default' + */ + public function __construct(string $name = '__default') + { + $this->name = $name; + } + + /** + * 信息级别日志 + * @param string $message 日志消息(支持 {} 占位符) + * @param array|Throwable $context 占位符替换参数或异常对象 + */ + public function info(string $message, array|Throwable $context = []): void + { + $processedMessage = $this->buildMessage($message, $context); + if (empty($processedMessage)) return; + Log::info($processedMessage); + } + + /** + * 调试级别日志 + * @param string $message 日志消息(支持 {} 占位符) + * @param array|Throwable $context 占位符替换参数或异常对象 + */ + public function debug(string $message, array|Throwable $context = []): void + { + $processedMessage = $this->buildMessage($message, $context); + if (empty($processedMessage)) return; + Log::debug($processedMessage); + } + + /** + * 警告级别日志 + * @param string $message 日志消息(支持 {} 占位符) + * @param array|Throwable $context 占位符替换参数或异常对象 + */ + public function warning(string $message, array|Throwable $context = []): void + { + $processedMessage = $this->buildMessage($message, $context); + if (empty($processedMessage)) return; + Log::warning($processedMessage); + } + + /** + * warn 方法 - warning 的别名 + * @param string $message 日志消息(支持 {} 占位符) + * @param array|Throwable $context 占位符替换参数或异常对象 + */ + public function warn(string $message, array|Throwable $context = []): void + { + $this->warning($message, $context); + } + + /** + * 错误级别日志 + * @param string $message 日志消息(支持 {} 占位符) + * @param array|Throwable $context 占位符替换参数或异常对象 + */ + public function error(string $message, array|Throwable $context = []): void + { + $processedMessage = $this->buildMessage($message, $context); + if (empty($processedMessage)) return; + Log::error($processedMessage); + } + + /** + * 通用日志方法 + * @param string $level 日志级别 (info/debug/warning/warn/error/fatal) + * @param string $message 日志消息(支持 {} 占位符) + * @param array|Throwable $context 占位符替换参数或异常对象 + */ + public function log(string $level, string $message, array|Throwable $context = []): void + { + // 验证并处理日志级别(兼容warn别名) + $validLevels = ['info', 'debug', 'warning', 'warn', 'error', 'fatal']; + $level = strtolower($level); + + // 将warn映射为warning + if ($level === 'warn') { + $level = 'warning'; + } + + if (!in_array($level, $validLevels)) { + $level = 'info'; // 默认使用info级别 + } + + $processedMessage = $this->buildMessage($message, $context); + if (empty($processedMessage)) return; + Log::{$level}($processedMessage); + } + + /** + * 处理日志消息:替换占位符、处理异常、添加名称前缀 + * @param string $message 原始日志消息 + * @param array|Throwable $context 占位符参数或异常对象 + * @return string 处理后的日志消息 + */ + private function buildMessage(string $message, array|Throwable $context = []): string + { + // 1. 先处理占位符替换(无论是否有异常,先处理消息本身) + $processedMessage = $this->replacePlaceholders($message, is_array($context) ? $context : []); + + // 2. 处理异常对象:换行输出完整异常内容,优化堆栈缩进 + if ($context instanceof Throwable) { + // 转换异常为字符串并优化堆栈缩进(制表符) + $exceptionStr = $this->formatExceptionStackTrace((string)$context); + // 使用 PHP_EOL 保证跨系统换行 + $processedMessage .= PHP_EOL . $exceptionStr; + } + + // 3. 添加名称前缀 [name] + $namePrefix = "name}>"; + $result = $namePrefix . $processedMessage; +// if ($this->isFilter()) return ""; + return $result; + } + + + /** + * 解析调用堆栈,获取实际业务代码的类名和方法名 + * @return array ['class' => 业务类名, 'method' => 业务方法名] + */ + private function parseBusinessStackInfo(): array + { + $stackInfo = ['class' => '', 'method' => '']; + // 获取调用栈(忽略参数,避免性能损耗,取前10层足够) + $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 10); + + // 跳过的日志相关类/方法(根据实际项目调整) + $skipPatterns = [ + 'class' => [ + 'Monolog\\', // 跳过Monolog核心类 + __CLASS__, // 跳过当前类自身 + 'app\\logger\\', // 跳过大日志封装类(根据你的项目调整) + ], + 'method' => [ + 'buildMessage', // 跳过当前方法 + 'isFilter', // 跳过过滤方法 + 'parseBusinessStackInfo', // 跳过堆栈解析方法 + 'replacePlaceholders',// 跳過占位符替换方法 + 'formatExceptionStackTrace', // 跳过异常格式化方法 + 'log', 'error', 'info', 'warning', 'debug' // 跳过日志级别方法 + ] + ]; + + // 遍历调用栈,找到第一个业务代码层 + foreach ($trace as $step) { + // 跳过无文件/无方法的栈帧 + if (!isset($step['file']) || !isset($step['function'])) { + continue; + } + + // 跳过日志相关类 + $currentClass = $step['class'] ?? ''; + $isSkipClass = false; + foreach ($skipPatterns['class'] as $pattern) { + if ($pattern && str_contains($currentClass, $pattern)) { + $isSkipClass = true; + break; + } + } + if ($isSkipClass) { + continue; + } + + // 跳过日志相关方法 + $currentMethod = $step['function']; + if (in_array($currentMethod, $skipPatterns['method'])) { + continue; + } + + // 解析业务类名(简化为项目内相对路径,和原逻辑一致) + $projectRoot = defined('BASE_PATH') ? BASE_PATH : (function_exists('base_path') ? base_path() : ''); + $file = $step['file']; + if ($projectRoot && str_starts_with($file, $projectRoot)) { + $file = substr($file, strlen($projectRoot) + 1); + // 转换为类名格式(app/controller/Index.php → app.controller.Index) + $class = str_replace(['.php', '\\', '/'], ['', '.', '.'], $file); + } else { + $class = $currentClass ?: basename($file, '.php'); + } + + // 赋值并终止遍历(找到第一个业务层即可) + $stackInfo['class'] = trim($class); + $stackInfo['method'] = $currentMethod; + break; + } + + return $stackInfo; + } + + /** + * 格式化异常堆栈:让Stack trace下的每一行添加制表符缩进 + * @param string $exceptionStr 原始异常字符串 + * @return string 格式化后的异常字符串 + */ + private function formatExceptionStackTrace(string $exceptionStr): string + { + // 正则匹配 Stack trace: 后的所有行,并在每行前添加制表符 + $pattern = '/(Stack trace:\s*)(.*)$/s'; + return preg_replace_callback($pattern, function ($matches) { + // $matches[1] 是 "Stack trace: " + // $matches[2] 是堆栈内容 + $stackTrace = $matches[2]; + // 将堆栈的每一行(#开头)前添加 制表符(\t) + $formattedStack = preg_replace('/(#\d+)/', "\t$1", $stackTrace); + return $matches[1] . $formattedStack; + }, $exceptionStr); + } + + /** + * 内部方法:替换 {} 占位符 + * @param string $message 原始消息 + * @param array $context 替换参数 + * @return string 替换后的消息 + */ + private function replacePlaceholders(string $message, array $context = []): string + { + if (empty($context)) { + return $message; + } + + // 将上下文参数转换为安全的字符串 + $replacements = array_map(function ($value) { + if ($value === null) { + return 'null'; + } + if (is_bool($value)) { + return $value ? 'true' : 'false'; + } + if (is_array($value) || is_object($value)) { + return json_encode($value, JSON_UNESCAPED_UNICODE | JSON_PARTIAL_OUTPUT_ON_ERROR); + } + return (string)$value; + }, $context); + + // 按顺序替换 {} 占位符 + $index = 0; + $result = preg_replace_callback('/\{\}/', function () use (&$index, $replacements) { + return isset($replacements[$index]) ? $replacements[$index++] : '{}'; + }, $message); + + // 替换失败时返回原始消息 + return $result ?: $message; + } +} \ No newline at end of file diff --git a/app/utils/ModelAutoGenerator.php b/app/utils/ModelAutoGenerator.php new file mode 100644 index 0000000..7799d64 --- /dev/null +++ b/app/utils/ModelAutoGenerator.php @@ -0,0 +1,337 @@ + bool, 'msg' => string] + */ + public static function generate(string $tableName, string $modelName, bool $force = false): array + { + // 1. 定义模型文件路径(兼容webman的app_path) + $modelPath = app_path() . '/model/' . ucfirst($modelName) . '.php'; + $modelClassName = ucfirst($modelName); // 确保类名首字母大写 + + // 2. 检查文件是否已存在,存在则返回不覆盖提示 + if (file_exists($modelPath) && !$force) { + return [ + 'status' => false, + 'msg' => "模型文件 {$modelClassName}.php 已存在,不执行覆盖操作" + ]; + } + + try { + // 3. 读取数据表基本信息(包含表注释)- 修复参数绑定问题 + $tableComment = self::getTableComment($tableName); + // 4. 读取数据表结构(包含字段注释、类型等) + $tableStruct = self::getTableStruct($tableName); + + if (empty($tableStruct)) { + return [ + 'status' => false, + 'msg' => "数据表 {$tableName} 无字段信息,生成失败" + ]; + } + + // 5. 构建模型文件内容(传入表注释) + $modelContent = self::buildModelContent($tableName, $modelClassName, $tableStruct, $tableComment); + + // 6. 确保model目录存在 + if (!is_dir(dirname($modelPath))) { + mkdir(dirname($modelPath), 0755, true); + } + + // 7. 写入模型文件 + $writeResult = file_put_contents($modelPath, $modelContent); + if ($writeResult === false) { + throw new RuntimeException("模型文件写入失败,检查目录权限"); + } + + return [ + 'status' => true, + 'msg' => "模型 {$modelClassName}.php 已成功生成至 app/model 目录" + ]; + + } catch (\Exception $e) { + return [ + 'status' => false, + 'msg' => "生成失败:{$e->getMessage()}" + ]; + } + } + + /** + * 批量生成所有数据表的模型文件 + * + * @param bool $force 是否强制覆盖已存在的模型文件 + * @param array $excludeTables 排除的数据表(如:['migrations', 'logs']) + * @return array 批量生成结果 ['success' => 成功数量, 'fail' => 失败列表] + */ + public static function generate_all(bool $force = false, array $excludeTables = []): array + { + $result = [ + 'success' => 0, + 'fail' => [] + ]; + + try { + // 1. 获取数据库中所有数据表 + $tables = Db::select("SHOW TABLES"); + $tableNameKey = 'Tables_in_' . config('database.connections.mysql.database'); + + foreach ($tables as $table) { + $tableName = $table->$tableNameKey; + + // 跳过排除的表 + if (in_array($tableName, $excludeTables)) { + continue; + } + + // 跳过视图(以 v_ 开头的表,避免视图生成模型) + if (str_starts_with($tableName, 'v_')) { + continue; + } + + // 2. 表名转模型名(下划线转驼峰,如 user_info → UserInfo) + $modelName = self::tableNameToModelName($tableName); + + // 3. 调用单个生成方法 + $generateResult = self::generate($tableName, $modelName, $force); + if ($generateResult['status']) { + $result['success']++; + } else { + $result['fail'][] = [ + 'table' => $tableName, + 'model' => $modelName, + 'reason' => $generateResult['msg'] + ]; + } + } + + return $result; + + } catch (\Exception $e) { + $result['fail'][] = [ + 'table' => 'all', + 'model' => 'all', + 'reason' => "批量生成异常:{$e->getMessage()}" + ]; + return $result; + } + } + + /** + * 获取数据表注释(修复参数绑定问题) + * + * @param string $tableName 数据表名 + * @return string 表注释(无注释则返回空字符串) + */ + private static function getTableComment(string $tableName): string + { + // 修复核心:不使用参数绑定,直接拼接表名(先过滤表名防止注入) + $safeTableName = preg_replace('/[^a-zA-Z0-9_]/', '', $tableName); + $sql = "SHOW TABLE STATUS LIKE '{$safeTableName}'"; + + try { + $tableInfo = Db::select($sql); + } catch (\Exception $e) { + // 读取表注释失败时返回空字符串,不影响模型生成 + return ''; + } + + if (empty($tableInfo)) { + return ''; + } + + // 兼容不同数据库驱动的字段名(Comment/comment) + $comment = $tableInfo[0]->Comment ?? $tableInfo[0]->comment ?? ''; + return trim($comment); + } + + /** + * 获取数据表结构(修复关键字冲突+兼容字段注释读取) + * + * @param string $tableName 数据表名 + * @return array 表结构数组 + */ + private static function getTableStruct(string $tableName): array + { + // 表名安全过滤 + $safeTableName = preg_replace('/[^a-zA-Z0-9_]/', '', $tableName); + // 修复关键字冲突:给别名加反引号,避免与MySQL保留字冲突 + $sql = "SELECT + COLUMN_NAME AS `Field`, + DATA_TYPE AS `TypeSimple`, + COLUMN_TYPE AS `Type`, + IS_NULLABLE AS `IsNull`, + COLUMN_KEY AS `ColumnKey`, + COLUMN_DEFAULT AS `ColumnDefault`, + COLUMN_COMMENT AS `Comment` + FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = ? + AND TABLE_NAME = ? + ORDER BY ORDINAL_POSITION"; + + // 使用参数绑定读取字段信息(INFORMATION_SCHEMA 支持参数绑定) + $tableStruct = Db::select($sql, [ + config('database.connections.mysql.database'), + $safeTableName + ]); + + // 格式化字段信息,确保注释字段统一 + return array_map(function ($field) { + return (object)[ + 'Field' => $field->Field ?? '', + 'Type' => $field->Type ?? $field->TypeSimple ?? '', + 'Key' => $field->ColumnKey ?? '', // 对应修改后的别名 + // 优先读取 COMMENT,兼容大小写,确保去空格 + 'Comment' => trim($field->Comment ?? $field->comment ?? ''), + 'Null' => $field->IsNull ?? '', // 对应修改后的别名 + 'Default' => $field->ColumnDefault ?? '' // 对应修改后的别名 + ]; + }, $tableStruct); + } + + /** + * 构建模型文件内容(确保字段注释正确展示) + * + * @param string $tableName 数据表名 + * @param string $modelClassName 模型类名 + * @param array $tableStruct 表结构信息 + * @param string $tableComment 表注释 + * @return string 模型文件内容 + */ + private static function buildModelContent(string $tableName, string $modelClassName, array $tableStruct, string $tableComment): string + { + // 提取主键 + $primaryKey = 'id'; + foreach ($tableStruct as $field) { + if ($field->Key === 'PRI') { + $primaryKey = $field->Field; + break; + } + } + + // 构建字段注释(格式:字段名 数据库注释) + $fieldComments = []; + foreach ($tableStruct as $field) { + $fieldName = $field->Field; // 原始字段名(如 dt) + $dbComment = $field->Comment; // 数据库注释(如 基准时间) + + // 拼接注释:字段名 + (有数据库注释则加)数据库注释 + $fullComment = $fieldName; + if (!empty($dbComment)) { + $fullComment .= " {$dbComment}"; + } + + // 补充字段属性说明(非空、主键、默认值) + $extComment = []; + if ($field->Null === 'NO') { + $extComment[] = '非空'; + } + if ($field->Key === 'PRI') { + $extComment[] = '主键'; + } + if ($field->Default !== '') { + $extComment[] = "默认值:{$field->Default}"; + } + + // 如有属性说明,追加到注释末尾 + if (!empty($extComment)) { + $fullComment .= '(' . implode(',', $extComment) . ')'; + } + + $fieldType = self::dbTypeToPhpType($field->Type); + $fieldComments[] = " * @property {$fieldType} \${$fieldName} {$fullComment}"; + } + $fieldCommentsStr = implode("\n", $fieldComments); + + // 构建表注释(无注释则使用默认描述) + $tableCommentStr = !empty($tableComment) ? $tableComment : "{$tableName} 数据表模型"; + + // 模型模板(适配webman的think-orm) + return << 'int', + 'float', 'double', 'decimal' => 'float', + 'date', 'time', 'datetime', 'timestamp' => 'string', + 'bool', 'boolean' => 'bool', + default => 'string' + }; + } +} \ No newline at end of file diff --git a/app/view/index/view.html b/app/view/index/view.html new file mode 100644 index 0000000..67ebb26 --- /dev/null +++ b/app/view/index/view.html @@ -0,0 +1,14 @@ + + + + + + + + webman + + + +hello + + diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..c848049 --- /dev/null +++ b/composer.json @@ -0,0 +1,71 @@ +{ + "name": "workerman/webman", + "type": "project", + "keywords": [ + "high performance", + "http service" + ], + "homepage": "https://www.workerman.net", + "license": "MIT", + "description": "High performance HTTP Service Framework.", + "authors": [ + { + "name": "walkor", + "email": "walkor@workerman.net", + "homepage": "https://www.workerman.net", + "role": "Developer" + } + ], + "support": { + "email": "walkor@workerman.net", + "issues": "https://github.com/walkor/webman/issues", + "forum": "https://wenda.workerman.net/", + "wiki": "https://workerman.net/doc/webman", + "source": "https://github.com/walkor/webman" + }, + "require": { + "php": ">=8.4", + "workerman/webman-framework": "^2.1", + "monolog/monolog": "^2.0", + "webman/console": "^2.2", + "webman/database": "^2.1", + "webman/think-orm": "^2.1", + "illuminate/pagination": "^12.53", + "illuminate/events": "^12.53", + "symfony/var-dumper": "^8.0", + "vlucas/phpdotenv": "^5.6" + }, + "suggest": { + "ext-event": "For better performance. " + }, + "autoload": { + "psr-4": { + "": "./", + "app\\": "./app", + "App\\": "./app", + "app\\View\\Components\\": "./app/view/components" + } + }, + "scripts": { + "post-package-install": [ + "support\\Plugin::install" + ], + "post-package-update": [ + "support\\Plugin::install" + ], + "pre-package-uninstall": [ + "support\\Plugin::uninstall" + ], + "post-create-project-cmd": [ + "support\\Setup::run" + ], + "setup-webman": [ + "support\\Setup::run" + ] + }, + "minimum-stability": "dev", + "prefer-stable": true, + "require-dev": { + "phpunit/phpunit": "^13.0" + } +} diff --git a/composer.lock b/composer.lock new file mode 100644 index 0000000..783b309 --- /dev/null +++ b/composer.lock @@ -0,0 +1,6981 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "ecd59dd718e1f410ae9c5a01fdd8c1be", + "packages": [ + { + "name": "brick/math", + "version": "0.14.8", + "source": { + "type": "git", + "url": "https://github.com/brick/math.git", + "reference": "63422359a44b7f06cae63c3b429b59e8efcc0629" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/brick/math/zipball/63422359a44b7f06cae63c3b429b59e8efcc0629", + "reference": "63422359a44b7f06cae63c3b429b59e8efcc0629", + "shasum": "" + }, + "require": { + "php": "^8.2" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.2", + "phpstan/phpstan": "2.1.22", + "phpunit/phpunit": "^11.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "Brick\\Math\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Arbitrary-precision arithmetic library", + "keywords": [ + "Arbitrary-precision", + "BigInteger", + "BigRational", + "arithmetic", + "bigdecimal", + "bignum", + "bignumber", + "brick", + "decimal", + "integer", + "math", + "mathematics", + "rational" + ], + "support": { + "issues": "https://github.com/brick/math/issues", + "source": "https://github.com/brick/math/tree/0.14.8" + }, + "funding": [ + { + "url": "https://github.com/BenMorel", + "type": "github" + } + ], + "time": "2026-02-10T14:33:43+00:00" + }, + { + "name": "carbonphp/carbon-doctrine-types", + "version": "3.2.0", + "source": { + "type": "git", + "url": "https://github.com/CarbonPHP/carbon-doctrine-types.git", + "reference": "18ba5ddfec8976260ead6e866180bd5d2f71aa1d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/CarbonPHP/carbon-doctrine-types/zipball/18ba5ddfec8976260ead6e866180bd5d2f71aa1d", + "reference": "18ba5ddfec8976260ead6e866180bd5d2f71aa1d", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "conflict": { + "doctrine/dbal": "<4.0.0 || >=5.0.0" + }, + "require-dev": { + "doctrine/dbal": "^4.0.0", + "nesbot/carbon": "^2.71.0 || ^3.0.0", + "phpunit/phpunit": "^10.3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Carbon\\Doctrine\\": "src/Carbon/Doctrine/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "KyleKatarn", + "email": "kylekatarnls@gmail.com" + } + ], + "description": "Types to use Carbon in Doctrine", + "keywords": [ + "carbon", + "date", + "datetime", + "doctrine", + "time" + ], + "support": { + "issues": "https://github.com/CarbonPHP/carbon-doctrine-types/issues", + "source": "https://github.com/CarbonPHP/carbon-doctrine-types/tree/3.2.0" + }, + "funding": [ + { + "url": "https://github.com/kylekatarnls", + "type": "github" + }, + { + "url": "https://opencollective.com/Carbon", + "type": "open_collective" + }, + { + "url": "https://tidelift.com/funding/github/packagist/nesbot/carbon", + "type": "tidelift" + } + ], + "time": "2024-02-09T16:56:22+00:00" + }, + { + "name": "doctrine/inflector", + "version": "2.1.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/inflector.git", + "reference": "6d6c96277ea252fc1304627204c3d5e6e15faa3b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/inflector/zipball/6d6c96277ea252fc1304627204c3d5e6e15faa3b", + "reference": "6d6c96277ea252fc1304627204c3d5e6e15faa3b", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "doctrine/coding-standard": "^12.0 || ^13.0", + "phpstan/phpstan": "^1.12 || ^2.0", + "phpstan/phpstan-phpunit": "^1.4 || ^2.0", + "phpstan/phpstan-strict-rules": "^1.6 || ^2.0", + "phpunit/phpunit": "^8.5 || ^12.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Inflector\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "PHP Doctrine Inflector is a small library that can perform string manipulations with regard to upper/lowercase and singular/plural forms of words.", + "homepage": "https://www.doctrine-project.org/projects/inflector.html", + "keywords": [ + "inflection", + "inflector", + "lowercase", + "manipulation", + "php", + "plural", + "singular", + "strings", + "uppercase", + "words" + ], + "support": { + "issues": "https://github.com/doctrine/inflector/issues", + "source": "https://github.com/doctrine/inflector/tree/2.1.0" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finflector", + "type": "tidelift" + } + ], + "time": "2025-08-10T19:31:58+00:00" + }, + { + "name": "fruitcake/php-cors", + "version": "v1.4.0", + "source": { + "type": "git", + "url": "https://github.com/fruitcake/php-cors.git", + "reference": "38aaa6c3fd4c157ffe2a4d10aa8b9b16ba8de379" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/fruitcake/php-cors/zipball/38aaa6c3fd4c157ffe2a4d10aa8b9b16ba8de379", + "reference": "38aaa6c3fd4c157ffe2a4d10aa8b9b16ba8de379", + "shasum": "" + }, + "require": { + "php": "^8.1", + "symfony/http-foundation": "^5.4|^6.4|^7.3|^8" + }, + "require-dev": { + "phpstan/phpstan": "^2", + "phpunit/phpunit": "^9", + "squizlabs/php_codesniffer": "^4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.3-dev" + } + }, + "autoload": { + "psr-4": { + "Fruitcake\\Cors\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fruitcake", + "homepage": "https://fruitcake.nl" + }, + { + "name": "Barryvdh", + "email": "barryvdh@gmail.com" + } + ], + "description": "Cross-origin resource sharing library for the Symfony HttpFoundation", + "homepage": "https://github.com/fruitcake/php-cors", + "keywords": [ + "cors", + "laravel", + "symfony" + ], + "support": { + "issues": "https://github.com/fruitcake/php-cors/issues", + "source": "https://github.com/fruitcake/php-cors/tree/v1.4.0" + }, + "funding": [ + { + "url": "https://fruitcake.nl", + "type": "custom" + }, + { + "url": "https://github.com/barryvdh", + "type": "github" + } + ], + "time": "2025-12-03T09:33:47+00:00" + }, + { + "name": "graham-campbell/result-type", + "version": "v1.1.4", + "source": { + "type": "git", + "url": "https://github.com/GrahamCampbell/Result-Type.git", + "reference": "e01f4a821471308ba86aa202fed6698b6b695e3b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/GrahamCampbell/Result-Type/zipball/e01f4a821471308ba86aa202fed6698b6b695e3b", + "reference": "e01f4a821471308ba86aa202fed6698b6b695e3b", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0", + "phpoption/phpoption": "^1.9.5" + }, + "require-dev": { + "phpunit/phpunit": "^8.5.41 || ^9.6.22 || ^10.5.45 || ^11.5.7" + }, + "type": "library", + "autoload": { + "psr-4": { + "GrahamCampbell\\ResultType\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + } + ], + "description": "An Implementation Of The Result Type", + "keywords": [ + "Graham Campbell", + "GrahamCampbell", + "Result Type", + "Result-Type", + "result" + ], + "support": { + "issues": "https://github.com/GrahamCampbell/Result-Type/issues", + "source": "https://github.com/GrahamCampbell/Result-Type/tree/v1.1.4" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/graham-campbell/result-type", + "type": "tidelift" + } + ], + "time": "2025-12-27T19:43:20+00:00" + }, + { + "name": "guzzlehttp/guzzle", + "version": "7.10.0", + "source": { + "type": "git", + "url": "https://github.com/guzzle/guzzle.git", + "reference": "b51ac707cfa420b7bfd4e4d5e510ba8008e822b4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/b51ac707cfa420b7bfd4e4d5e510ba8008e822b4", + "reference": "b51ac707cfa420b7bfd4e4d5e510ba8008e822b4", + "shasum": "" + }, + "require": { + "ext-json": "*", + "guzzlehttp/promises": "^2.3", + "guzzlehttp/psr7": "^2.8", + "php": "^7.2.5 || ^8.0", + "psr/http-client": "^1.0", + "symfony/deprecation-contracts": "^2.2 || ^3.0" + }, + "provide": { + "psr/http-client-implementation": "1.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "ext-curl": "*", + "guzzle/client-integration-tests": "3.0.2", + "php-http/message-factory": "^1.1", + "phpunit/phpunit": "^8.5.39 || ^9.6.20", + "psr/log": "^1.1 || ^2.0 || ^3.0" + }, + "suggest": { + "ext-curl": "Required for CURL handler support", + "ext-intl": "Required for Internationalized Domain Name (IDN) support", + "psr/log": "Required for using the Log middleware" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "GuzzleHttp\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Jeremy Lindblom", + "email": "jeremeamia@gmail.com", + "homepage": "https://github.com/jeremeamia" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://github.com/sagikazarmark" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + } + ], + "description": "Guzzle is a PHP HTTP client library", + "keywords": [ + "client", + "curl", + "framework", + "http", + "http client", + "psr-18", + "psr-7", + "rest", + "web service" + ], + "support": { + "issues": "https://github.com/guzzle/guzzle/issues", + "source": "https://github.com/guzzle/guzzle/tree/7.10.0" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/guzzle", + "type": "tidelift" + } + ], + "time": "2025-08-23T22:36:01+00:00" + }, + { + "name": "guzzlehttp/promises", + "version": "2.3.0", + "source": { + "type": "git", + "url": "https://github.com/guzzle/promises.git", + "reference": "481557b130ef3790cf82b713667b43030dc9c957" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/promises/zipball/481557b130ef3790cf82b713667b43030dc9c957", + "reference": "481557b130ef3790cf82b713667b43030dc9c957", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "phpunit/phpunit": "^8.5.44 || ^9.6.25" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Promise\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + } + ], + "description": "Guzzle promises library", + "keywords": [ + "promise" + ], + "support": { + "issues": "https://github.com/guzzle/promises/issues", + "source": "https://github.com/guzzle/promises/tree/2.3.0" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/promises", + "type": "tidelift" + } + ], + "time": "2025-08-22T14:34:08+00:00" + }, + { + "name": "guzzlehttp/psr7", + "version": "2.8.0", + "source": { + "type": "git", + "url": "https://github.com/guzzle/psr7.git", + "reference": "21dc724a0583619cd1652f673303492272778051" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/21dc724a0583619cd1652f673303492272778051", + "reference": "21dc724a0583619cd1652f673303492272778051", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.1 || ^2.0", + "ralouphie/getallheaders": "^3.0" + }, + "provide": { + "psr/http-factory-implementation": "1.0", + "psr/http-message-implementation": "1.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "http-interop/http-factory-tests": "0.9.0", + "phpunit/phpunit": "^8.5.44 || ^9.6.25" + }, + "suggest": { + "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Psr7\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://github.com/sagikazarmark" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://sagikazarmark.hu" + } + ], + "description": "PSR-7 message implementation that also provides common utility methods", + "keywords": [ + "http", + "message", + "psr-7", + "request", + "response", + "stream", + "uri", + "url" + ], + "support": { + "issues": "https://github.com/guzzle/psr7/issues", + "source": "https://github.com/guzzle/psr7/tree/2.8.0" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/psr7", + "type": "tidelift" + } + ], + "time": "2025-08-23T21:21:41+00:00" + }, + { + "name": "guzzlehttp/uri-template", + "version": "v1.0.5", + "source": { + "type": "git", + "url": "https://github.com/guzzle/uri-template.git", + "reference": "4f4bbd4e7172148801e76e3decc1e559bdee34e1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/uri-template/zipball/4f4bbd4e7172148801e76e3decc1e559bdee34e1", + "reference": "4f4bbd4e7172148801e76e3decc1e559bdee34e1", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0", + "symfony/polyfill-php80": "^1.24" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "phpunit/phpunit": "^8.5.44 || ^9.6.25", + "uri-template/tests": "1.0.0" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\UriTemplate\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + } + ], + "description": "A polyfill class for uri_template of PHP", + "keywords": [ + "guzzlehttp", + "uri-template" + ], + "support": { + "issues": "https://github.com/guzzle/uri-template/issues", + "source": "https://github.com/guzzle/uri-template/tree/v1.0.5" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/uri-template", + "type": "tidelift" + } + ], + "time": "2025-08-22T14:27:06+00:00" + }, + { + "name": "illuminate/bus", + "version": "v12.53.0", + "source": { + "type": "git", + "url": "https://github.com/illuminate/bus.git", + "reference": "c2ceb60c70961815f88da24ab0b7a7f32d47cf4e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/illuminate/bus/zipball/c2ceb60c70961815f88da24ab0b7a7f32d47cf4e", + "reference": "c2ceb60c70961815f88da24ab0b7a7f32d47cf4e", + "shasum": "" + }, + "require": { + "illuminate/collections": "^12.0", + "illuminate/contracts": "^12.0", + "illuminate/pipeline": "^12.0", + "illuminate/support": "^12.0", + "php": "^8.2" + }, + "suggest": { + "illuminate/queue": "Required to use closures when chaining jobs (^12.0)." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "12.x-dev" + } + }, + "autoload": { + "psr-4": { + "Illuminate\\Bus\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Illuminate Bus package.", + "homepage": "https://laravel.com", + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2026-02-23T15:43:34+00:00" + }, + { + "name": "illuminate/collections", + "version": "v12.53.0", + "source": { + "type": "git", + "url": "https://github.com/illuminate/collections.git", + "reference": "f35c084f0d9bc57895515cb4d0665797c66285fd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/illuminate/collections/zipball/f35c084f0d9bc57895515cb4d0665797c66285fd", + "reference": "f35c084f0d9bc57895515cb4d0665797c66285fd", + "shasum": "" + }, + "require": { + "illuminate/conditionable": "^12.0", + "illuminate/contracts": "^12.0", + "illuminate/macroable": "^12.0", + "php": "^8.2", + "symfony/polyfill-php83": "^1.33", + "symfony/polyfill-php84": "^1.33", + "symfony/polyfill-php85": "^1.33" + }, + "suggest": { + "illuminate/http": "Required to convert collections to API resources (^12.0).", + "symfony/var-dumper": "Required to use the dump method (^7.2)." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "12.x-dev" + } + }, + "autoload": { + "files": [ + "functions.php", + "helpers.php" + ], + "psr-4": { + "Illuminate\\Support\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Illuminate Collections package.", + "homepage": "https://laravel.com", + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2026-02-16T14:10:38+00:00" + }, + { + "name": "illuminate/conditionable", + "version": "v12.53.0", + "source": { + "type": "git", + "url": "https://github.com/illuminate/conditionable.git", + "reference": "ec677967c1f2faf90b8428919124d2184a4c9b49" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/illuminate/conditionable/zipball/ec677967c1f2faf90b8428919124d2184a4c9b49", + "reference": "ec677967c1f2faf90b8428919124d2184a4c9b49", + "shasum": "" + }, + "require": { + "php": "^8.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "12.x-dev" + } + }, + "autoload": { + "psr-4": { + "Illuminate\\Support\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Illuminate Conditionable package.", + "homepage": "https://laravel.com", + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2025-05-13T15:08:45+00:00" + }, + { + "name": "illuminate/container", + "version": "v12.53.0", + "source": { + "type": "git", + "url": "https://github.com/illuminate/container.git", + "reference": "648307e8f54bcd9450c858f99abd11bc50c364a0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/illuminate/container/zipball/648307e8f54bcd9450c858f99abd11bc50c364a0", + "reference": "648307e8f54bcd9450c858f99abd11bc50c364a0", + "shasum": "" + }, + "require": { + "illuminate/contracts": "^12.0", + "illuminate/reflection": "^12.0", + "php": "^8.2", + "psr/container": "^1.1.1|^2.0.1", + "symfony/polyfill-php84": "^1.33", + "symfony/polyfill-php85": "^1.33" + }, + "provide": { + "psr/container-implementation": "1.1|2.0" + }, + "suggest": { + "illuminate/auth": "Required to use the Auth attribute", + "illuminate/cache": "Required to use the Cache attribute", + "illuminate/config": "Required to use the Config attribute", + "illuminate/database": "Required to use the DB attribute", + "illuminate/filesystem": "Required to use the Storage attribute", + "illuminate/log": "Required to use the Log or Context attributes" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "12.x-dev" + } + }, + "autoload": { + "psr-4": { + "Illuminate\\Container\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Illuminate Container package.", + "homepage": "https://laravel.com", + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2026-02-12T16:13:27+00:00" + }, + { + "name": "illuminate/contracts", + "version": "v12.53.0", + "source": { + "type": "git", + "url": "https://github.com/illuminate/contracts.git", + "reference": "099fd9b56ccaf776facaa27699b960a3f2451127" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/illuminate/contracts/zipball/099fd9b56ccaf776facaa27699b960a3f2451127", + "reference": "099fd9b56ccaf776facaa27699b960a3f2451127", + "shasum": "" + }, + "require": { + "php": "^8.2", + "psr/container": "^1.1.1|^2.0.1", + "psr/simple-cache": "^1.0|^2.0|^3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "12.x-dev" + } + }, + "autoload": { + "psr-4": { + "Illuminate\\Contracts\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Illuminate Contracts package.", + "homepage": "https://laravel.com", + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2026-02-20T14:37:40+00:00" + }, + { + "name": "illuminate/database", + "version": "v12.53.0", + "source": { + "type": "git", + "url": "https://github.com/illuminate/database.git", + "reference": "c685208bdd8bfec3e13c7b52f6235b6f3bde9fea" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/illuminate/database/zipball/c685208bdd8bfec3e13c7b52f6235b6f3bde9fea", + "reference": "c685208bdd8bfec3e13c7b52f6235b6f3bde9fea", + "shasum": "" + }, + "require": { + "brick/math": "^0.11|^0.12|^0.13|^0.14", + "ext-pdo": "*", + "illuminate/collections": "^12.0", + "illuminate/container": "^12.0", + "illuminate/contracts": "^12.0", + "illuminate/macroable": "^12.0", + "illuminate/support": "^12.0", + "laravel/serializable-closure": "^1.3|^2.0", + "php": "^8.2", + "symfony/polyfill-php83": "^1.33", + "symfony/polyfill-php85": "^1.33" + }, + "suggest": { + "ext-filter": "Required to use the Postgres database driver.", + "fakerphp/faker": "Required to use the eloquent factory builder (^1.24).", + "illuminate/console": "Required to use the database commands (^12.0).", + "illuminate/events": "Required to use the observers with Eloquent (^12.0).", + "illuminate/filesystem": "Required to use the migrations (^12.0).", + "illuminate/http": "Required to convert Eloquent models to API resources (^12.0).", + "illuminate/pagination": "Required to paginate the result set (^12.0).", + "symfony/finder": "Required to use Eloquent model factories (^7.2)." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "12.x-dev" + } + }, + "autoload": { + "psr-4": { + "Illuminate\\Database\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Illuminate Database package.", + "homepage": "https://laravel.com", + "keywords": [ + "database", + "laravel", + "orm", + "sql" + ], + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2026-02-23T00:20:44+00:00" + }, + { + "name": "illuminate/events", + "version": "v12.53.0", + "source": { + "type": "git", + "url": "https://github.com/illuminate/events.git", + "reference": "b71e42451496175f8fd898cb6a67ad7fd613d00b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/illuminate/events/zipball/b71e42451496175f8fd898cb6a67ad7fd613d00b", + "reference": "b71e42451496175f8fd898cb6a67ad7fd613d00b", + "shasum": "" + }, + "require": { + "illuminate/bus": "^12.0", + "illuminate/collections": "^12.0", + "illuminate/container": "^12.0", + "illuminate/contracts": "^12.0", + "illuminate/macroable": "^12.0", + "illuminate/support": "^12.0", + "php": "^8.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "12.x-dev" + } + }, + "autoload": { + "files": [ + "functions.php" + ], + "psr-4": { + "Illuminate\\Events\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Illuminate Events package.", + "homepage": "https://laravel.com", + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2026-02-23T15:43:34+00:00" + }, + { + "name": "illuminate/filesystem", + "version": "v12.53.0", + "source": { + "type": "git", + "url": "https://github.com/illuminate/filesystem.git", + "reference": "c4c3f8612f218afcf09f3c7f5c7dc9e282626800" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/illuminate/filesystem/zipball/c4c3f8612f218afcf09f3c7f5c7dc9e282626800", + "reference": "c4c3f8612f218afcf09f3c7f5c7dc9e282626800", + "shasum": "" + }, + "require": { + "illuminate/collections": "^12.0", + "illuminate/contracts": "^12.0", + "illuminate/macroable": "^12.0", + "illuminate/support": "^12.0", + "php": "^8.2", + "symfony/finder": "^7.2.0" + }, + "suggest": { + "ext-fileinfo": "Required to use the Filesystem class.", + "ext-ftp": "Required to use the Flysystem FTP driver.", + "ext-hash": "Required to use the Filesystem class.", + "illuminate/http": "Required for handling uploaded files (^12.0).", + "league/flysystem": "Required to use the Flysystem local driver (^3.25.1).", + "league/flysystem-aws-s3-v3": "Required to use the Flysystem S3 driver (^3.25.1).", + "league/flysystem-ftp": "Required to use the Flysystem FTP driver (^3.25.1).", + "league/flysystem-sftp-v3": "Required to use the Flysystem SFTP driver (^3.25.1).", + "psr/http-message": "Required to allow Storage::put to accept a StreamInterface (^1.0).", + "symfony/filesystem": "Required to enable support for relative symbolic links (^7.2).", + "symfony/mime": "Required to enable support for guessing extensions (^7.2)." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "12.x-dev" + } + }, + "autoload": { + "files": [ + "functions.php" + ], + "psr-4": { + "Illuminate\\Filesystem\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Illuminate Filesystem package.", + "homepage": "https://laravel.com", + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2026-02-13T20:26:32+00:00" + }, + { + "name": "illuminate/http", + "version": "v12.53.0", + "source": { + "type": "git", + "url": "https://github.com/illuminate/http.git", + "reference": "b6351a4b8d3b6b1aef4b08cb98892045b20aa0ad" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/illuminate/http/zipball/b6351a4b8d3b6b1aef4b08cb98892045b20aa0ad", + "reference": "b6351a4b8d3b6b1aef4b08cb98892045b20aa0ad", + "shasum": "" + }, + "require": { + "ext-filter": "*", + "fruitcake/php-cors": "^1.3", + "guzzlehttp/guzzle": "^7.8.2", + "guzzlehttp/uri-template": "^1.0", + "illuminate/collections": "^12.0", + "illuminate/macroable": "^12.0", + "illuminate/session": "^12.0", + "illuminate/support": "^12.0", + "php": "^8.2", + "symfony/http-foundation": "^7.2.0", + "symfony/http-kernel": "^7.2.0", + "symfony/mime": "^7.2.0", + "symfony/polyfill-php83": "^1.33", + "symfony/polyfill-php85": "^1.33" + }, + "suggest": { + "ext-gd": "Required to use Illuminate\\Http\\Testing\\FileFactory::image()." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "12.x-dev" + } + }, + "autoload": { + "psr-4": { + "Illuminate\\Http\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Illuminate Http package.", + "homepage": "https://laravel.com", + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2026-02-23T15:41:33+00:00" + }, + { + "name": "illuminate/macroable", + "version": "v12.53.0", + "source": { + "type": "git", + "url": "https://github.com/illuminate/macroable.git", + "reference": "e862e5648ee34004fa56046b746f490dfa86c613" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/illuminate/macroable/zipball/e862e5648ee34004fa56046b746f490dfa86c613", + "reference": "e862e5648ee34004fa56046b746f490dfa86c613", + "shasum": "" + }, + "require": { + "php": "^8.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "12.x-dev" + } + }, + "autoload": { + "psr-4": { + "Illuminate\\Support\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Illuminate Macroable package.", + "homepage": "https://laravel.com", + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2024-07-23T16:31:01+00:00" + }, + { + "name": "illuminate/pagination", + "version": "v12.53.0", + "source": { + "type": "git", + "url": "https://github.com/illuminate/pagination.git", + "reference": "87e7e3e7b02d6809b1bcd41782e1ca2c6d2a413b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/illuminate/pagination/zipball/87e7e3e7b02d6809b1bcd41782e1ca2c6d2a413b", + "reference": "87e7e3e7b02d6809b1bcd41782e1ca2c6d2a413b", + "shasum": "" + }, + "require": { + "ext-filter": "*", + "illuminate/collections": "^12.0", + "illuminate/contracts": "^12.0", + "illuminate/support": "^12.0", + "php": "^8.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "12.x-dev" + } + }, + "autoload": { + "psr-4": { + "Illuminate\\Pagination\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Illuminate Pagination package.", + "homepage": "https://laravel.com", + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2025-11-16T14:36:17+00:00" + }, + { + "name": "illuminate/pipeline", + "version": "v12.53.0", + "source": { + "type": "git", + "url": "https://github.com/illuminate/pipeline.git", + "reference": "b6a14c20d69a44bf0a6fba664a00d23ca71770ee" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/illuminate/pipeline/zipball/b6a14c20d69a44bf0a6fba664a00d23ca71770ee", + "reference": "b6a14c20d69a44bf0a6fba664a00d23ca71770ee", + "shasum": "" + }, + "require": { + "illuminate/contracts": "^12.0", + "illuminate/macroable": "^12.0", + "illuminate/support": "^12.0", + "php": "^8.2" + }, + "suggest": { + "illuminate/database": "Required to use database transactions (^12.0)." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "12.x-dev" + } + }, + "autoload": { + "psr-4": { + "Illuminate\\Pipeline\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Illuminate Pipeline package.", + "homepage": "https://laravel.com", + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2025-08-20T13:36:50+00:00" + }, + { + "name": "illuminate/reflection", + "version": "v12.53.0", + "source": { + "type": "git", + "url": "https://github.com/illuminate/reflection.git", + "reference": "6188e97a587371b9951c2a7e337cd760308c17d7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/illuminate/reflection/zipball/6188e97a587371b9951c2a7e337cd760308c17d7", + "reference": "6188e97a587371b9951c2a7e337cd760308c17d7", + "shasum": "" + }, + "require": { + "illuminate/collections": "^12.0", + "illuminate/contracts": "^12.0", + "php": "^8.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "12.x-dev" + } + }, + "autoload": { + "files": [ + "helpers.php" + ], + "psr-4": { + "Illuminate\\Support\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Illuminate Reflection package.", + "homepage": "https://laravel.com", + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2026-02-04T15:21:22+00:00" + }, + { + "name": "illuminate/session", + "version": "v12.53.0", + "source": { + "type": "git", + "url": "https://github.com/illuminate/session.git", + "reference": "98802e67dd5e059c0b978b3fe8f5f0a3ac17ec4e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/illuminate/session/zipball/98802e67dd5e059c0b978b3fe8f5f0a3ac17ec4e", + "reference": "98802e67dd5e059c0b978b3fe8f5f0a3ac17ec4e", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-session": "*", + "illuminate/collections": "^12.0", + "illuminate/contracts": "^12.0", + "illuminate/filesystem": "^12.0", + "illuminate/support": "^12.0", + "php": "^8.2", + "symfony/finder": "^7.2.0", + "symfony/http-foundation": "^7.2.0" + }, + "suggest": { + "illuminate/console": "Required to use the session:table command (^12.0)." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "12.x-dev" + } + }, + "autoload": { + "psr-4": { + "Illuminate\\Session\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Illuminate Session package.", + "homepage": "https://laravel.com", + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2026-02-14T23:03:41+00:00" + }, + { + "name": "illuminate/support", + "version": "v12.53.0", + "source": { + "type": "git", + "url": "https://github.com/illuminate/support.git", + "reference": "18d7d75366ddb9eded3b7f05173f791da47faf34" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/illuminate/support/zipball/18d7d75366ddb9eded3b7f05173f791da47faf34", + "reference": "18d7d75366ddb9eded3b7f05173f791da47faf34", + "shasum": "" + }, + "require": { + "doctrine/inflector": "^2.0", + "ext-ctype": "*", + "ext-filter": "*", + "ext-mbstring": "*", + "illuminate/collections": "^12.0", + "illuminate/conditionable": "^12.0", + "illuminate/contracts": "^12.0", + "illuminate/macroable": "^12.0", + "illuminate/reflection": "^12.0", + "nesbot/carbon": "^3.8.4", + "php": "^8.2", + "symfony/polyfill-php83": "^1.33", + "symfony/polyfill-php85": "^1.33", + "voku/portable-ascii": "^2.0.2" + }, + "conflict": { + "tightenco/collect": "<5.5.33" + }, + "replace": { + "spatie/once": "*" + }, + "suggest": { + "illuminate/filesystem": "Required to use the Composer class (^12.0).", + "laravel/serializable-closure": "Required to use the once function (^1.3|^2.0).", + "league/commonmark": "Required to use Str::markdown() and Stringable::markdown() (^2.7).", + "league/uri": "Required to use the Uri class (^7.5.1).", + "ramsey/uuid": "Required to use Str::uuid() (^4.7).", + "symfony/process": "Required to use the Composer class (^7.2).", + "symfony/uid": "Required to use Str::ulid() (^7.2).", + "symfony/var-dumper": "Required to use the dd function (^7.2).", + "vlucas/phpdotenv": "Required to use the Env class and env helper (^5.6.1)." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "12.x-dev" + } + }, + "autoload": { + "files": [ + "functions.php", + "helpers.php" + ], + "psr-4": { + "Illuminate\\Support\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Illuminate Support package.", + "homepage": "https://laravel.com", + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2026-02-23T15:44:06+00:00" + }, + { + "name": "laravel/serializable-closure", + "version": "v2.0.10", + "source": { + "type": "git", + "url": "https://github.com/laravel/serializable-closure.git", + "reference": "870fc81d2f879903dfc5b60bf8a0f94a1609e669" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/870fc81d2f879903dfc5b60bf8a0f94a1609e669", + "reference": "870fc81d2f879903dfc5b60bf8a0f94a1609e669", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "illuminate/support": "^10.0|^11.0|^12.0|^13.0", + "nesbot/carbon": "^2.67|^3.0", + "pestphp/pest": "^2.36|^3.0|^4.0", + "phpstan/phpstan": "^2.0", + "symfony/var-dumper": "^6.2.0|^7.0.0|^8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "Laravel\\SerializableClosure\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + }, + { + "name": "Nuno Maduro", + "email": "nuno@laravel.com" + } + ], + "description": "Laravel Serializable Closure provides an easy and secure way to serialize closures in PHP.", + "keywords": [ + "closure", + "laravel", + "serializable" + ], + "support": { + "issues": "https://github.com/laravel/serializable-closure/issues", + "source": "https://github.com/laravel/serializable-closure" + }, + "time": "2026-02-20T19:59:49+00:00" + }, + { + "name": "monolog/monolog", + "version": "2.11.0", + "source": { + "type": "git", + "url": "https://github.com/Seldaek/monolog.git", + "reference": "37308608e599f34a1a4845b16440047ec98a172a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Seldaek/monolog/zipball/37308608e599f34a1a4845b16440047ec98a172a", + "reference": "37308608e599f34a1a4845b16440047ec98a172a", + "shasum": "" + }, + "require": { + "php": ">=7.2", + "psr/log": "^1.0.1 || ^2.0 || ^3.0" + }, + "provide": { + "psr/log-implementation": "1.0.0 || 2.0.0 || 3.0.0" + }, + "require-dev": { + "aws/aws-sdk-php": "^2.4.9 || ^3.0", + "doctrine/couchdb": "~1.0@dev", + "elasticsearch/elasticsearch": "^7 || ^8", + "ext-json": "*", + "graylog2/gelf-php": "^1.4.2 || ^2@dev", + "guzzlehttp/guzzle": "^7.4", + "guzzlehttp/psr7": "^2.2", + "mongodb/mongodb": "^1.8 || ^2.0", + "php-amqplib/php-amqplib": "~2.4 || ^3", + "phpspec/prophecy": "^1.15", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^8.5.38 || ^9.6.19", + "predis/predis": "^1.1 || ^2.0", + "rollbar/rollbar": "^1.3 || ^2 || ^3", + "ruflin/elastica": "^7", + "swiftmailer/swiftmailer": "^5.3|^6.0", + "symfony/mailer": "^5.4 || ^6", + "symfony/mime": "^5.4 || ^6" + }, + "suggest": { + "aws/aws-sdk-php": "Allow sending log messages to AWS services like DynamoDB", + "doctrine/couchdb": "Allow sending log messages to a CouchDB server", + "elasticsearch/elasticsearch": "Allow sending log messages to an Elasticsearch server via official client", + "ext-amqp": "Allow sending log messages to an AMQP server (1.0+ required)", + "ext-curl": "Required to send log messages using the IFTTTHandler, the LogglyHandler, the SendGridHandler, the SlackWebhookHandler or the TelegramBotHandler", + "ext-mbstring": "Allow to work properly with unicode symbols", + "ext-mongodb": "Allow sending log messages to a MongoDB server (via driver)", + "ext-openssl": "Required to send log messages using SSL", + "ext-sockets": "Allow sending log messages to a Syslog server (via UDP driver)", + "graylog2/gelf-php": "Allow sending log messages to a GrayLog2 server", + "mongodb/mongodb": "Allow sending log messages to a MongoDB server (via library)", + "php-amqplib/php-amqplib": "Allow sending log messages to an AMQP server using php-amqplib", + "rollbar/rollbar": "Allow sending log messages to Rollbar", + "ruflin/elastica": "Allow sending log messages to an Elastic Search server" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "Monolog\\": "src/Monolog" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "https://seld.be" + } + ], + "description": "Sends your logs to files, sockets, inboxes, databases and various web services", + "homepage": "https://github.com/Seldaek/monolog", + "keywords": [ + "log", + "logging", + "psr-3" + ], + "support": { + "issues": "https://github.com/Seldaek/monolog/issues", + "source": "https://github.com/Seldaek/monolog/tree/2.11.0" + }, + "funding": [ + { + "url": "https://github.com/Seldaek", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/monolog/monolog", + "type": "tidelift" + } + ], + "time": "2026-01-01T13:05:00+00:00" + }, + { + "name": "nesbot/carbon", + "version": "3.11.1", + "source": { + "type": "git", + "url": "https://github.com/CarbonPHP/carbon.git", + "reference": "f438fcc98f92babee98381d399c65336f3a3827f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/f438fcc98f92babee98381d399c65336f3a3827f", + "reference": "f438fcc98f92babee98381d399c65336f3a3827f", + "shasum": "" + }, + "require": { + "carbonphp/carbon-doctrine-types": "<100.0", + "ext-json": "*", + "php": "^8.1", + "psr/clock": "^1.0", + "symfony/clock": "^6.3.12 || ^7.0 || ^8.0", + "symfony/polyfill-mbstring": "^1.0", + "symfony/translation": "^4.4.18 || ^5.2.1 || ^6.0 || ^7.0 || ^8.0" + }, + "provide": { + "psr/clock-implementation": "1.0" + }, + "require-dev": { + "doctrine/dbal": "^3.6.3 || ^4.0", + "doctrine/orm": "^2.15.2 || ^3.0", + "friendsofphp/php-cs-fixer": "^v3.87.1", + "kylekatarnls/multi-tester": "^2.5.3", + "phpmd/phpmd": "^2.15.0", + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan": "^2.1.22", + "phpunit/phpunit": "^10.5.53", + "squizlabs/php_codesniffer": "^3.13.4 || ^4.0.0" + }, + "bin": [ + "bin/carbon" + ], + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Carbon\\Laravel\\ServiceProvider" + ] + }, + "phpstan": { + "includes": [ + "extension.neon" + ] + }, + "branch-alias": { + "dev-2.x": "2.x-dev", + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Carbon\\": "src/Carbon/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Brian Nesbitt", + "email": "brian@nesbot.com", + "homepage": "https://markido.com" + }, + { + "name": "kylekatarnls", + "homepage": "https://github.com/kylekatarnls" + } + ], + "description": "An API extension for DateTime that supports 281 different languages.", + "homepage": "https://carbonphp.github.io/carbon/", + "keywords": [ + "date", + "datetime", + "time" + ], + "support": { + "docs": "https://carbonphp.github.io/carbon/guide/getting-started/introduction.html", + "issues": "https://github.com/CarbonPHP/carbon/issues", + "source": "https://github.com/CarbonPHP/carbon" + }, + "funding": [ + { + "url": "https://github.com/sponsors/kylekatarnls", + "type": "github" + }, + { + "url": "https://opencollective.com/Carbon#sponsor", + "type": "opencollective" + }, + { + "url": "https://tidelift.com/subscription/pkg/packagist-nesbot-carbon?utm_source=packagist-nesbot-carbon&utm_medium=referral&utm_campaign=readme", + "type": "tidelift" + } + ], + "time": "2026-01-29T09:26:29+00:00" + }, + { + "name": "nikic/fast-route", + "version": "v1.3.0", + "source": { + "type": "git", + "url": "https://github.com/nikic/FastRoute.git", + "reference": "181d480e08d9476e61381e04a71b34dc0432e812" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/FastRoute/zipball/181d480e08d9476e61381e04a71b34dc0432e812", + "reference": "181d480e08d9476e61381e04a71b34dc0432e812", + "shasum": "" + }, + "require": { + "php": ">=5.4.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.35|~5.7" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "FastRoute\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nikita Popov", + "email": "nikic@php.net" + } + ], + "description": "Fast request router for PHP", + "keywords": [ + "router", + "routing" + ], + "support": { + "issues": "https://github.com/nikic/FastRoute/issues", + "source": "https://github.com/nikic/FastRoute/tree/master" + }, + "time": "2018-02-13T20:26:39+00:00" + }, + { + "name": "phpoption/phpoption", + "version": "1.9.5", + "source": { + "type": "git", + "url": "https://github.com/schmittjoh/php-option.git", + "reference": "75365b91986c2405cf5e1e012c5595cd487a98be" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/schmittjoh/php-option/zipball/75365b91986c2405cf5e1e012c5595cd487a98be", + "reference": "75365b91986c2405cf5e1e012c5595cd487a98be", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "phpunit/phpunit": "^8.5.44 || ^9.6.25 || ^10.5.53 || ^11.5.34" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + }, + "branch-alias": { + "dev-master": "1.9-dev" + } + }, + "autoload": { + "psr-4": { + "PhpOption\\": "src/PhpOption/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Johannes M. Schmitt", + "email": "schmittjoh@gmail.com", + "homepage": "https://github.com/schmittjoh" + }, + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + } + ], + "description": "Option Type for PHP", + "keywords": [ + "language", + "option", + "php", + "type" + ], + "support": { + "issues": "https://github.com/schmittjoh/php-option/issues", + "source": "https://github.com/schmittjoh/php-option/tree/1.9.5" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpoption/phpoption", + "type": "tidelift" + } + ], + "time": "2025-12-27T19:41:33+00:00" + }, + { + "name": "psr/clock", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/clock.git", + "reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/clock/zipball/e41a24703d4560fd0acb709162f73b8adfc3aa0d", + "reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d", + "shasum": "" + }, + "require": { + "php": "^7.0 || ^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Psr\\Clock\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for reading the clock.", + "homepage": "https://github.com/php-fig/clock", + "keywords": [ + "clock", + "now", + "psr", + "psr-20", + "time" + ], + "support": { + "issues": "https://github.com/php-fig/clock/issues", + "source": "https://github.com/php-fig/clock/tree/1.0.0" + }, + "time": "2022-11-25T14:36:26+00:00" + }, + { + "name": "psr/container", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/container.git", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "shasum": "" + }, + "require": { + "php": ">=7.4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Container\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common Container Interface (PHP FIG PSR-11)", + "homepage": "https://github.com/php-fig/container", + "keywords": [ + "PSR-11", + "container", + "container-interface", + "container-interop", + "psr" + ], + "support": { + "issues": "https://github.com/php-fig/container/issues", + "source": "https://github.com/php-fig/container/tree/2.0.2" + }, + "time": "2021-11-05T16:47:00+00:00" + }, + { + "name": "psr/event-dispatcher", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/event-dispatcher.git", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/event-dispatcher/zipball/dbefd12671e8a14ec7f180cab83036ed26714bb0", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0", + "shasum": "" + }, + "require": { + "php": ">=7.2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\EventDispatcher\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Standard interfaces for event handling.", + "keywords": [ + "events", + "psr", + "psr-14" + ], + "support": { + "issues": "https://github.com/php-fig/event-dispatcher/issues", + "source": "https://github.com/php-fig/event-dispatcher/tree/1.0.0" + }, + "time": "2019-01-08T18:20:26+00:00" + }, + { + "name": "psr/http-client", + "version": "1.0.3", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-client.git", + "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-client/zipball/bb5906edc1c324c9a05aa0873d40117941e5fa90", + "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90", + "shasum": "" + }, + "require": { + "php": "^7.0 || ^8.0", + "psr/http-message": "^1.0 || ^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Client\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP clients", + "homepage": "https://github.com/php-fig/http-client", + "keywords": [ + "http", + "http-client", + "psr", + "psr-18" + ], + "support": { + "source": "https://github.com/php-fig/http-client" + }, + "time": "2023-09-23T14:17:50+00:00" + }, + { + "name": "psr/http-factory", + "version": "1.1.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-factory.git", + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-factory/zipball/2b4765fddfe3b508ac62f829e852b1501d3f6e8a", + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a", + "shasum": "" + }, + "require": { + "php": ">=7.1", + "psr/http-message": "^1.0 || ^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "PSR-17: Common interfaces for PSR-7 HTTP message factories", + "keywords": [ + "factory", + "http", + "message", + "psr", + "psr-17", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-factory" + }, + "time": "2024-04-15T12:06:14+00:00" + }, + { + "name": "psr/http-message", + "version": "2.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-message.git", + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP messages", + "homepage": "https://github.com/php-fig/http-message", + "keywords": [ + "http", + "http-message", + "psr", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-message/tree/2.0" + }, + "time": "2023-04-04T09:54:51+00:00" + }, + { + "name": "psr/log", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Log\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", + "keywords": [ + "log", + "psr", + "psr-3" + ], + "support": { + "source": "https://github.com/php-fig/log/tree/3.0.2" + }, + "time": "2024-09-11T13:17:53+00:00" + }, + { + "name": "psr/simple-cache", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/simple-cache.git", + "reference": "764e0b3939f5ca87cb904f570ef9be2d78a07865" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/simple-cache/zipball/764e0b3939f5ca87cb904f570ef9be2d78a07865", + "reference": "764e0b3939f5ca87cb904f570ef9be2d78a07865", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\SimpleCache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interfaces for simple caching", + "keywords": [ + "cache", + "caching", + "psr", + "psr-16", + "simple-cache" + ], + "support": { + "source": "https://github.com/php-fig/simple-cache/tree/3.0.0" + }, + "time": "2021-10-29T13:26:27+00:00" + }, + { + "name": "ralouphie/getallheaders", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/ralouphie/getallheaders.git", + "reference": "120b605dfeb996808c31b6477290a714d356e822" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822", + "reference": "120b605dfeb996808c31b6477290a714d356e822", + "shasum": "" + }, + "require": { + "php": ">=5.6" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.1", + "phpunit/phpunit": "^5 || ^6.5" + }, + "type": "library", + "autoload": { + "files": [ + "src/getallheaders.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ralph Khattar", + "email": "ralph.khattar@gmail.com" + } + ], + "description": "A polyfill for getallheaders.", + "support": { + "issues": "https://github.com/ralouphie/getallheaders/issues", + "source": "https://github.com/ralouphie/getallheaders/tree/develop" + }, + "time": "2019-03-08T08:55:37+00:00" + }, + { + "name": "symfony/clock", + "version": "v8.0.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/clock.git", + "reference": "832119f9b8dbc6c8e6f65f30c5969eca1e88764f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/clock/zipball/832119f9b8dbc6c8e6f65f30c5969eca1e88764f", + "reference": "832119f9b8dbc6c8e6f65f30c5969eca1e88764f", + "shasum": "" + }, + "require": { + "php": ">=8.4", + "psr/clock": "^1.0" + }, + "provide": { + "psr/clock-implementation": "1.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/now.php" + ], + "psr-4": { + "Symfony\\Component\\Clock\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Decouples applications from the system clock", + "homepage": "https://symfony.com", + "keywords": [ + "clock", + "psr20", + "time" + ], + "support": { + "source": "https://github.com/symfony/clock/tree/v8.0.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-11-12T15:46:48+00:00" + }, + { + "name": "symfony/console", + "version": "v7.4.6", + "source": { + "type": "git", + "url": "https://github.com/symfony/console.git", + "reference": "6d643a93b47398599124022eb24d97c153c12f27" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/console/zipball/6d643a93b47398599124022eb24d97c153c12f27", + "reference": "6d643a93b47398599124022eb24d97c153c12f27", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "~1.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/string": "^7.2|^8.0" + }, + "conflict": { + "symfony/dependency-injection": "<6.4", + "symfony/dotenv": "<6.4", + "symfony/event-dispatcher": "<6.4", + "symfony/lock": "<6.4", + "symfony/process": "<6.4" + }, + "provide": { + "psr/log-implementation": "1.0|2.0|3.0" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/event-dispatcher": "^6.4|^7.0|^8.0", + "symfony/http-foundation": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/lock": "^6.4|^7.0|^8.0", + "symfony/messenger": "^6.4|^7.0|^8.0", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/stopwatch": "^6.4|^7.0|^8.0", + "symfony/var-dumper": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Console\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Eases the creation of beautiful and testable command line interfaces", + "homepage": "https://symfony.com", + "keywords": [ + "cli", + "command-line", + "console", + "terminal" + ], + "support": { + "source": "https://github.com/symfony/console/tree/v7.4.6" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-02-25T17:02:47+00:00" + }, + { + "name": "symfony/deprecation-contracts", + "version": "v3.6.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/63afe740e99a13ba87ec199bb07bbdee937a5b62", + "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" + } + }, + "autoload": { + "files": [ + "function.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "A generic function and convention to trigger deprecation notices", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.6.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:21:43+00:00" + }, + { + "name": "symfony/error-handler", + "version": "v8.0.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/error-handler.git", + "reference": "7620b97ec0ab1d2d6c7fb737aa55da411bea776a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/error-handler/zipball/7620b97ec0ab1d2d6c7fb737aa55da411bea776a", + "reference": "7620b97ec0ab1d2d6c7fb737aa55da411bea776a", + "shasum": "" + }, + "require": { + "php": ">=8.4", + "psr/log": "^1|^2|^3", + "symfony/polyfill-php85": "^1.32", + "symfony/var-dumper": "^7.4|^8.0" + }, + "conflict": { + "symfony/deprecation-contracts": "<2.5" + }, + "require-dev": { + "symfony/console": "^7.4|^8.0", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/http-kernel": "^7.4|^8.0", + "symfony/serializer": "^7.4|^8.0", + "symfony/webpack-encore-bundle": "^1.0|^2.0" + }, + "bin": [ + "Resources/bin/patch-type-declarations" + ], + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\ErrorHandler\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides tools to manage errors and ease debugging PHP code", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/error-handler/tree/v8.0.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-01-23T11:07:10+00:00" + }, + { + "name": "symfony/event-dispatcher", + "version": "v8.0.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher.git", + "reference": "99301401da182b6cfaa4700dbe9987bb75474b47" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/99301401da182b6cfaa4700dbe9987bb75474b47", + "reference": "99301401da182b6cfaa4700dbe9987bb75474b47", + "shasum": "" + }, + "require": { + "php": ">=8.4", + "symfony/event-dispatcher-contracts": "^2.5|^3" + }, + "conflict": { + "symfony/security-http": "<7.4", + "symfony/service-contracts": "<2.5" + }, + "provide": { + "psr/event-dispatcher-implementation": "1.0", + "symfony/event-dispatcher-implementation": "2.0|3.0" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^7.4|^8.0", + "symfony/dependency-injection": "^7.4|^8.0", + "symfony/error-handler": "^7.4|^8.0", + "symfony/expression-language": "^7.4|^8.0", + "symfony/framework-bundle": "^7.4|^8.0", + "symfony/http-foundation": "^7.4|^8.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/stopwatch": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\EventDispatcher\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/event-dispatcher/tree/v8.0.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-01-05T11:45:55+00:00" + }, + { + "name": "symfony/event-dispatcher-contracts", + "version": "v3.6.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher-contracts.git", + "reference": "59eb412e93815df44f05f342958efa9f46b1e586" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/59eb412e93815df44f05f342958efa9f46b1e586", + "reference": "59eb412e93815df44f05f342958efa9f46b1e586", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/event-dispatcher": "^1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\EventDispatcher\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to dispatching event", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.6.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:21:43+00:00" + }, + { + "name": "symfony/finder", + "version": "v7.4.6", + "source": { + "type": "git", + "url": "https://github.com/symfony/finder.git", + "reference": "8655bf1076b7a3a346cb11413ffdabff50c7ffcf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/finder/zipball/8655bf1076b7a3a346cb11413ffdabff50c7ffcf", + "reference": "8655bf1076b7a3a346cb11413ffdabff50c7ffcf", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "symfony/filesystem": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Finder\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Finds files and directories via an intuitive fluent interface", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/finder/tree/v7.4.6" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-01-29T09:40:50+00:00" + }, + { + "name": "symfony/http-foundation", + "version": "v7.4.6", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-foundation.git", + "reference": "fd97d5e926e988a363cef56fbbf88c5c528e9065" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/fd97d5e926e988a363cef56fbbf88c5c528e9065", + "reference": "fd97d5e926e988a363cef56fbbf88c5c528e9065", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "^1.1" + }, + "conflict": { + "doctrine/dbal": "<3.6", + "symfony/cache": "<6.4.12|>=7.0,<7.1.5" + }, + "require-dev": { + "doctrine/dbal": "^3.6|^4", + "predis/predis": "^1.1|^2.0", + "symfony/cache": "^6.4.12|^7.1.5|^8.0", + "symfony/clock": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/mime": "^6.4|^7.0|^8.0", + "symfony/rate-limiter": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\HttpFoundation\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Defines an object-oriented layer for the HTTP specification", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/http-foundation/tree/v7.4.6" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-02-21T16:25:55+00:00" + }, + { + "name": "symfony/http-kernel", + "version": "v7.4.6", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-kernel.git", + "reference": "002ac0cf4cd972a7fd0912dcd513a95e8a81ce83" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/002ac0cf4cd972a7fd0912dcd513a95e8a81ce83", + "reference": "002ac0cf4cd972a7fd0912dcd513a95e8a81ce83", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "psr/log": "^1|^2|^3", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/error-handler": "^6.4|^7.0|^8.0", + "symfony/event-dispatcher": "^7.3|^8.0", + "symfony/http-foundation": "^7.4|^8.0", + "symfony/polyfill-ctype": "^1.8" + }, + "conflict": { + "symfony/browser-kit": "<6.4", + "symfony/cache": "<6.4", + "symfony/config": "<6.4", + "symfony/console": "<6.4", + "symfony/dependency-injection": "<6.4", + "symfony/doctrine-bridge": "<6.4", + "symfony/flex": "<2.10", + "symfony/form": "<6.4", + "symfony/http-client": "<6.4", + "symfony/http-client-contracts": "<2.5", + "symfony/mailer": "<6.4", + "symfony/messenger": "<6.4", + "symfony/translation": "<6.4", + "symfony/translation-contracts": "<2.5", + "symfony/twig-bridge": "<6.4", + "symfony/validator": "<6.4", + "symfony/var-dumper": "<6.4", + "twig/twig": "<3.12" + }, + "provide": { + "psr/log-implementation": "1.0|2.0|3.0" + }, + "require-dev": { + "psr/cache": "^1.0|^2.0|^3.0", + "symfony/browser-kit": "^6.4|^7.0|^8.0", + "symfony/clock": "^6.4|^7.0|^8.0", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/css-selector": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4.1|^7.0.1|^8.0", + "symfony/dom-crawler": "^6.4|^7.0|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/finder": "^6.4|^7.0|^8.0", + "symfony/http-client-contracts": "^2.5|^3", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/property-access": "^7.1|^8.0", + "symfony/routing": "^6.4|^7.0|^8.0", + "symfony/serializer": "^7.1|^8.0", + "symfony/stopwatch": "^6.4|^7.0|^8.0", + "symfony/translation": "^6.4|^7.0|^8.0", + "symfony/translation-contracts": "^2.5|^3", + "symfony/uid": "^6.4|^7.0|^8.0", + "symfony/validator": "^6.4|^7.0|^8.0", + "symfony/var-dumper": "^6.4|^7.0|^8.0", + "symfony/var-exporter": "^6.4|^7.0|^8.0", + "twig/twig": "^3.12" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\HttpKernel\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides a structured process for converting a Request into a Response", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/http-kernel/tree/v7.4.6" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-02-26T08:30:57+00:00" + }, + { + "name": "symfony/mime", + "version": "v7.4.6", + "source": { + "type": "git", + "url": "https://github.com/symfony/mime.git", + "reference": "9fc881d95feae4c6c48678cb6372bd8a7ba04f5f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/mime/zipball/9fc881d95feae4c6c48678cb6372bd8a7ba04f5f", + "reference": "9fc881d95feae4c6c48678cb6372bd8a7ba04f5f", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-intl-idn": "^1.10", + "symfony/polyfill-mbstring": "^1.0" + }, + "conflict": { + "egulias/email-validator": "~3.0.0", + "phpdocumentor/reflection-docblock": "<5.2|>=7", + "phpdocumentor/type-resolver": "<1.5.1", + "symfony/mailer": "<6.4", + "symfony/serializer": "<6.4.3|>7.0,<7.0.3" + }, + "require-dev": { + "egulias/email-validator": "^2.1.10|^3.1|^4", + "league/html-to-markdown": "^5.0", + "phpdocumentor/reflection-docblock": "^5.2|^6.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/property-access": "^6.4|^7.0|^8.0", + "symfony/property-info": "^6.4|^7.0|^8.0", + "symfony/serializer": "^6.4.3|^7.0.3|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Mime\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Allows manipulating MIME messages", + "homepage": "https://symfony.com", + "keywords": [ + "mime", + "mime-type" + ], + "support": { + "source": "https://github.com/symfony/mime/tree/v7.4.6" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-02-05T15:57:06+00:00" + }, + { + "name": "symfony/polyfill-ctype", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/a3cc8b044a6ea513310cbd48ef7333b384945638", + "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "provide": { + "ext-ctype": "*" + }, + "suggest": { + "ext-ctype": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for ctype functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "ctype", + "polyfill", + "portable" + ], + "support": { + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/polyfill-intl-grapheme", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-grapheme.git", + "reference": "380872130d3a5dd3ace2f4010d95125fde5d5c70" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/380872130d3a5dd3ace2f4010d95125fde5d5c70", + "reference": "380872130d3a5dd3ace2f4010d95125fde5d5c70", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Grapheme\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's grapheme_* functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "grapheme", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-06-27T09:58:17+00:00" + }, + { + "name": "symfony/polyfill-intl-idn", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-idn.git", + "reference": "9614ac4d8061dc257ecc64cba1b140873dce8ad3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/9614ac4d8061dc257ecc64cba1b140873dce8ad3", + "reference": "9614ac4d8061dc257ecc64cba1b140873dce8ad3", + "shasum": "" + }, + "require": { + "php": ">=7.2", + "symfony/polyfill-intl-normalizer": "^1.10" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Idn\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Laurent Bassin", + "email": "laurent@bassin.info" + }, + { + "name": "Trevor Rowbotham", + "email": "trevor.rowbotham@pm.me" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's idn_to_ascii and idn_to_utf8 functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "idn", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-10T14:38:51+00:00" + }, + { + "name": "symfony/polyfill-intl-normalizer", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-normalizer.git", + "reference": "3833d7255cc303546435cb650316bff708a1c75c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/3833d7255cc303546435cb650316bff708a1c75c", + "reference": "3833d7255cc303546435cb650316bff708a1c75c", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Normalizer\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's Normalizer class and related functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "intl", + "normalizer", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6d857f4d76bd4b343eac26d6b539585d2bc56493", + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493", + "shasum": "" + }, + "require": { + "ext-iconv": "*", + "php": ">=7.2" + }, + "provide": { + "ext-mbstring": "*" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-12-23T08:48:59+00:00" + }, + { + "name": "symfony/polyfill-php80", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php80.git", + "reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/0cc9dd0f17f61d8131e7df6b84bd344899fe2608", + "reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php80\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ion Bazan", + "email": "ion.bazan@gmail.com" + }, + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php80/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-01-02T08:10:11+00:00" + }, + { + "name": "symfony/polyfill-php83", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php83.git", + "reference": "17f6f9a6b1735c0f163024d959f700cfbc5155e5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/17f6f9a6b1735c0f163024d959f700cfbc5155e5", + "reference": "17f6f9a6b1735c0f163024d959f700cfbc5155e5", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php83\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.3+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php83/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-07-08T02:45:35+00:00" + }, + { + "name": "symfony/polyfill-php84", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php84.git", + "reference": "d8ced4d875142b6a7426000426b8abc631d6b191" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php84/zipball/d8ced4d875142b6a7426000426b8abc631d6b191", + "reference": "d8ced4d875142b6a7426000426b8abc631d6b191", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php84\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.4+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php84/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-06-24T13:30:11+00:00" + }, + { + "name": "symfony/polyfill-php85", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php85.git", + "reference": "d4e5fcd4ab3d998ab16c0db48e6cbb9a01993f91" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php85/zipball/d4e5fcd4ab3d998ab16c0db48e6cbb9a01993f91", + "reference": "d4e5fcd4ab3d998ab16c0db48e6cbb9a01993f91", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php85\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.5+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php85/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-06-23T16:12:55+00:00" + }, + { + "name": "symfony/service-contracts", + "version": "v3.6.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/service-contracts.git", + "reference": "45112560a3ba2d715666a509a0bc9521d10b6c43" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/45112560a3ba2d715666a509a0bc9521d10b6c43", + "reference": "45112560a3ba2d715666a509a0bc9521d10b6c43", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/container": "^1.1|^2.0", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "conflict": { + "ext-psr": "<1.1|>=2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Service\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to writing services", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/service-contracts/tree/v3.6.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-07-15T11:30:57+00:00" + }, + { + "name": "symfony/string", + "version": "v8.0.6", + "source": { + "type": "git", + "url": "https://github.com/symfony/string.git", + "reference": "6c9e1108041b5dce21a9a4984b531c4923aa9ec4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/string/zipball/6c9e1108041b5dce21a9a4984b531c4923aa9ec4", + "reference": "6c9e1108041b5dce21a9a4984b531c4923aa9ec4", + "shasum": "" + }, + "require": { + "php": ">=8.4", + "symfony/polyfill-ctype": "^1.8", + "symfony/polyfill-intl-grapheme": "^1.33", + "symfony/polyfill-intl-normalizer": "^1.0", + "symfony/polyfill-mbstring": "^1.0" + }, + "conflict": { + "symfony/translation-contracts": "<2.5" + }, + "require-dev": { + "symfony/emoji": "^7.4|^8.0", + "symfony/http-client": "^7.4|^8.0", + "symfony/intl": "^7.4|^8.0", + "symfony/translation-contracts": "^2.5|^3.0", + "symfony/var-exporter": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/functions.php" + ], + "psr-4": { + "Symfony\\Component\\String\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way", + "homepage": "https://symfony.com", + "keywords": [ + "grapheme", + "i18n", + "string", + "unicode", + "utf-8", + "utf8" + ], + "support": { + "source": "https://github.com/symfony/string/tree/v8.0.6" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-02-09T10:14:57+00:00" + }, + { + "name": "symfony/translation", + "version": "v8.0.6", + "source": { + "type": "git", + "url": "https://github.com/symfony/translation.git", + "reference": "13ff19bcf2bea492d3c2fbeaa194dd6f4599ce1b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/translation/zipball/13ff19bcf2bea492d3c2fbeaa194dd6f4599ce1b", + "reference": "13ff19bcf2bea492d3c2fbeaa194dd6f4599ce1b", + "shasum": "" + }, + "require": { + "php": ">=8.4", + "symfony/polyfill-mbstring": "^1.0", + "symfony/translation-contracts": "^3.6.1" + }, + "conflict": { + "nikic/php-parser": "<5.0", + "symfony/http-client-contracts": "<2.5", + "symfony/service-contracts": "<2.5" + }, + "provide": { + "symfony/translation-implementation": "2.3|3.0" + }, + "require-dev": { + "nikic/php-parser": "^5.0", + "psr/log": "^1|^2|^3", + "symfony/config": "^7.4|^8.0", + "symfony/console": "^7.4|^8.0", + "symfony/dependency-injection": "^7.4|^8.0", + "symfony/finder": "^7.4|^8.0", + "symfony/http-client-contracts": "^2.5|^3.0", + "symfony/http-kernel": "^7.4|^8.0", + "symfony/intl": "^7.4|^8.0", + "symfony/polyfill-intl-icu": "^1.21", + "symfony/routing": "^7.4|^8.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/yaml": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/functions.php" + ], + "psr-4": { + "Symfony\\Component\\Translation\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides tools to internationalize your application", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/translation/tree/v8.0.6" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-02-17T13:07:04+00:00" + }, + { + "name": "symfony/translation-contracts", + "version": "v3.6.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/translation-contracts.git", + "reference": "65a8bc82080447fae78373aa10f8d13b38338977" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/65a8bc82080447fae78373aa10f8d13b38338977", + "reference": "65a8bc82080447fae78373aa10f8d13b38338977", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Translation\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to translation", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/translation-contracts/tree/v3.6.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-07-15T13:41:35+00:00" + }, + { + "name": "symfony/var-dumper", + "version": "v8.0.6", + "source": { + "type": "git", + "url": "https://github.com/symfony/var-dumper.git", + "reference": "2e14f7e0bf5ff02c6e63bd31cb8e4855a13d6209" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/2e14f7e0bf5ff02c6e63bd31cb8e4855a13d6209", + "reference": "2e14f7e0bf5ff02c6e63bd31cb8e4855a13d6209", + "shasum": "" + }, + "require": { + "php": ">=8.4", + "symfony/polyfill-mbstring": "^1.0" + }, + "conflict": { + "symfony/console": "<7.4", + "symfony/error-handler": "<7.4" + }, + "require-dev": { + "symfony/console": "^7.4|^8.0", + "symfony/http-kernel": "^7.4|^8.0", + "symfony/process": "^7.4|^8.0", + "symfony/uid": "^7.4|^8.0", + "twig/twig": "^3.12" + }, + "bin": [ + "Resources/bin/var-dump-server" + ], + "type": "library", + "autoload": { + "files": [ + "Resources/functions/dump.php" + ], + "psr-4": { + "Symfony\\Component\\VarDumper\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides mechanisms for walking through any arbitrary PHP variable", + "homepage": "https://symfony.com", + "keywords": [ + "debug", + "dump" + ], + "support": { + "source": "https://github.com/symfony/var-dumper/tree/v8.0.6" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-02-15T10:53:29+00:00" + }, + { + "name": "topthink/think-container", + "version": "v3.0.2", + "source": { + "type": "git", + "url": "https://github.com/top-think/think-container.git", + "reference": "b2df244be1e7399ad4c8be1ccc40ed57868f730a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/top-think/think-container/zipball/b2df244be1e7399ad4c8be1ccc40ed57868f730a", + "reference": "b2df244be1e7399ad4c8be1ccc40ed57868f730a", + "shasum": "" + }, + "require": { + "php": ">=8.0", + "psr/container": "^2.0", + "topthink/think-helper": "^3.1" + }, + "require-dev": { + "phpunit/phpunit": "^9.5" + }, + "type": "library", + "autoload": { + "files": [], + "psr-4": { + "think\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "liu21st", + "email": "liu21st@gmail.com" + } + ], + "description": "PHP Container & Facade Manager", + "support": { + "issues": "https://github.com/top-think/think-container/issues", + "source": "https://github.com/top-think/think-container/tree/v3.0.2" + }, + "time": "2025-04-07T03:21:51+00:00" + }, + { + "name": "topthink/think-helper", + "version": "v3.1.12", + "source": { + "type": "git", + "url": "https://github.com/top-think/think-helper.git", + "reference": "fe277121112a8f1c872e169a733ca80bb11c4acb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/top-think/think-helper/zipball/fe277121112a8f1c872e169a733ca80bb11c4acb", + "reference": "fe277121112a8f1c872e169a733ca80bb11c4acb", + "shasum": "" + }, + "require": { + "php": ">=7.1.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.5" + }, + "type": "library", + "autoload": { + "files": [ + "src/helper.php" + ], + "psr-4": { + "think\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "yunwuxin", + "email": "448901948@qq.com" + } + ], + "description": "The ThinkPHP6 Helper Package", + "support": { + "issues": "https://github.com/top-think/think-helper/issues", + "source": "https://github.com/top-think/think-helper/tree/v3.1.12" + }, + "time": "2025-12-26T09:58:29+00:00" + }, + { + "name": "topthink/think-orm", + "version": "v4.0.51", + "source": { + "type": "git", + "url": "https://github.com/top-think/think-orm.git", + "reference": "46abe2f824eb3bcb117d4c0ce93b203b592b79f7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/top-think/think-orm/zipball/46abe2f824eb3bcb117d4c0ce93b203b592b79f7", + "reference": "46abe2f824eb3bcb117d4c0ce93b203b592b79f7", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-pdo": "*", + "php": ">=8.0.0", + "psr/log": ">=1.0", + "psr/simple-cache": "^3.0", + "topthink/think-helper": "^3.1", + "topthink/think-validate": "^3.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.6|^10" + }, + "suggest": { + "ext-mongodb": "provide mongodb support" + }, + "type": "library", + "autoload": { + "files": [ + "src/helper.php", + "stubs/load_stubs.php" + ], + "psr-4": { + "think\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "liu21st", + "email": "liu21st@gmail.com" + } + ], + "description": "the PHP Database&ORM Framework", + "keywords": [ + "database", + "orm" + ], + "support": { + "issues": "https://github.com/top-think/think-orm/issues", + "source": "https://github.com/top-think/think-orm/tree/v4.0.51" + }, + "time": "2025-12-18T13:11:52+00:00" + }, + { + "name": "topthink/think-validate", + "version": "v3.0.7", + "source": { + "type": "git", + "url": "https://github.com/top-think/think-validate.git", + "reference": "85063f6d4ef8ed122f17a36179dc3e0949b30988" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/top-think/think-validate/zipball/85063f6d4ef8ed122f17a36179dc3e0949b30988", + "reference": "85063f6d4ef8ed122f17a36179dc3e0949b30988", + "shasum": "" + }, + "require": { + "php": ">=8.0", + "topthink/think-container": ">=3.0" + }, + "type": "library", + "autoload": { + "files": [ + "src/helper.php" + ], + "psr-4": { + "think\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "liu21st", + "email": "liu21st@gmail.com" + } + ], + "description": "think validate", + "support": { + "issues": "https://github.com/top-think/think-validate/issues", + "source": "https://github.com/top-think/think-validate/tree/v3.0.7" + }, + "time": "2025-06-11T05:51:40+00:00" + }, + { + "name": "vlucas/phpdotenv", + "version": "v5.6.3", + "source": { + "type": "git", + "url": "https://github.com/vlucas/phpdotenv.git", + "reference": "955e7815d677a3eaa7075231212f2110983adecc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/955e7815d677a3eaa7075231212f2110983adecc", + "reference": "955e7815d677a3eaa7075231212f2110983adecc", + "shasum": "" + }, + "require": { + "ext-pcre": "*", + "graham-campbell/result-type": "^1.1.4", + "php": "^7.2.5 || ^8.0", + "phpoption/phpoption": "^1.9.5", + "symfony/polyfill-ctype": "^1.26", + "symfony/polyfill-mbstring": "^1.26", + "symfony/polyfill-php80": "^1.26" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "ext-filter": "*", + "phpunit/phpunit": "^8.5.34 || ^9.6.13 || ^10.4.2" + }, + "suggest": { + "ext-filter": "Required to use the boolean validator." + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + }, + "branch-alias": { + "dev-master": "5.6-dev" + } + }, + "autoload": { + "psr-4": { + "Dotenv\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Vance Lucas", + "email": "vance@vancelucas.com", + "homepage": "https://github.com/vlucas" + } + ], + "description": "Loads environment variables from `.env` to `getenv()`, `$_ENV` and `$_SERVER` automagically.", + "keywords": [ + "dotenv", + "env", + "environment" + ], + "support": { + "issues": "https://github.com/vlucas/phpdotenv/issues", + "source": "https://github.com/vlucas/phpdotenv/tree/v5.6.3" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/vlucas/phpdotenv", + "type": "tidelift" + } + ], + "time": "2025-12-27T19:49:13+00:00" + }, + { + "name": "voku/portable-ascii", + "version": "2.0.3", + "source": { + "type": "git", + "url": "https://github.com/voku/portable-ascii.git", + "reference": "b1d923f88091c6bf09699efcd7c8a1b1bfd7351d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/voku/portable-ascii/zipball/b1d923f88091c6bf09699efcd7c8a1b1bfd7351d", + "reference": "b1d923f88091c6bf09699efcd7c8a1b1bfd7351d", + "shasum": "" + }, + "require": { + "php": ">=7.0.0" + }, + "require-dev": { + "phpunit/phpunit": "~6.0 || ~7.0 || ~9.0" + }, + "suggest": { + "ext-intl": "Use Intl for transliterator_transliterate() support" + }, + "type": "library", + "autoload": { + "psr-4": { + "voku\\": "src/voku/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Lars Moelleken", + "homepage": "https://www.moelleken.org/" + } + ], + "description": "Portable ASCII library - performance optimized (ascii) string functions for php.", + "homepage": "https://github.com/voku/portable-ascii", + "keywords": [ + "ascii", + "clean", + "php" + ], + "support": { + "issues": "https://github.com/voku/portable-ascii/issues", + "source": "https://github.com/voku/portable-ascii/tree/2.0.3" + }, + "funding": [ + { + "url": "https://www.paypal.me/moelleken", + "type": "custom" + }, + { + "url": "https://github.com/voku", + "type": "github" + }, + { + "url": "https://opencollective.com/portable-ascii", + "type": "open_collective" + }, + { + "url": "https://www.patreon.com/voku", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/voku/portable-ascii", + "type": "tidelift" + } + ], + "time": "2024-11-21T01:49:47+00:00" + }, + { + "name": "webman/console", + "version": "v2.2.2", + "source": { + "type": "git", + "url": "https://github.com/webman-php/console.git", + "reference": "e66e21c3db1685ac76841df3316d488b25e23d81" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/webman-php/console/zipball/e66e21c3db1685ac76841df3316d488b25e23d81", + "reference": "e66e21c3db1685ac76841df3316d488b25e23d81", + "shasum": "" + }, + "require": { + "doctrine/inflector": "^2.0", + "php": ">=8.1", + "symfony/console": "^6.1 || ^7.0" + }, + "require-dev": { + "workerman/webman": "^2.1" + }, + "type": "library", + "autoload": { + "psr-4": { + "Webman\\Console\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "walkor", + "email": "walkor@workerman.net", + "homepage": "http://www.workerman.net", + "role": "Developer" + } + ], + "description": "Webman console", + "homepage": "http://www.workerman.net", + "keywords": [ + "webman console" + ], + "support": { + "email": "walkor@workerman.net", + "forum": "http://www.workerman.net/questions", + "issues": "https://github.com/webman-php/console/issues", + "source": "https://github.com/webman-php/console", + "wiki": "http://www.workerman.net/doc/webman" + }, + "time": "2026-02-26T02:45:43+00:00" + }, + { + "name": "webman/database", + "version": "v2.1.9", + "source": { + "type": "git", + "url": "https://github.com/webman-php/database.git", + "reference": "5a1463c96c79b35225f1cbd2f3e65830a7b4da0e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/webman-php/database/zipball/5a1463c96c79b35225f1cbd2f3e65830a7b4da0e", + "reference": "5a1463c96c79b35225f1cbd2f3e65830a7b4da0e", + "shasum": "" + }, + "require": { + "illuminate/database": "^10.0 || ^11.0 || ^12.0", + "illuminate/http": "^10.0 || ^11.0 || ^12.0", + "laravel/serializable-closure": "^1.0 || ^2.0", + "workerman/webman-framework": "^2.1 || dev-master" + }, + "type": "library", + "autoload": { + "psr-4": { + "support\\": "src/support", + "Webman\\Database\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Webman database", + "support": { + "issues": "https://github.com/webman-php/database/issues", + "source": "https://github.com/webman-php/database/tree/v2.1.9" + }, + "time": "2026-02-02T10:45:05+00:00" + }, + { + "name": "webman/think-orm", + "version": "v2.1.10", + "source": { + "type": "git", + "url": "https://github.com/webman-php/think-orm.git", + "reference": "b779fce0de4d66e08eb8925eb5841107d1ca9ec4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/webman-php/think-orm/zipball/b779fce0de4d66e08eb8925eb5841107d1ca9ec4", + "reference": "b779fce0de4d66e08eb8925eb5841107d1ca9ec4", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "topthink/think-container": "^2.0|^3.0", + "topthink/think-orm": "^2.0.53 || ^3.0.0 || ^4.0.30 || dev-master", + "workerman/webman-framework": "^2.1 || dev-master" + }, + "type": "library", + "autoload": { + "psr-4": { + "support\\": "src/support", + "Webman\\ThinkOrm\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "support": { + "issues": "https://github.com/webman-php/think-orm/issues", + "source": "https://github.com/webman-php/think-orm/tree/v2.1.10" + }, + "time": "2026-02-04T09:07:18+00:00" + }, + { + "name": "workerman/coroutine", + "version": "v1.1.4", + "source": { + "type": "git", + "url": "https://github.com/workerman-php/coroutine.git", + "reference": "b0bebfa9d41b992ad0a835ddf2ee8fa5d58eca44" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/workerman-php/coroutine/zipball/b0bebfa9d41b992ad0a835ddf2ee8fa5d58eca44", + "reference": "b0bebfa9d41b992ad0a835ddf2ee8fa5d58eca44", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^11.0", + "psr/log": "*" + }, + "type": "library", + "autoload": { + "psr-4": { + "Workerman\\": "src", + "Workerman\\Coroutine\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Workerman coroutine", + "support": { + "issues": "https://github.com/workerman-php/coroutine/issues", + "source": "https://github.com/workerman-php/coroutine/tree/v1.1.4" + }, + "time": "2025-10-11T15:09:08+00:00" + }, + { + "name": "workerman/webman-framework", + "version": "2.2.0", + "source": { + "type": "git", + "url": "https://github.com/walkor/webman-framework.git", + "reference": "2da4e49259d41925f1732f95f6fb052a3f42ceee" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/walkor/webman-framework/zipball/2da4e49259d41925f1732f95f6fb052a3f42ceee", + "reference": "2da4e49259d41925f1732f95f6fb052a3f42ceee", + "shasum": "" + }, + "require": { + "ext-json": "*", + "nikic/fast-route": "^1.3", + "php": ">=8.1", + "psr/container": ">=1.0", + "psr/log": "^2.0 || ^3.0", + "workerman/workerman": "^5.1 || dev-master" + }, + "suggest": { + "ext-event": "For better performance. " + }, + "type": "library", + "autoload": { + "files": [ + "./src/support/helpers.php" + ], + "psr-4": { + "Webman\\": "./src", + "Support\\": "./src/support", + "support\\": "./src/support", + "Support\\View\\": "./src/support/view", + "Support\\Bootstrap\\": "./src/support/bootstrap", + "Support\\Exception\\": "./src/support/exception" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "walkor", + "email": "walkor@workerman.net", + "homepage": "https://www.workerman.net", + "role": "Developer" + } + ], + "description": "High performance HTTP Service Framework.", + "homepage": "https://www.workerman.net", + "keywords": [ + "High Performance", + "http service" + ], + "support": { + "email": "walkor@workerman.net", + "forum": "https://wenda.workerman.net/", + "issues": "https://github.com/walkor/webman/issues", + "source": "https://github.com/walkor/webman-framework", + "wiki": "https://doc.workerman.net/" + }, + "time": "2026-02-20T02:37:25+00:00" + }, + { + "name": "workerman/workerman", + "version": "v5.1.9", + "source": { + "type": "git", + "url": "https://github.com/walkor/workerman.git", + "reference": "fff0954628f8ceeccfe29d3e817f0fad87cfdbf2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/walkor/workerman/zipball/fff0954628f8ceeccfe29d3e817f0fad87cfdbf2", + "reference": "fff0954628f8ceeccfe29d3e817f0fad87cfdbf2", + "shasum": "" + }, + "require": { + "ext-json": "*", + "php": ">=8.1", + "workerman/coroutine": "^1.1 || dev-main" + }, + "conflict": { + "ext-swow": "=3 <3.2.2" + }, + "require-dev": { + "doctrine/collections": "^1.6.8", + "doctrine/common": "^2.13.3 || ^3.2.2", + "phpspec/prophecy": "^1.10", + "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" + }, + "type": "library", + "autoload": { + "files": [ + "src/DeepCopy/deep_copy.php" + ], + "psr-4": { + "DeepCopy\\": "src/DeepCopy/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Create deep copies (clones) of your objects", + "keywords": [ + "clone", + "copy", + "duplicate", + "object", + "object graph" + ], + "support": { + "issues": "https://github.com/myclabs/DeepCopy/issues", + "source": "https://github.com/myclabs/DeepCopy/tree/1.13.4" + }, + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", + "type": "tidelift" + } + ], + "time": "2025-08-01T08:46:24+00:00" + }, + { + "name": "nikic/php-parser", + "version": "v5.7.0", + "source": { + "type": "git", + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/dca41cd15c2ac9d055ad70dbfd011130757d1f82", + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-json": "*", + "ext-tokenizer": "*", + "php": ">=7.4" + }, + "require-dev": { + "ircmaxell/php-yacc": "^0.0.7", + "phpunit/phpunit": "^9.0" + }, + "bin": [ + "bin/php-parse" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.x-dev" + } + }, + "autoload": { + "psr-4": { + "PhpParser\\": "lib/PhpParser" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nikita Popov" + } + ], + "description": "A PHP parser written in PHP", + "keywords": [ + "parser", + "php" + ], + "support": { + "issues": "https://github.com/nikic/PHP-Parser/issues", + "source": "https://github.com/nikic/PHP-Parser/tree/v5.7.0" + }, + "time": "2025-12-06T11:56:16+00:00" + }, + { + "name": "phar-io/manifest", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/phar-io/manifest.git", + "reference": "54750ef60c58e43759730615a392c31c80e23176" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/54750ef60c58e43759730615a392c31c80e23176", + "reference": "54750ef60c58e43759730615a392c31c80e23176", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-phar": "*", + "ext-xmlwriter": "*", + "phar-io/version": "^3.0.1", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", + "support": { + "issues": "https://github.com/phar-io/manifest/issues", + "source": "https://github.com/phar-io/manifest/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2024-03-03T12:33:53+00:00" + }, + { + "name": "phar-io/version", + "version": "3.2.1", + "source": { + "type": "git", + "url": "https://github.com/phar-io/version.git", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Library for handling version information and constraints", + "support": { + "issues": "https://github.com/phar-io/version/issues", + "source": "https://github.com/phar-io/version/tree/3.2.1" + }, + "time": "2022-02-21T01:04:05+00:00" + }, + { + "name": "phpunit/php-code-coverage", + "version": "13.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "a8b58fde2f4fbc69a064e1f80ff917607cf7737c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/a8b58fde2f4fbc69a064e1f80ff917607cf7737c", + "reference": "a8b58fde2f4fbc69a064e1f80ff917607cf7737c", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-xmlwriter": "*", + "nikic/php-parser": "^5.7.0", + "php": ">=8.4", + "phpunit/php-file-iterator": "^7.0", + "phpunit/php-text-template": "^6.0", + "sebastian/complexity": "^6.0", + "sebastian/environment": "^9.0", + "sebastian/lines-of-code": "^5.0", + "sebastian/version": "^7.0", + "theseer/tokenizer": "^2.0.1" + }, + "require-dev": { + "phpunit/phpunit": "^13.0" + }, + "suggest": { + "ext-pcov": "PHP extension that provides line coverage", + "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "13.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", + "homepage": "https://github.com/sebastianbergmann/php-code-coverage", + "keywords": [ + "coverage", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", + "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/13.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/php-code-coverage", + "type": "tidelift" + } + ], + "time": "2026-02-06T06:05:15+00:00" + }, + { + "name": "phpunit/php-file-iterator", + "version": "7.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-file-iterator.git", + "reference": "6e5aa1fb0a95b1703d83e721299ee18bb4e2de50" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/6e5aa1fb0a95b1703d83e721299ee18bb4e2de50", + "reference": "6e5aa1fb0a95b1703d83e721299ee18bb4e2de50", + "shasum": "" + }, + "require": { + "php": ">=8.4" + }, + "require-dev": { + "phpunit/phpunit": "^13.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "FilterIterator implementation that filters files based on a list of suffixes.", + "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", + "keywords": [ + "filesystem", + "iterator" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", + "security": "https://github.com/sebastianbergmann/php-file-iterator/security/policy", + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/7.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/php-file-iterator", + "type": "tidelift" + } + ], + "time": "2026-02-06T04:33:26+00:00" + }, + { + "name": "phpunit/php-invoker", + "version": "7.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-invoker.git", + "reference": "42e5c5cae0c65df12d1b1a3ab52bf3f50f244d88" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/42e5c5cae0c65df12d1b1a3ab52bf3f50f244d88", + "reference": "42e5c5cae0c65df12d1b1a3ab52bf3f50f244d88", + "shasum": "" + }, + "require": { + "php": ">=8.4" + }, + "require-dev": { + "ext-pcntl": "*", + "phpunit/phpunit": "^13.0" + }, + "suggest": { + "ext-pcntl": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Invoke callables with a timeout", + "homepage": "https://github.com/sebastianbergmann/php-invoker/", + "keywords": [ + "process" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-invoker/issues", + "security": "https://github.com/sebastianbergmann/php-invoker/security/policy", + "source": "https://github.com/sebastianbergmann/php-invoker/tree/7.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/php-invoker", + "type": "tidelift" + } + ], + "time": "2026-02-06T04:34:47+00:00" + }, + { + "name": "phpunit/php-text-template", + "version": "6.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-text-template.git", + "reference": "a47af19f93f76aa3368303d752aa5272ca3299f4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/a47af19f93f76aa3368303d752aa5272ca3299f4", + "reference": "a47af19f93f76aa3368303d752aa5272ca3299f4", + "shasum": "" + }, + "require": { + "php": ">=8.4" + }, + "require-dev": { + "phpunit/phpunit": "^13.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Simple template engine.", + "homepage": "https://github.com/sebastianbergmann/php-text-template/", + "keywords": [ + "template" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-text-template/issues", + "security": "https://github.com/sebastianbergmann/php-text-template/security/policy", + "source": "https://github.com/sebastianbergmann/php-text-template/tree/6.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/php-text-template", + "type": "tidelift" + } + ], + "time": "2026-02-06T04:36:37+00:00" + }, + { + "name": "phpunit/php-timer", + "version": "9.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-timer.git", + "reference": "a0e12065831f6ab0d83120dc61513eb8d9a966f6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/a0e12065831f6ab0d83120dc61513eb8d9a966f6", + "reference": "a0e12065831f6ab0d83120dc61513eb8d9a966f6", + "shasum": "" + }, + "require": { + "php": ">=8.4" + }, + "require-dev": { + "phpunit/phpunit": "^13.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "9.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Utility class for timing", + "homepage": "https://github.com/sebastianbergmann/php-timer/", + "keywords": [ + "timer" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-timer/issues", + "security": "https://github.com/sebastianbergmann/php-timer/security/policy", + "source": "https://github.com/sebastianbergmann/php-timer/tree/9.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/php-timer", + "type": "tidelift" + } + ], + "time": "2026-02-06T04:37:53+00:00" + }, + { + "name": "phpunit/phpunit", + "version": "13.0.5", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "d57826e8921a534680c613924bfd921ded8047f4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/d57826e8921a534680c613924bfd921ded8047f4", + "reference": "d57826e8921a534680c613924bfd921ded8047f4", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-xml": "*", + "ext-xmlwriter": "*", + "myclabs/deep-copy": "^1.13.4", + "phar-io/manifest": "^2.0.4", + "phar-io/version": "^3.2.1", + "php": ">=8.4.1", + "phpunit/php-code-coverage": "^13.0.1", + "phpunit/php-file-iterator": "^7.0.0", + "phpunit/php-invoker": "^7.0.0", + "phpunit/php-text-template": "^6.0.0", + "phpunit/php-timer": "^9.0.0", + "sebastian/cli-parser": "^5.0.0", + "sebastian/comparator": "^8.0.0", + "sebastian/diff": "^8.0.0", + "sebastian/environment": "^9.0.0", + "sebastian/exporter": "^8.0.0", + "sebastian/global-state": "^9.0.0", + "sebastian/object-enumerator": "^8.0.0", + "sebastian/recursion-context": "^8.0.0", + "sebastian/type": "^7.0.0", + "sebastian/version": "^7.0.0", + "staabm/side-effects-detector": "^1.0.5" + }, + "bin": [ + "phpunit" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "13.0-dev" + } + }, + "autoload": { + "files": [ + "src/Framework/Assert/Functions.php" + ], + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "The PHP Unit Testing framework.", + "homepage": "https://phpunit.de/", + "keywords": [ + "phpunit", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/phpunit/issues", + "security": "https://github.com/sebastianbergmann/phpunit/security/policy", + "source": "https://github.com/sebastianbergmann/phpunit/tree/13.0.5" + }, + "funding": [ + { + "url": "https://phpunit.de/sponsors.html", + "type": "custom" + }, + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit", + "type": "tidelift" + } + ], + "time": "2026-02-18T12:40:03+00:00" + }, + { + "name": "sebastian/cli-parser", + "version": "5.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/cli-parser.git", + "reference": "48a4654fa5e48c1c81214e9930048a572d4b23ca" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/48a4654fa5e48c1c81214e9930048a572d4b23ca", + "reference": "48a4654fa5e48c1c81214e9930048a572d4b23ca", + "shasum": "" + }, + "require": { + "php": ">=8.4" + }, + "require-dev": { + "phpunit/phpunit": "^13.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for parsing CLI options", + "homepage": "https://github.com/sebastianbergmann/cli-parser", + "support": { + "issues": "https://github.com/sebastianbergmann/cli-parser/issues", + "security": "https://github.com/sebastianbergmann/cli-parser/security/policy", + "source": "https://github.com/sebastianbergmann/cli-parser/tree/5.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/cli-parser", + "type": "tidelift" + } + ], + "time": "2026-02-06T04:39:44+00:00" + }, + { + "name": "sebastian/comparator", + "version": "8.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "29b232ddc29c2b114c0358c69b3084e7c3da0d58" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/29b232ddc29c2b114c0358c69b3084e7c3da0d58", + "reference": "29b232ddc29c2b114c0358c69b3084e7c3da0d58", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-mbstring": "*", + "php": ">=8.4", + "sebastian/diff": "^8.0", + "sebastian/exporter": "^8.0" + }, + "require-dev": { + "phpunit/phpunit": "^13.0" + }, + "suggest": { + "ext-bcmath": "For comparing BcMath\\Number objects" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "8.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + } + ], + "description": "Provides the functionality to compare PHP values for equality", + "homepage": "https://github.com/sebastianbergmann/comparator", + "keywords": [ + "comparator", + "compare", + "equality" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/comparator/issues", + "security": "https://github.com/sebastianbergmann/comparator/security/policy", + "source": "https://github.com/sebastianbergmann/comparator/tree/8.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/comparator", + "type": "tidelift" + } + ], + "time": "2026-02-06T04:40:39+00:00" + }, + { + "name": "sebastian/complexity", + "version": "6.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/complexity.git", + "reference": "c5651c795c98093480df79350cb050813fc7a2f3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/c5651c795c98093480df79350cb050813fc7a2f3", + "reference": "c5651c795c98093480df79350cb050813fc7a2f3", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^5.0", + "php": ">=8.4" + }, + "require-dev": { + "phpunit/phpunit": "^13.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for calculating the complexity of PHP code units", + "homepage": "https://github.com/sebastianbergmann/complexity", + "support": { + "issues": "https://github.com/sebastianbergmann/complexity/issues", + "security": "https://github.com/sebastianbergmann/complexity/security/policy", + "source": "https://github.com/sebastianbergmann/complexity/tree/6.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/complexity", + "type": "tidelift" + } + ], + "time": "2026-02-06T04:41:32+00:00" + }, + { + "name": "sebastian/diff", + "version": "8.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "a2b6d09d7729ee87d605a439469f9dcc39be5ea3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/a2b6d09d7729ee87d605a439469f9dcc39be5ea3", + "reference": "a2b6d09d7729ee87d605a439469f9dcc39be5ea3", + "shasum": "" + }, + "require": { + "php": ">=8.4" + }, + "require-dev": { + "phpunit/phpunit": "^13.0", + "symfony/process": "^7.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "8.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + } + ], + "description": "Diff implementation", + "homepage": "https://github.com/sebastianbergmann/diff", + "keywords": [ + "diff", + "udiff", + "unidiff", + "unified diff" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/diff/issues", + "security": "https://github.com/sebastianbergmann/diff/security/policy", + "source": "https://github.com/sebastianbergmann/diff/tree/8.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/diff", + "type": "tidelift" + } + ], + "time": "2026-02-06T04:42:27+00:00" + }, + { + "name": "sebastian/environment", + "version": "9.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/environment.git", + "reference": "bb64d08145b021b67d5f253308a498b73ab0461e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/bb64d08145b021b67d5f253308a498b73ab0461e", + "reference": "bb64d08145b021b67d5f253308a498b73ab0461e", + "shasum": "" + }, + "require": { + "php": ">=8.4" + }, + "require-dev": { + "phpunit/phpunit": "^13.0" + }, + "suggest": { + "ext-posix": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "9.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides functionality to handle HHVM/PHP environments", + "homepage": "https://github.com/sebastianbergmann/environment", + "keywords": [ + "Xdebug", + "environment", + "hhvm" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/environment/issues", + "security": "https://github.com/sebastianbergmann/environment/security/policy", + "source": "https://github.com/sebastianbergmann/environment/tree/9.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/environment", + "type": "tidelift" + } + ], + "time": "2026-02-06T04:43:29+00:00" + }, + { + "name": "sebastian/exporter", + "version": "8.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/exporter.git", + "reference": "dc31f1f8e0186c8f0bb3e48fd4d51421d8905fea" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/dc31f1f8e0186c8f0bb3e48fd4d51421d8905fea", + "reference": "dc31f1f8e0186c8f0bb3e48fd4d51421d8905fea", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": ">=8.4", + "sebastian/recursion-context": "^8.0" + }, + "require-dev": { + "phpunit/phpunit": "^13.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "8.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Provides the functionality to export PHP variables for visualization", + "homepage": "https://www.github.com/sebastianbergmann/exporter", + "keywords": [ + "export", + "exporter" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/exporter/issues", + "security": "https://github.com/sebastianbergmann/exporter/security/policy", + "source": "https://github.com/sebastianbergmann/exporter/tree/8.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/exporter", + "type": "tidelift" + } + ], + "time": "2026-02-06T04:44:28+00:00" + }, + { + "name": "sebastian/global-state", + "version": "9.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/global-state.git", + "reference": "e52e3dc22441e6218c710afe72c3042f8fc41ea7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/e52e3dc22441e6218c710afe72c3042f8fc41ea7", + "reference": "e52e3dc22441e6218c710afe72c3042f8fc41ea7", + "shasum": "" + }, + "require": { + "php": ">=8.4", + "sebastian/object-reflector": "^6.0", + "sebastian/recursion-context": "^8.0" + }, + "require-dev": { + "ext-dom": "*", + "phpunit/phpunit": "^13.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "9.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Snapshotting of global state", + "homepage": "https://www.github.com/sebastianbergmann/global-state", + "keywords": [ + "global state" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/global-state/issues", + "security": "https://github.com/sebastianbergmann/global-state/security/policy", + "source": "https://github.com/sebastianbergmann/global-state/tree/9.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/global-state", + "type": "tidelift" + } + ], + "time": "2026-02-06T04:45:13+00:00" + }, + { + "name": "sebastian/lines-of-code", + "version": "5.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/lines-of-code.git", + "reference": "4f21bb7768e1c997722ccc7efb1d6b5c11bfd471" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/4f21bb7768e1c997722ccc7efb1d6b5c11bfd471", + "reference": "4f21bb7768e1c997722ccc7efb1d6b5c11bfd471", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^5.0", + "php": ">=8.4" + }, + "require-dev": { + "phpunit/phpunit": "^13.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for counting the lines of code in PHP source code", + "homepage": "https://github.com/sebastianbergmann/lines-of-code", + "support": { + "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", + "security": "https://github.com/sebastianbergmann/lines-of-code/security/policy", + "source": "https://github.com/sebastianbergmann/lines-of-code/tree/5.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/lines-of-code", + "type": "tidelift" + } + ], + "time": "2026-02-06T04:45:54+00:00" + }, + { + "name": "sebastian/object-enumerator", + "version": "8.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-enumerator.git", + "reference": "b39ab125fd9a7434b0ecbc4202eebce11a98cfc5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/b39ab125fd9a7434b0ecbc4202eebce11a98cfc5", + "reference": "b39ab125fd9a7434b0ecbc4202eebce11a98cfc5", + "shasum": "" + }, + "require": { + "php": ">=8.4", + "sebastian/object-reflector": "^6.0", + "sebastian/recursion-context": "^8.0" + }, + "require-dev": { + "phpunit/phpunit": "^13.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "8.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Traverses array structures and object graphs to enumerate all referenced objects", + "homepage": "https://github.com/sebastianbergmann/object-enumerator/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", + "security": "https://github.com/sebastianbergmann/object-enumerator/security/policy", + "source": "https://github.com/sebastianbergmann/object-enumerator/tree/8.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/object-enumerator", + "type": "tidelift" + } + ], + "time": "2026-02-06T04:46:36+00:00" + }, + { + "name": "sebastian/object-reflector", + "version": "6.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-reflector.git", + "reference": "3ca042c2c60b0eab094f8a1b6a7093f4d4c72200" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/3ca042c2c60b0eab094f8a1b6a7093f4d4c72200", + "reference": "3ca042c2c60b0eab094f8a1b6a7093f4d4c72200", + "shasum": "" + }, + "require": { + "php": ">=8.4" + }, + "require-dev": { + "phpunit/phpunit": "^13.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Allows reflection of object attributes, including inherited and non-public ones", + "homepage": "https://github.com/sebastianbergmann/object-reflector/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-reflector/issues", + "security": "https://github.com/sebastianbergmann/object-reflector/security/policy", + "source": "https://github.com/sebastianbergmann/object-reflector/tree/6.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/object-reflector", + "type": "tidelift" + } + ], + "time": "2026-02-06T04:47:13+00:00" + }, + { + "name": "sebastian/recursion-context", + "version": "8.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "74c5af21f6a5833e91767ca068c4d3dfec15317e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/74c5af21f6a5833e91767ca068c4d3dfec15317e", + "reference": "74c5af21f6a5833e91767ca068c4d3dfec15317e", + "shasum": "" + }, + "require": { + "php": ">=8.4" + }, + "require-dev": { + "phpunit/phpunit": "^13.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "8.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + } + ], + "description": "Provides functionality to recursively process PHP variables", + "homepage": "https://github.com/sebastianbergmann/recursion-context", + "support": { + "issues": "https://github.com/sebastianbergmann/recursion-context/issues", + "security": "https://github.com/sebastianbergmann/recursion-context/security/policy", + "source": "https://github.com/sebastianbergmann/recursion-context/tree/8.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/recursion-context", + "type": "tidelift" + } + ], + "time": "2026-02-06T04:51:28+00:00" + }, + { + "name": "sebastian/type", + "version": "7.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/type.git", + "reference": "42412224607bd3931241bbd17f38e0f972f5a916" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/42412224607bd3931241bbd17f38e0f972f5a916", + "reference": "42412224607bd3931241bbd17f38e0f972f5a916", + "shasum": "" + }, + "require": { + "php": ">=8.4" + }, + "require-dev": { + "phpunit/phpunit": "^13.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the types of the PHP type system", + "homepage": "https://github.com/sebastianbergmann/type", + "support": { + "issues": "https://github.com/sebastianbergmann/type/issues", + "security": "https://github.com/sebastianbergmann/type/security/policy", + "source": "https://github.com/sebastianbergmann/type/tree/7.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/type", + "type": "tidelift" + } + ], + "time": "2026-02-06T04:52:09+00:00" + }, + { + "name": "sebastian/version", + "version": "7.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "ad37a5552c8e2b88572249fdc19b6da7792e021b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/ad37a5552c8e2b88572249fdc19b6da7792e021b", + "reference": "ad37a5552c8e2b88572249fdc19b6da7792e021b", + "shasum": "" + }, + "require": { + "php": ">=8.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that helps with managing the version number of Git-hosted PHP projects", + "homepage": "https://github.com/sebastianbergmann/version", + "support": { + "issues": "https://github.com/sebastianbergmann/version/issues", + "security": "https://github.com/sebastianbergmann/version/security/policy", + "source": "https://github.com/sebastianbergmann/version/tree/7.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/version", + "type": "tidelift" + } + ], + "time": "2026-02-06T04:52:52+00:00" + }, + { + "name": "staabm/side-effects-detector", + "version": "1.0.5", + "source": { + "type": "git", + "url": "https://github.com/staabm/side-effects-detector.git", + "reference": "d8334211a140ce329c13726d4a715adbddd0a163" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/staabm/side-effects-detector/zipball/d8334211a140ce329c13726d4a715adbddd0a163", + "reference": "d8334211a140ce329c13726d4a715adbddd0a163", + "shasum": "" + }, + "require": { + "ext-tokenizer": "*", + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan": "^1.12.6", + "phpunit/phpunit": "^9.6.21", + "symfony/var-dumper": "^5.4.43", + "tomasvotruba/type-coverage": "1.0.0", + "tomasvotruba/unused-public": "1.0.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "lib/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A static analysis tool to detect side effects in PHP code", + "keywords": [ + "static analysis" + ], + "support": { + "issues": "https://github.com/staabm/side-effects-detector/issues", + "source": "https://github.com/staabm/side-effects-detector/tree/1.0.5" + }, + "funding": [ + { + "url": "https://github.com/staabm", + "type": "github" + } + ], + "time": "2024-10-20T05:08:20+00:00" + }, + { + "name": "theseer/tokenizer", + "version": "2.0.1", + "source": { + "type": "git", + "url": "https://github.com/theseer/tokenizer.git", + "reference": "7989e43bf381af0eac72e4f0ca5bcbfa81658be4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/7989e43bf381af0eac72e4f0ca5bcbfa81658be4", + "reference": "7989e43bf381af0eac72e4f0ca5bcbfa81658be4", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": "^8.1" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + } + ], + "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", + "support": { + "issues": "https://github.com/theseer/tokenizer/issues", + "source": "https://github.com/theseer/tokenizer/tree/2.0.1" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2025-12-08T11:19:18+00:00" + } + ], + "aliases": [], + "minimum-stability": "dev", + "stability-flags": {}, + "prefer-stable": true, + "prefer-lowest": false, + "platform": { + "php": ">=8.1" + }, + "platform-dev": {}, + "plugin-api-version": "2.6.0" +} diff --git a/config/app.php b/config/app.php new file mode 100644 index 0000000..f26e358 --- /dev/null +++ b/config/app.php @@ -0,0 +1,26 @@ + + * @copyright walkor + * @link http://www.workerman.net/ + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ + +use support\Request; + +return [ + 'debug' => true, + 'error_reporting' => E_ALL, + 'default_timezone' => 'Asia/Shanghai', + 'request_class' => Request::class, + 'public_path' => base_path() . DIRECTORY_SEPARATOR . 'public', + 'runtime_path' => base_path(false) . DIRECTORY_SEPARATOR . 'runtime', + 'controller_suffix' => 'Controller', + 'controller_reuse' => false, +]; diff --git a/config/autoload.php b/config/autoload.php new file mode 100644 index 0000000..69a8135 --- /dev/null +++ b/config/autoload.php @@ -0,0 +1,21 @@ + + * @copyright walkor + * @link http://www.workerman.net/ + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ + +return [ + 'files' => [ + base_path() . '/app/functions.php', + base_path() . '/support/Request.php', + base_path() . '/support/Response.php', + ] +]; diff --git a/config/bootstrap.php b/config/bootstrap.php new file mode 100644 index 0000000..5e7047a --- /dev/null +++ b/config/bootstrap.php @@ -0,0 +1,19 @@ + + * @copyright walkor + * @link http://www.workerman.net/ + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ + +return [ + support\bootstrap\Session::class, + Webman\ThinkOrm\ThinkOrm::class, + app\bootstrap\SqlDebug::class, +]; diff --git a/config/container.php b/config/container.php new file mode 100644 index 0000000..106b7b4 --- /dev/null +++ b/config/container.php @@ -0,0 +1,15 @@ + + * @copyright walkor + * @link http://www.workerman.net/ + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ + +return new Webman\Container; \ No newline at end of file diff --git a/config/database.php b/config/database.php new file mode 100644 index 0000000..07c0fb5 --- /dev/null +++ b/config/database.php @@ -0,0 +1,32 @@ + 'mysql', + 'connections' => [ + 'mysql' => [ + 'driver' => 'mysql', + 'host' => Config::getStringEnv('DB_HOST', 'localhost'), + 'port' => Config::getIntEnv('DB_PORT',3306), + 'database' => Config::getStringEnv('DB_NAME', 'opm_ectms'), + 'username' => Config::getStringEnv('DB_USER', 'root'), + 'password' => Config::getStringEnv('DB_PASSWORD'), + 'charset' => 'utf8mb4', + 'collation' => 'utf8mb4_general_ci', + 'prefix' => '', + 'strict' => true, + 'engine' => null, + 'options' => [ + PDO::ATTR_EMULATE_PREPARES => false, // Must be false for Swoole and Swow drivers. + ], + 'pool' => [ + 'max_connections' => 5, + 'min_connections' => 1, + 'wait_timeout' => 3, + 'idle_timeout' => 60, + 'heartbeat_interval' => 50, + ], + ], + ], +]; \ No newline at end of file diff --git a/config/dependence.php b/config/dependence.php new file mode 100644 index 0000000..8e964ed --- /dev/null +++ b/config/dependence.php @@ -0,0 +1,15 @@ + + * @copyright walkor + * @link http://www.workerman.net/ + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ + +return []; \ No newline at end of file diff --git a/config/exception.php b/config/exception.php new file mode 100644 index 0000000..f2aede3 --- /dev/null +++ b/config/exception.php @@ -0,0 +1,17 @@ + + * @copyright walkor + * @link http://www.workerman.net/ + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ + +return [ + '' => support\exception\Handler::class, +]; \ No newline at end of file diff --git a/config/log.php b/config/log.php new file mode 100644 index 0000000..dbc11e2 --- /dev/null +++ b/config/log.php @@ -0,0 +1,184 @@ + [ + 'handlers' => [ + // 控制台输出处理器 + [ + 'class' => StreamHandler::class, + 'constructor' => [ + 'php://stdout', + Config::getInstance()->logLevel, + ], + 'formatter' => [ + 'class' => Monolog\Formatter\LineFormatter::class, + /** + * 格式化日志输出 + * %start%: 标记日志颜色 + * %end%: 结束标记颜色 + * %logger%: 日志记录器名称 + * %L: 行号 + * %M: 方法名 + * %P: 进程ID + * %thread%: 线程ID + * %Level%: 日志级别(带有占位符) + * %level_name%: 日志级别名称 + * %level%: 日志等级数字 + * %message%: 日志内容 + * %datetime%: 时间 + */ + 'constructor' => [ + "%start%%datetime% [%thread%] %Level% %logger%:%L% - %message%%end%\n", + 'Y-m-d H:i:s', + true + ], + ], + ], + // 默认文件输出处理器 + [ + 'class' => RotatingFileHandler::class, + 'constructor' => [ + runtime_path() . '/logs/webman.log', + Config::getInstance()->logRotationTimeByDay, + Config::getInstance()->logLevel, + ], + 'formatter' => [ + 'class' => Monolog\Formatter\LineFormatter::class, + 'constructor' => [ + "%datetime% [%thread%] %Level% %logger%:%L% - %message%\n", + 'Y-m-d H:i:s', + true + ], + ], + ], + // Error级别单独文件处理器 + [ + 'class' => FilterHandler::class, + 'constructor' => [ + new RotatingFileHandler( + runtime_path() . '/logs/error.log', + Config::getInstance()->errorLogRotationTimeByDay, + Logger::DEBUG + ), + Logger::ERROR, + Logger::EMERGENCY + ], + 'formatter' => [ + 'class' => Monolog\Formatter\LineFormatter::class, + 'constructor' => [ + "%datetime% [%thread%] %Level% %logger%:%L% - %message%\n", + 'Y-m-d H:i:s', + true + ], + ], + ], + ], + // 全局处理器:手动解析调用栈,精准定位业务代码 + 'processors' => [ + // 1. 注入进程ID(生成线程名) + new ProcessIdProcessor(), + // 2. 手动解析调用栈,获取实际业务代码位置 + 日志过滤 + function ($record) { + $message = $record['message']; + // 线程名 + $processId = $record['extra']['process'] ?? 0; + $record['thread'] = "thread-{$processId}"; + + // 手动解析调用栈,定位实际业务代码 + $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 10); // 获取调用栈(前10层) + $file = $record['channel']; + $function = ''; + $line = '0'; + + // 遍历调用栈,跳过日志类相关的层,找到业务代码层 + foreach ($trace as $step) { + // 跳过 webman 日志类、monolog 相关的调用层 + if (isset($step['file']) + && str_contains($step['file'], 'support') === false + && str_contains($step['file'], 'monolog') === false + && str_contains($step['file'], 'Logger') === false + ) { + // 获取文件路径(简化为项目内相对路径) + $file = $step['file']; + $projectRoot = base_path(); + if (str_starts_with($file, $projectRoot)) { + $file = substr($file, strlen($projectRoot) + 1); + } + $file = str_replace('.php', '', $file); + $file = str_replace('\\', '.', $file); + + // 获取方法名 + $function = $step['function']; + // 获取行 + $line = $step['line'] ?? '0'; + break; // 找到第一个业务代码层就停止 + } + } + + $record['M'] = $function; + $record['L'] = $line; + $record['P'] = $record['extra']['process_id']; + + // 解析 标签 + $pattern = '//si'; + if (preg_match_all($pattern, $message, $matches) && !empty($matches[1])) { + $record['logger'] = $matches[1][0]; + $record['message'] = preg_replace($pattern, '', $message); + if ($matches[1][0] == '__default') $record['logger'] = $file; + } else { + $record['logger'] = $file; + $record['message'] = $message; + } + + // 解析 SQL_LOG 相关标签 + $tagPatterns = [ + 'logger' => '/(.*?)<\/SQL_LOG_F>/s', // 文件路径 + 'L' => '/(.*?)<\/SQL_LOG_L>/s', // 行号 + 'M' => '/(.*?)<\/SQL_LOG_M>/s' // 方法名 + ]; + $tempMessage = $record['message']; // 基于处理后的message继续解析 + foreach ($tagPatterns as $key => $pattern) { + if (preg_match($pattern, $tempMessage, $matches)) { + $record[$key] = $matches[1]; + $tempMessage = preg_replace($pattern, '', $tempMessage); // 移除当前标签 + $record['message'] = trim($tempMessage); + } + } + + // 日志级别颜色映射 + $levelColorMap = [ + Logger::EMERGENCY => "\033[41m\033[37m", // 最高级别:红色底色+白色字体 + Logger::ALERT => "\033[41m\033[37m", // 警报:红色底色+白色字体 + Logger::CRITICAL => "\033[41m\033[37m", // 严重:红色底色+白色字体 + Logger::ERROR => "\033[31m", // 错误:红色字体(无底色) + Logger::WARNING => "\033[33m", // 警告:深黄色字体 + Logger::INFO => "\033[32m", // 信息:绿色字体 + Logger::DEBUG => "\033[34m", // 调试:蓝色字体 + ]; + $record['start'] = "\033[0m"; + $record['end'] = "\033[0m"; + foreach ($levelColorMap as $level => $color) { + if ($record['level'] >= $level) { + $record['start'] = $color; + $record['end'] = "\033[0m"; + break; + } + } + + $record['Level'] = str_pad(strtoupper($record['level_name']), 5); + return $record; + } + ], + 'ignore' => [ + app_path() . '/flow/config/AbstractConfig.php' + ], + ], +]; \ No newline at end of file diff --git a/config/middleware.php b/config/middleware.php new file mode 100644 index 0000000..8e964ed --- /dev/null +++ b/config/middleware.php @@ -0,0 +1,15 @@ + + * @copyright walkor + * @link http://www.workerman.net/ + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ + +return []; \ No newline at end of file diff --git a/config/plugin/webman/console/app.php b/config/plugin/webman/console/app.php new file mode 100644 index 0000000..d7bf968 --- /dev/null +++ b/config/plugin/webman/console/app.php @@ -0,0 +1,28 @@ + true, + + 'build_dir' => BASE_PATH . DIRECTORY_SEPARATOR . 'build', + + 'phar_filename' => 'webman.phar', + + 'phar_format' => Phar::PHAR, // Phar archive format: Phar::PHAR, Phar::TAR, Phar::ZIP + + 'phar_compression' => Phar::NONE, // Compression method for Phar archive: Phar::NONE, Phar::GZ, Phar::BZ2 + + 'bin_filename' => 'webman.bin', + + 'signature_algorithm'=> Phar::SHA256, //set the signature algorithm for a phar and apply it. The signature algorithm must be one of Phar::MD5, Phar::SHA1, Phar::SHA256, Phar::SHA512, or Phar::OPENSSL. + + 'private_key_file' => '', // The file path for certificate or OpenSSL private key file. + + 'exclude_pattern' => '#^(?!.*(composer.json|/.github/|/.idea/|/.git/|/.setting/|/runtime/|/vendor-bin/|/build/|/vendor/webman/admin/))(.*)$#', + + 'exclude_files' => [ + '.env', 'LICENSE', 'composer.json', 'composer.lock', 'start.php', 'webman.phar', 'webman.bin' + ], + + 'custom_ini' => ' +memory_limit = 256M + ', +]; diff --git a/config/process.php b/config/process.php new file mode 100644 index 0000000..3b253aa --- /dev/null +++ b/config/process.php @@ -0,0 +1,70 @@ + + * @copyright walkor + * @link http://www.workerman.net/ + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ + +use app\config\Config; +use support\Log; +use support\Request; +use app\process\Http; + +global $argv; + +return [ + 'webman' => [ + 'handler' => Http::class, + 'listen' => 'http://0.0.0.0:8787', +// 'count' => cpu_count() * 4, + 'count' => 1, + 'user' => '', + 'group' => '', + 'reusePort' => false, + 'eventLoop' => '', + 'context' => [], + 'constructor' => [ + 'requestClass' => Request::class, + 'logger' => Log::channel('default'), + 'appPath' => app_path(), + 'publicPath' => public_path() + ] + ], + // File update detection and automatic reload + 'monitor' => [ + 'handler' => app\process\Monitor::class, + 'reloadable' => false, + 'constructor' => [ + // Monitor these directories + 'monitorDir' => array_merge([ + app_path(), + config_path(), + base_path() . '/process', + base_path() . '/support', + base_path() . '/resource', + base_path() . '/.env', + ], glob(base_path() . '/plugin/*/app'), glob(base_path() . '/plugin/*/config'), glob(base_path() . '/plugin/*/api')), + // Files with these suffixes will be monitored + 'monitorExtensions' => [ + 'php', 'html', 'htm', 'env' + ], + 'options' => [ + 'enable_file_monitor' => !in_array('-d', $argv) && DIRECTORY_SEPARATOR === '/', + 'enable_memory_monitor' => DIRECTORY_SEPARATOR === '/', + ] + ] + ], + 'TcpServer' => [ + 'handler' => app\process\TcpServer::class, + 'listen' => 'tcp://0.0.0.0:'. Config::getInstance()->tcpServerPort, + 'count' => Config::getInstance()->tcpServerProcessNum, + 'reusePort' => true, + ], +]; diff --git a/config/route.php b/config/route.php new file mode 100644 index 0000000..a5064fc --- /dev/null +++ b/config/route.php @@ -0,0 +1,21 @@ + + * @copyright walkor + * @link http://www.workerman.net/ + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ + +use Webman\Route; + + + + + + diff --git a/config/server.php b/config/server.php new file mode 100644 index 0000000..054d01f --- /dev/null +++ b/config/server.php @@ -0,0 +1,23 @@ + + * @copyright walkor + * @link http://www.workerman.net/ + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ + +return [ + 'event_loop' => '', + 'stop_timeout' => 2, + 'pid_file' => runtime_path() . '/webman.pid', + 'status_file' => runtime_path() . '/webman.status', + 'stdout_file' => runtime_path() . '/logs/stdout.log', + 'log_file' => runtime_path() . '/logs/workerman.log', + 'max_package_size' => 10 * 1024 * 1024 +]; diff --git a/config/session.php b/config/session.php new file mode 100644 index 0000000..043f8c4 --- /dev/null +++ b/config/session.php @@ -0,0 +1,65 @@ + + * @copyright walkor + * @link http://www.workerman.net/ + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ + +use Webman\Session\FileSessionHandler; +use Webman\Session\RedisSessionHandler; +use Webman\Session\RedisClusterSessionHandler; + +return [ + + 'type' => 'file', // or redis or redis_cluster + + 'handler' => FileSessionHandler::class, + + 'config' => [ + 'file' => [ + 'save_path' => runtime_path() . '/sessions', + ], + 'redis' => [ + 'host' => '127.0.0.1', + 'port' => 6379, + 'auth' => '', + 'timeout' => 2, + 'database' => '', + 'prefix' => 'redis_session_', + ], + 'redis_cluster' => [ + 'host' => ['127.0.0.1:7000', '127.0.0.1:7001', '127.0.0.1:7001'], + 'timeout' => 2, + 'auth' => '', + 'prefix' => 'redis_session_', + ] + ], + + 'session_name' => 'PHPSID', + + 'auto_update_timestamp' => false, + + 'lifetime' => 7*24*60*60, + + 'cookie_lifetime' => 365*24*60*60, + + 'cookie_path' => '/', + + 'domain' => '', + + 'http_only' => true, + + 'secure' => false, + + 'same_site' => '', + + 'gc_probability' => [1, 1000], + +]; diff --git a/config/static.php b/config/static.php new file mode 100644 index 0000000..6313679 --- /dev/null +++ b/config/static.php @@ -0,0 +1,23 @@ + + * @copyright walkor + * @link http://www.workerman.net/ + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ + +/** + * Static file settings + */ +return [ + 'enable' => true, + 'middleware' => [ // Static file Middleware + //app\middleware\StaticFile::class, + ], +]; \ No newline at end of file diff --git a/config/think-orm.php b/config/think-orm.php new file mode 100644 index 0000000..479ff90 --- /dev/null +++ b/config/think-orm.php @@ -0,0 +1,42 @@ + 'mysql', + 'connections' => [ + 'mysql' => [ + // 数据库类型 + 'type' => 'mysql', + // 服务器地址 + 'hostname' => '127.0.0.1', + // 数据库名 + 'database' => 'test', + // 数据库用户名 + 'username' => 'root', + // 数据库密码 + 'password' => '123456', + // 数据库连接端口 + 'hostport' => '3306', + // 数据库连接参数 + 'params' => [ + // 连接超时3秒 + \PDO::ATTR_TIMEOUT => 3, + ], + // 数据库编码默认采用utf8 + 'charset' => 'utf8', + // 数据库表前缀 + 'prefix' => '', + // 断线重连 + 'break_reconnect' => true, + // 连接池配置 + 'pool' => [ + 'max_connections' => 5, // 最大连接数 + 'min_connections' => 1, // 最小连接数 + 'wait_timeout' => 3, // 从连接池获取连接等待超时时间 + 'idle_timeout' => 60, // 连接最大空闲时间,超过该时间会被回收 + 'heartbeat_interval' => 50, // 心跳检测间隔,需要小于60秒 + ], + ], + ], + // 自定义分页类 + 'paginator' => '', +]; diff --git a/config/translation.php b/config/translation.php new file mode 100644 index 0000000..96589b2 --- /dev/null +++ b/config/translation.php @@ -0,0 +1,25 @@ + + * @copyright walkor + * @link http://www.workerman.net/ + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ + +/** + * Multilingual configuration + */ +return [ + // Default language + 'locale' => 'zh_CN', + // Fallback language + 'fallback_locale' => ['zh_CN', 'en'], + // Folder where language files are stored + 'path' => base_path() . '/resource/translations', +]; \ No newline at end of file diff --git a/config/view.php b/config/view.php new file mode 100644 index 0000000..e3a7b85 --- /dev/null +++ b/config/view.php @@ -0,0 +1,22 @@ + + * @copyright walkor + * @link http://www.workerman.net/ + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ + +use support\view\Raw; +use support\view\Twig; +use support\view\Blade; +use support\view\ThinkPHP; + +return [ + 'handler' => Raw::class +]; diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..352871e --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,11 @@ +version: "3" +services: + webman: + build: . + container_name: docker-webman + restart: unless-stopped + volumes: + - "./:/app" + ports: + - "8787:8787" + command: ["php", "start.php", "start" ] \ No newline at end of file diff --git a/phpunit-ide.php b/phpunit-ide.php new file mode 100644 index 0000000..d6ac7fa --- /dev/null +++ b/phpunit-ide.php @@ -0,0 +1,37 @@ + + + + + ./tests + + + + + ./app + + + \ No newline at end of file diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..7e47604 --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,25 @@ + + + + + tests/flow + + + + + app/flow + + + + + + + + + diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000..b9f722e Binary files /dev/null and b/public/favicon.ico differ diff --git a/start.php b/start.php new file mode 100644 index 0000000..41ad7ef --- /dev/null +++ b/start.php @@ -0,0 +1,5 @@ +#!/usr/bin/env php + + * @copyright walkor + * @link http://www.workerman.net/ + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ + +namespace support; + +/** + * Class Request + * @package support + */ +class Request extends \Webman\Http\Request +{ + +} \ No newline at end of file diff --git a/support/Response.php b/support/Response.php new file mode 100644 index 0000000..9bc4e1e --- /dev/null +++ b/support/Response.php @@ -0,0 +1,24 @@ + + * @copyright walkor + * @link http://www.workerman.net/ + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ + +namespace support; + +/** + * Class Response + * @package support + */ +class Response extends \Webman\Http\Response +{ + +} \ No newline at end of file diff --git a/support/Setup.php b/support/Setup.php new file mode 100644 index 0000000..99320e8 --- /dev/null +++ b/support/Setup.php @@ -0,0 +1,1558 @@ + \DateTimeZone::ASIA, + 'Europe' => \DateTimeZone::EUROPE, + 'America' => \DateTimeZone::AMERICA, + 'Africa' => \DateTimeZone::AFRICA, + 'Australia' => \DateTimeZone::AUSTRALIA, + 'Pacific' => \DateTimeZone::PACIFIC, + 'Atlantic' => \DateTimeZone::ATLANTIC, + 'Indian' => \DateTimeZone::INDIAN, + 'Antarctica' => \DateTimeZone::ANTARCTICA, + 'Arctic' => \DateTimeZone::ARCTIC, + 'UTC' => \DateTimeZone::UTC, + ]; + + // --- Locale => default timezone --- + + private const LOCALE_DEFAULT_TIMEZONES = [ + 'zh_CN' => 'Asia/Shanghai', + 'zh_TW' => 'Asia/Taipei', + 'en' => 'UTC', + 'ja' => 'Asia/Tokyo', + 'ko' => 'Asia/Seoul', + 'fr' => 'Europe/Paris', + 'de' => 'Europe/Berlin', + 'es' => 'Europe/Madrid', + 'pt_BR' => 'America/Sao_Paulo', + 'ru' => 'Europe/Moscow', + 'vi' => 'Asia/Ho_Chi_Minh', + 'tr' => 'Europe/Istanbul', + 'id' => 'Asia/Jakarta', + 'th' => 'Asia/Bangkok', + ]; + + // --- Locale options (localized display names) --- + + private const LOCALE_LABELS = [ + 'zh_CN' => '简体中文', + 'zh_TW' => '繁體中文', + 'en' => 'English', + 'ja' => '日本語', + 'ko' => '한국어', + 'fr' => 'Français', + 'de' => 'Deutsch', + 'es' => 'Español', + 'pt_BR' => 'Português (Brasil)', + 'ru' => 'Русский', + 'vi' => 'Tiếng Việt', + 'tr' => 'Türkçe', + 'id' => 'Bahasa Indonesia', + 'th' => 'ไทย', + ]; + + // --- Multilingual messages (%s = placeholder) --- + + private const MESSAGES = [ + 'zh_CN' => [ + 'remove_package_question' => '发现以下已安装组件本次未选择,是否将其卸载 ?%s', + 'removing_package' => '- 准备移除组件 %s', + 'removing' => '卸载:', + 'error_remove' => '卸载组件出错,请手动执行:composer remove %s', + 'done_remove' => '已卸载组件。', + 'skip' => '非交互模式,跳过安装向导。', + 'default_choice' => ' (默认 %s)', + 'timezone_prompt' => '时区 (默认 %s,输入可联想补全): ', + 'timezone_title' => '时区设置 (默认 %s)', + 'timezone_help' => '输入关键字Tab自动补全,可↑↓下选择:', + 'timezone_region' => '选择时区区域', + 'timezone_city' => '选择时区', + 'timezone_invalid' => '无效的时区,已使用默认值 %s', + 'timezone_input_prompt' => '输入时区或关键字:', + 'timezone_pick_prompt' => '请输入数字编号或关键字:', + 'timezone_no_match' => '未找到匹配的时区,请重试。', + 'timezone_invalid_index' => '无效的编号,请重新输入。', + 'yes' => '是', + 'no' => '否', + 'adding_package' => '- 添加依赖 %s', + 'console_question' => '安装命令行组件 webman/console', + 'db_question' => '数据库组件', + 'db_none' => '不安装', + 'db_invalid' => '请输入有效选项', + 'redis_question' => '安装 Redis 组件 webman/redis', + 'events_note' => ' (Redis 依赖 illuminate/events,已自动包含)', + 'validation_question' => '安装验证器组件 webman/validation', + 'template_question' => '模板引擎', + 'template_none' => '不安装', + 'no_components' => '未选择额外组件。', + 'installing' => '即将安装:', + 'running' => '执行:', + 'error_install' => '安装可选组件时出错,请手动执行:composer require %s', + 'done' => '可选组件安装完成。', + 'summary_locale' => '语言:%s', + 'summary_timezone' => '时区:%s', + ], + 'zh_TW' => [ + 'skip' => '非交互模式,跳過安裝嚮導。', + 'default_choice' => ' (預設 %s)', + 'timezone_prompt' => '時區 (預設 %s,輸入可聯想補全): ', + 'timezone_title' => '時區設定 (預設 %s)', + 'timezone_help' => '輸入關鍵字Tab自動補全,可↑↓上下選擇:', + 'timezone_region' => '選擇時區區域', + 'timezone_city' => '選擇時區', + 'timezone_invalid' => '無效的時區,已使用預設值 %s', + 'timezone_input_prompt' => '輸入時區或關鍵字:', + 'timezone_pick_prompt' => '請輸入數字編號或關鍵字:', + 'timezone_no_match' => '未找到匹配的時區,請重試。', + 'timezone_invalid_index' => '無效的編號,請重新輸入。', + 'yes' => '是', + 'no' => '否', + 'adding_package' => '- 新增依賴 %s', + 'console_question' => '安裝命令列組件 webman/console', + 'db_question' => '資料庫組件', + 'db_none' => '不安裝', + 'db_invalid' => '請輸入有效選項', + 'redis_question' => '安裝 Redis 組件 webman/redis', + 'events_note' => ' (Redis 依賴 illuminate/events,已自動包含)', + 'validation_question' => '安裝驗證器組件 webman/validation', + 'template_question' => '模板引擎', + 'template_none' => '不安裝', + 'no_components' => '未選擇額外組件。', + 'installing' => '即將安裝:', + 'running' => '執行:', + 'error_install' => '安裝可選組件時出錯,請手動執行:composer require %s', + 'done' => '可選組件安裝完成。', + 'summary_locale' => '語言:%s', + 'summary_timezone' => '時區:%s', + ], + 'en' => [ + 'skip' => 'Non-interactive mode, skipping setup wizard.', + 'default_choice' => ' (default %s)', + 'timezone_prompt' => 'Timezone (default=%s, type to autocomplete): ', + 'timezone_title' => 'Timezone (default=%s)', + 'timezone_help' => 'Type keyword then press Tab to autocomplete, use ↑↓ to choose:', + 'timezone_region' => 'Select timezone region', + 'timezone_city' => 'Select timezone', + 'timezone_invalid' => 'Invalid timezone, using default %s', + 'timezone_input_prompt' => 'Enter timezone or keyword:', + 'timezone_pick_prompt' => 'Enter number or keyword:', + 'timezone_no_match' => 'No matching timezone found, please try again.', + 'timezone_invalid_index' => 'Invalid number, please try again.', + 'yes' => 'yes', + 'no' => 'no', + 'adding_package' => '- Adding package %s', + 'console_question' => 'Install console component webman/console', + 'db_question' => 'Database component', + 'db_none' => 'None', + 'db_invalid' => 'Please enter a valid option', + 'redis_question' => 'Install Redis component webman/redis', + 'events_note' => ' (Redis requires illuminate/events, automatically included)', + 'validation_question' => 'Install validator component webman/validation', + 'template_question' => 'Template engine', + 'template_none' => 'None', + 'no_components' => 'No optional components selected.', + 'installing' => 'Installing:', + 'running' => 'Running:', + 'error_install' => 'Failed to install. Try manually: composer require %s', + 'done' => 'Optional components installed.', + 'summary_locale' => 'Language: %s', + 'summary_timezone' => 'Timezone: %s', + ], + 'ja' => [ + 'skip' => '非対話モードのため、セットアップウィザードをスキップします。', + 'default_choice' => ' (デフォルト %s)', + 'timezone_prompt' => 'タイムゾーン (デフォルト=%s、入力で補完): ', + 'timezone_title' => 'タイムゾーン (デフォルト=%s)', + 'timezone_help' => 'キーワード入力→Tabで補完、↑↓で選択:', + 'timezone_region' => 'タイムゾーンの地域を選択', + 'timezone_city' => 'タイムゾーンを選択', + 'timezone_invalid' => '無効なタイムゾーンです。デフォルト %s を使用します', + 'timezone_input_prompt' => 'タイムゾーンまたはキーワードを入力:', + 'timezone_pick_prompt' => '番号またはキーワードを入力:', + 'timezone_no_match' => '一致するタイムゾーンが見つかりません。再試行してください。', + 'timezone_invalid_index' => '無効な番号です。もう一度入力してください。', + 'yes' => 'はい', + 'no' => 'いいえ', + 'adding_package' => '- パッケージを追加 %s', + 'console_question' => 'コンソールコンポーネント webman/console をインストール', + 'db_question' => 'データベースコンポーネント', + 'db_none' => 'インストールしない', + 'db_invalid' => '有効なオプションを入力してください', + 'redis_question' => 'Redis コンポーネント webman/redis をインストール', + 'events_note' => ' (Redis は illuminate/events が必要です。自動的に含まれます)', + 'validation_question' => 'バリデーションコンポーネント webman/validation をインストール', + 'template_question' => 'テンプレートエンジン', + 'template_none' => 'インストールしない', + 'no_components' => 'オプションコンポーネントが選択されていません。', + 'installing' => 'インストール中:', + 'running' => '実行中:', + 'error_install' => 'インストールに失敗しました。手動で実行してください:composer require %s', + 'done' => 'オプションコンポーネントのインストールが完了しました。', + 'summary_locale' => '言語:%s', + 'summary_timezone' => 'タイムゾーン:%s', + ], + 'ko' => [ + 'skip' => '비대화형 모드입니다. 설치 마법사를 건너뜁니다.', + 'default_choice' => ' (기본값 %s)', + 'timezone_prompt' => '시간대 (기본값=%s, 입력하여 자동완성): ', + 'timezone_title' => '시간대 (기본값=%s)', + 'timezone_help' => '키워드 입력 후 Tab 자동완성, ↑↓로 선택:', + 'timezone_region' => '시간대 지역 선택', + 'timezone_city' => '시간대 선택', + 'timezone_invalid' => '잘못된 시간대입니다. 기본값 %s 을(를) 사용합니다', + 'timezone_input_prompt' => '시간대 또는 키워드 입력:', + 'timezone_pick_prompt' => '번호 또는 키워드 입력:', + 'timezone_no_match' => '일치하는 시간대를 찾을 수 없습니다. 다시 시도하세요.', + 'timezone_invalid_index' => '잘못된 번호입니다. 다시 입력하세요.', + 'yes' => '예', + 'no' => '아니오', + 'adding_package' => '- 패키지 추가 %s', + 'console_question' => '콘솔 컴포넌트 webman/console 설치', + 'db_question' => '데이터베이스 컴포넌트', + 'db_none' => '설치 안 함', + 'db_invalid' => '유효한 옵션을 입력하세요', + 'redis_question' => 'Redis 컴포넌트 webman/redis 설치', + 'events_note' => ' (Redis는 illuminate/events가 필요합니다. 자동으로 포함됩니다)', + 'validation_question' => '검증 컴포넌트 webman/validation 설치', + 'template_question' => '템플릿 엔진', + 'template_none' => '설치 안 함', + 'no_components' => '선택된 추가 컴포넌트가 없습니다.', + 'installing' => '설치 예정:', + 'running' => '실행 중:', + 'error_install' => '설치에 실패했습니다. 수동으로 실행하세요: composer require %s', + 'done' => '선택 컴포넌트 설치가 완료되었습니다.', + 'summary_locale' => '언어: %s', + 'summary_timezone' => '시간대: %s', + ], + 'fr' => [ + 'skip' => 'Mode non interactif, assistant d\'installation ignoré.', + 'default_choice' => ' (défaut %s)', + 'timezone_prompt' => 'Fuseau horaire (défaut=%s, tapez pour compléter) : ', + 'timezone_title' => 'Fuseau horaire (défaut=%s)', + 'timezone_help' => 'Tapez un mot-clé, Tab pour compléter, ↑↓ pour choisir :', + 'timezone_region' => 'Sélectionnez la région du fuseau horaire', + 'timezone_city' => 'Sélectionnez le fuseau horaire', + 'timezone_invalid' => 'Fuseau horaire invalide, utilisation de %s par défaut', + 'timezone_input_prompt' => 'Entrez un fuseau horaire ou un mot-clé :', + 'timezone_pick_prompt' => 'Entrez un numéro ou un mot-clé :', + 'timezone_no_match' => 'Aucun fuseau horaire correspondant, veuillez réessayer.', + 'timezone_invalid_index' => 'Numéro invalide, veuillez réessayer.', + 'yes' => 'oui', + 'no' => 'non', + 'adding_package' => '- Ajout du paquet %s', + 'console_question' => 'Installer le composant console webman/console', + 'db_question' => 'Composant base de données', + 'db_none' => 'Aucun', + 'db_invalid' => 'Veuillez entrer une option valide', + 'redis_question' => 'Installer le composant Redis webman/redis', + 'events_note' => ' (Redis nécessite illuminate/events, inclus automatiquement)', + 'validation_question' => 'Installer le composant de validation webman/validation', + 'template_question' => 'Moteur de templates', + 'template_none' => 'Aucun', + 'no_components' => 'Aucun composant optionnel sélectionné.', + 'installing' => 'Installation en cours :', + 'running' => 'Exécution :', + 'error_install' => 'Échec de l\'installation. Essayez manuellement : composer require %s', + 'done' => 'Composants optionnels installés.', + 'summary_locale' => 'Langue : %s', + 'summary_timezone' => 'Fuseau horaire : %s', + ], + 'de' => [ + 'skip' => 'Nicht-interaktiver Modus, Einrichtungsassistent übersprungen.', + 'default_choice' => ' (Standard %s)', + 'timezone_prompt' => 'Zeitzone (Standard=%s, Eingabe zur Vervollständigung): ', + 'timezone_title' => 'Zeitzone (Standard=%s)', + 'timezone_help' => 'Stichwort tippen, Tab ergänzt, ↑↓ auswählen:', + 'timezone_region' => 'Zeitzone Region auswählen', + 'timezone_city' => 'Zeitzone auswählen', + 'timezone_invalid' => 'Ungültige Zeitzone, Standardwert %s wird verwendet', + 'timezone_input_prompt' => 'Zeitzone oder Stichwort eingeben:', + 'timezone_pick_prompt' => 'Nummer oder Stichwort eingeben:', + 'timezone_no_match' => 'Keine passende Zeitzone gefunden, bitte erneut versuchen.', + 'timezone_invalid_index' => 'Ungültige Nummer, bitte erneut eingeben.', + 'yes' => 'ja', + 'no' => 'nein', + 'adding_package' => '- Paket hinzufügen %s', + 'console_question' => 'Konsolen-Komponente webman/console installieren', + 'db_question' => 'Datenbank-Komponente', + 'db_none' => 'Keine', + 'db_invalid' => 'Bitte geben Sie eine gültige Option ein', + 'redis_question' => 'Redis-Komponente webman/redis installieren', + 'events_note' => ' (Redis benötigt illuminate/events, automatisch eingeschlossen)', + 'validation_question' => 'Validierungs-Komponente webman/validation installieren', + 'template_question' => 'Template-Engine', + 'template_none' => 'Keine', + 'no_components' => 'Keine optionalen Komponenten ausgewählt.', + 'installing' => 'Installation:', + 'running' => 'Ausführung:', + 'error_install' => 'Installation fehlgeschlagen. Manuell ausführen: composer require %s', + 'done' => 'Optionale Komponenten installiert.', + 'summary_locale' => 'Sprache: %s', + 'summary_timezone' => 'Zeitzone: %s', + ], + 'es' => [ + 'skip' => 'Modo no interactivo, asistente de instalación omitido.', + 'default_choice' => ' (predeterminado %s)', + 'timezone_prompt' => 'Zona horaria (predeterminado=%s, escriba para autocompletar): ', + 'timezone_title' => 'Zona horaria (predeterminado=%s)', + 'timezone_help' => 'Escriba una palabra clave, Tab autocompleta, use ↑↓ para elegir:', + 'timezone_region' => 'Seleccione la región de zona horaria', + 'timezone_city' => 'Seleccione la zona horaria', + 'timezone_invalid' => 'Zona horaria inválida, usando valor predeterminado %s', + 'timezone_input_prompt' => 'Ingrese zona horaria o palabra clave:', + 'timezone_pick_prompt' => 'Ingrese número o palabra clave:', + 'timezone_no_match' => 'No se encontró zona horaria coincidente, intente de nuevo.', + 'timezone_invalid_index' => 'Número inválido, intente de nuevo.', + 'yes' => 'sí', + 'no' => 'no', + 'adding_package' => '- Agregando paquete %s', + 'console_question' => 'Instalar componente de consola webman/console', + 'db_question' => 'Componente de base de datos', + 'db_none' => 'Ninguno', + 'db_invalid' => 'Por favor ingrese una opción válida', + 'redis_question' => 'Instalar componente Redis webman/redis', + 'events_note' => ' (Redis requiere illuminate/events, incluido automáticamente)', + 'validation_question' => 'Instalar componente de validación webman/validation', + 'template_question' => 'Motor de plantillas', + 'template_none' => 'Ninguno', + 'no_components' => 'No se seleccionaron componentes opcionales.', + 'installing' => 'Instalando:', + 'running' => 'Ejecutando:', + 'error_install' => 'Error en la instalación. Intente manualmente: composer require %s', + 'done' => 'Componentes opcionales instalados.', + 'summary_locale' => 'Idioma: %s', + 'summary_timezone' => 'Zona horaria: %s', + ], + 'pt_BR' => [ + 'skip' => 'Modo não interativo, assistente de instalação ignorado.', + 'default_choice' => ' (padrão %s)', + 'timezone_prompt' => 'Fuso horário (padrão=%s, digite para autocompletar): ', + 'timezone_title' => 'Fuso horário (padrão=%s)', + 'timezone_help' => 'Digite uma palavra-chave, Tab autocompleta, use ↑↓ para escolher:', + 'timezone_region' => 'Selecione a região do fuso horário', + 'timezone_city' => 'Selecione o fuso horário', + 'timezone_invalid' => 'Fuso horário inválido, usando padrão %s', + 'timezone_input_prompt' => 'Digite fuso horário ou palavra-chave:', + 'timezone_pick_prompt' => 'Digite número ou palavra-chave:', + 'timezone_no_match' => 'Nenhum fuso horário encontrado, tente novamente.', + 'timezone_invalid_index' => 'Número inválido, tente novamente.', + 'yes' => 'sim', + 'no' => 'não', + 'adding_package' => '- Adicionando pacote %s', + 'console_question' => 'Instalar componente de console webman/console', + 'db_question' => 'Componente de banco de dados', + 'db_none' => 'Nenhum', + 'db_invalid' => 'Por favor, digite uma opção válida', + 'redis_question' => 'Instalar componente Redis webman/redis', + 'events_note' => ' (Redis requer illuminate/events, incluído automaticamente)', + 'validation_question' => 'Instalar componente de validação webman/validation', + 'template_question' => 'Motor de templates', + 'template_none' => 'Nenhum', + 'no_components' => 'Nenhum componente opcional selecionado.', + 'installing' => 'Instalando:', + 'running' => 'Executando:', + 'error_install' => 'Falha na instalação. Tente manualmente: composer require %s', + 'done' => 'Componentes opcionais instalados.', + 'summary_locale' => 'Idioma: %s', + 'summary_timezone' => 'Fuso horário: %s', + ], + 'ru' => [ + 'skip' => 'Неинтерактивный режим, мастер установки пропущен.', + 'default_choice' => ' (по умолчанию %s)', + 'timezone_prompt' => 'Часовой пояс (по умолчанию=%s, введите для автодополнения): ', + 'timezone_title' => 'Часовой пояс (по умолчанию=%s)', + 'timezone_help' => 'Введите ключевое слово, Tab для автодополнения, ↑↓ для выбора:', + 'timezone_region' => 'Выберите регион часового пояса', + 'timezone_city' => 'Выберите часовой пояс', + 'timezone_invalid' => 'Неверный часовой пояс, используется значение по умолчанию %s', + 'timezone_input_prompt' => 'Введите часовой пояс или ключевое слово:', + 'timezone_pick_prompt' => 'Введите номер или ключевое слово:', + 'timezone_no_match' => 'Совпадающий часовой пояс не найден, попробуйте снова.', + 'timezone_invalid_index' => 'Неверный номер, попробуйте снова.', + 'yes' => 'да', + 'no' => 'нет', + 'adding_package' => '- Добавление пакета %s', + 'console_question' => 'Установить консольный компонент webman/console', + 'db_question' => 'Компонент базы данных', + 'db_none' => 'Не устанавливать', + 'db_invalid' => 'Пожалуйста, введите допустимый вариант', + 'redis_question' => 'Установить компонент Redis webman/redis', + 'events_note' => ' (Redis требует illuminate/events, автоматически включён)', + 'validation_question' => 'Установить компонент валидации webman/validation', + 'template_question' => 'Шаблонизатор', + 'template_none' => 'Не устанавливать', + 'no_components' => 'Дополнительные компоненты не выбраны.', + 'installing' => 'Установка:', + 'running' => 'Выполнение:', + 'error_install' => 'Ошибка установки. Выполните вручную: composer require %s', + 'done' => 'Дополнительные компоненты установлены.', + 'summary_locale' => 'Язык: %s', + 'summary_timezone' => 'Часовой пояс: %s', + ], + 'vi' => [ + 'skip' => 'Chế độ không tương tác, bỏ qua trình hướng dẫn cài đặt.', + 'default_choice' => ' (mặc định %s)', + 'timezone_prompt' => 'Múi giờ (mặc định=%s, nhập để tự động hoàn thành): ', + 'timezone_title' => 'Múi giờ (mặc định=%s)', + 'timezone_help' => 'Nhập từ khóa, Tab để tự hoàn thành, dùng ↑↓ để chọn:', + 'timezone_region' => 'Chọn khu vực múi giờ', + 'timezone_city' => 'Chọn múi giờ', + 'timezone_invalid' => 'Múi giờ không hợp lệ, sử dụng mặc định %s', + 'timezone_input_prompt' => 'Nhập múi giờ hoặc từ khóa:', + 'timezone_pick_prompt' => 'Nhập số thứ tự hoặc từ khóa:', + 'timezone_no_match' => 'Không tìm thấy múi giờ phù hợp, vui lòng thử lại.', + 'timezone_invalid_index' => 'Số không hợp lệ, vui lòng thử lại.', + 'yes' => 'có', + 'no' => 'không', + 'adding_package' => '- Thêm gói %s', + 'console_question' => 'Cài đặt thành phần console webman/console', + 'db_question' => 'Thành phần cơ sở dữ liệu', + 'db_none' => 'Không cài đặt', + 'db_invalid' => 'Vui lòng nhập tùy chọn hợp lệ', + 'redis_question' => 'Cài đặt thành phần Redis webman/redis', + 'events_note' => ' (Redis cần illuminate/events, đã tự động bao gồm)', + 'validation_question' => 'Cài đặt thành phần xác thực webman/validation', + 'template_question' => 'Công cụ mẫu', + 'template_none' => 'Không cài đặt', + 'no_components' => 'Không có thành phần tùy chọn nào được chọn.', + 'installing' => 'Đang cài đặt:', + 'running' => 'Đang thực thi:', + 'error_install' => 'Cài đặt thất bại. Thử thủ công: composer require %s', + 'done' => 'Các thành phần tùy chọn đã được cài đặt.', + 'summary_locale' => 'Ngôn ngữ: %s', + 'summary_timezone' => 'Múi giờ: %s', + ], + 'tr' => [ + 'skip' => 'Etkileşimsiz mod, kurulum sihirbazı atlanıyor.', + 'default_choice' => ' (varsayılan %s)', + 'timezone_prompt' => 'Saat dilimi (varsayılan=%s, otomatik tamamlama için yazın): ', + 'timezone_title' => 'Saat dilimi (varsayılan=%s)', + 'timezone_help' => 'Anahtar kelime yazın, Tab tamamlar, ↑↓ ile seçin:', + 'timezone_region' => 'Saat dilimi bölgesini seçin', + 'timezone_city' => 'Saat dilimini seçin', + 'timezone_invalid' => 'Geçersiz saat dilimi, varsayılan %s kullanılıyor', + 'timezone_input_prompt' => 'Saat dilimi veya anahtar kelime girin:', + 'timezone_pick_prompt' => 'Numara veya anahtar kelime girin:', + 'timezone_no_match' => 'Eşleşen saat dilimi bulunamadı, tekrar deneyin.', + 'timezone_invalid_index' => 'Geçersiz numara, tekrar deneyin.', + 'yes' => 'evet', + 'no' => 'hayır', + 'adding_package' => '- Paket ekleniyor %s', + 'console_question' => 'Konsol bileşeni webman/console yüklensin mi', + 'db_question' => 'Veritabanı bileşeni', + 'db_none' => 'Yok', + 'db_invalid' => 'Lütfen geçerli bir seçenek girin', + 'redis_question' => 'Redis bileşeni webman/redis yüklensin mi', + 'events_note' => ' (Redis, illuminate/events gerektirir, otomatik olarak dahil edildi)', + 'validation_question' => 'Doğrulama bileşeni webman/validation yüklensin mi', + 'template_question' => 'Şablon motoru', + 'template_none' => 'Yok', + 'no_components' => 'İsteğe bağlı bileşen seçilmedi.', + 'installing' => 'Yükleniyor:', + 'running' => 'Çalıştırılıyor:', + 'error_install' => 'Yükleme başarısız. Manuel olarak deneyin: composer require %s', + 'done' => 'İsteğe bağlı bileşenler yüklendi.', + 'summary_locale' => 'Dil: %s', + 'summary_timezone' => 'Saat dilimi: %s', + ], + 'id' => [ + 'skip' => 'Mode non-interaktif, melewati wizard instalasi.', + 'default_choice' => ' (default %s)', + 'timezone_prompt' => 'Zona waktu (default=%s, ketik untuk melengkapi): ', + 'timezone_title' => 'Zona waktu (default=%s)', + 'timezone_help' => 'Ketik kata kunci, Tab untuk melengkapi, gunakan ↑↓ untuk memilih:', + 'timezone_region' => 'Pilih wilayah zona waktu', + 'timezone_city' => 'Pilih zona waktu', + 'timezone_invalid' => 'Zona waktu tidak valid, menggunakan default %s', + 'timezone_input_prompt' => 'Masukkan zona waktu atau kata kunci:', + 'timezone_pick_prompt' => 'Masukkan nomor atau kata kunci:', + 'timezone_no_match' => 'Zona waktu tidak ditemukan, silakan coba lagi.', + 'timezone_invalid_index' => 'Nomor tidak valid, silakan coba lagi.', + 'yes' => 'ya', + 'no' => 'tidak', + 'adding_package' => '- Menambahkan paket %s', + 'console_question' => 'Instal komponen konsol webman/console', + 'db_question' => 'Komponen database', + 'db_none' => 'Tidak ada', + 'db_invalid' => 'Silakan masukkan opsi yang valid', + 'redis_question' => 'Instal komponen Redis webman/redis', + 'events_note' => ' (Redis memerlukan illuminate/events, otomatis disertakan)', + 'validation_question' => 'Instal komponen validasi webman/validation', + 'template_question' => 'Mesin template', + 'template_none' => 'Tidak ada', + 'no_components' => 'Tidak ada komponen opsional yang dipilih.', + 'installing' => 'Menginstal:', + 'running' => 'Menjalankan:', + 'error_install' => 'Instalasi gagal. Coba manual: composer require %s', + 'done' => 'Komponen opsional terinstal.', + 'summary_locale' => 'Bahasa: %s', + 'summary_timezone' => 'Zona waktu: %s', + ], + 'th' => [ + 'skip' => 'โหมดไม่โต้ตอบ ข้ามตัวช่วยติดตั้ง', + 'default_choice' => ' (ค่าเริ่มต้น %s)', + 'timezone_prompt' => 'เขตเวลา (ค่าเริ่มต้น=%s พิมพ์เพื่อเติมอัตโนมัติ): ', + 'timezone_title' => 'เขตเวลา (ค่าเริ่มต้น=%s)', + 'timezone_help' => 'พิมพ์คีย์เวิร์ดแล้วกด Tab เพื่อเติมอัตโนมัติ ใช้ ↑↓ เพื่อเลือก:', + 'timezone_region' => 'เลือกภูมิภาคเขตเวลา', + 'timezone_city' => 'เลือกเขตเวลา', + 'timezone_invalid' => 'เขตเวลาไม่ถูกต้อง ใช้ค่าเริ่มต้น %s', + 'timezone_input_prompt' => 'ป้อนเขตเวลาหรือคำค้น:', + 'timezone_pick_prompt' => 'ป้อนหมายเลขหรือคำค้น:', + 'timezone_no_match' => 'ไม่พบเขตเวลาที่ตรงกัน กรุณาลองอีกครั้ง', + 'timezone_invalid_index' => 'หมายเลขไม่ถูกต้อง กรุณาลองอีกครั้ง', + 'yes' => 'ใช่', + 'no' => 'ไม่', + 'adding_package' => '- เพิ่มแพ็กเกจ %s', + 'console_question' => 'ติดตั้งคอมโพเนนต์คอนโซล webman/console', + 'db_question' => 'คอมโพเนนต์ฐานข้อมูล', + 'db_none' => 'ไม่ติดตั้ง', + 'db_invalid' => 'กรุณาป้อนตัวเลือกที่ถูกต้อง', + 'redis_question' => 'ติดตั้งคอมโพเนนต์ Redis webman/redis', + 'events_note' => ' (Redis ต้องการ illuminate/events รวมไว้โดยอัตโนมัติ)', + 'validation_question' => 'ติดตั้งคอมโพเนนต์ตรวจสอบ webman/validation', + 'template_question' => 'เทมเพลตเอนจิน', + 'template_none' => 'ไม่ติดตั้ง', + 'no_components' => 'ไม่ได้เลือกคอมโพเนนต์เสริม', + 'installing' => 'กำลังติดตั้ง:', + 'running' => 'กำลังดำเนินการ:', + 'error_install' => 'ติดตั้งล้มเหลว ลองด้วยตนเอง: composer require %s', + 'done' => 'คอมโพเนนต์เสริมติดตั้งเรียบร้อยแล้ว', + 'summary_locale' => 'ภาษา: %s', + 'summary_timezone' => 'เขตเวลา: %s', + ], + ]; + + // --- Interrupt message (Ctrl+C) --- + + private const INTERRUPTED_MESSAGES = [ + 'zh_CN' => '安装中断,可运行 composer setup-webman 可重新设置。', + 'zh_TW' => '安裝中斷,可運行 composer setup-webman 重新設置。', + 'en' => 'Setup interrupted. Run "composer setup-webman" to restart setup.', + 'ja' => 'セットアップが中断されました。composer setup-webman を実行して再設定できます。', + 'ko' => '설치가 중단되었습니다. composer setup-webman 을 실행하여 다시 설정할 수 있습니다.', + 'fr' => 'Installation interrompue. Exécutez « composer setup-webman » pour recommencer.', + 'de' => 'Einrichtung abgebrochen. Führen Sie "composer setup-webman" aus, um neu zu starten.', + 'es' => 'Instalación interrumpida. Ejecute "composer setup-webman" para reiniciar.', + 'pt_BR' => 'Instalação interrompida. Execute "composer setup-webman" para reiniciar.', + 'ru' => 'Установка прервана. Выполните «composer setup-webman» для повторной настройки.', + 'vi' => 'Cài đặt bị gián đoạn. Chạy "composer setup-webman" để cài đặt lại.', + 'tr' => 'Kurulum kesildi. Yeniden kurmak için "composer setup-webman" komutunu çalıştırın.', + 'id' => 'Instalasi terganggu. Jalankan "composer setup-webman" untuk mengatur ulang.', + 'th' => 'การติดตั้งถูกขัดจังหวะ เรียกใช้ "composer setup-webman" เพื่อตั้งค่าใหม่', + ]; + + // --- Signal handling state --- + + /** @var string|null Saved stty mode for terminal restoration on interrupt */ + private static ?string $sttyMode = null; + + /** @var string Current locale for interrupt message */ + private static string $interruptLocale = 'en'; + + // ═══════════════════════════════════════════════════════════════ + // Entry + // ═══════════════════════════════════════════════════════════════ + + public static function run(Event $event): void + { + $io = $event->getIO(); + + // Non-interactive mode: use English for skip message + if (!$io->isInteractive()) { + $io->write('' . self::MESSAGES['en']['skip'] . ''); + return; + } + + try { + self::doRun($event, $io); + } catch (\Throwable $e) { + $io->writeError(''); + $io->writeError('Setup wizard error: ' . $e->getMessage() . ''); + $io->writeError('Run "composer setup-webman" to retry.'); + } + } + + private static function doRun(Event $event, IOInterface $io): void + { + $io->write(''); + + // Register Ctrl+C handler + self::registerInterruptHandler(); + + // Banner title (must be before locale selection) + self::renderTitle(); + + // 1. Locale selection + $locale = self::askLocale($io); + self::$interruptLocale = $locale; + $defaultTimezone = self::LOCALE_DEFAULT_TIMEZONES[$locale] ?? 'UTC'; + $msg = fn(string $key, string ...$args): string => + empty($args) ? self::MESSAGES[$locale][$key] : sprintf(self::MESSAGES[$locale][$key], ...$args); + + // Write locale config (update when not default) + if ($locale !== 'zh_CN') { + self::updateConfig($event, 'config/translation.php', "'locale'", $locale); + } + + $io->write(''); + $io->write(''); + + // 2. Timezone selection (default by locale) + $timezone = self::askTimezone($io, $msg, $defaultTimezone); + if ($timezone !== 'Asia/Shanghai') { + self::updateConfig($event, 'config/app.php', "'default_timezone'", $timezone); + } + + // 3. Optional components + $packages = self::askComponents($io, $msg); + + // 4. Remove unselected components + $removePackages = self::askRemoveComponents($event, $packages, $io, $msg); + + // 5. Summary + $io->write(''); + $io->write('─────────────────────────────────────'); + $io->write('' . $msg('summary_locale', self::LOCALE_LABELS[$locale]) . ''); + $io->write('' . $msg('summary_timezone', $timezone) . ''); + + // Remove unselected packages first to avoid dependency conflicts + if ($removePackages !== []) { + $io->write(''); + $io->write('' . $msg('removing') . ''); + + $secondaryPackages = [ + self::PACKAGE_ILLUMINATE_EVENTS, + self::PACKAGE_ILLUMINATE_PAGINATION, + self::PACKAGE_SYMFONY_VAR_DUMPER, + ]; + $displayRemovePackages = array_diff($removePackages, $secondaryPackages); + foreach ($displayRemovePackages as $pkg) { + $io->write(' - ' . $pkg); + } + $io->write(''); + self::runComposerRemove($removePackages, $io, $msg); + } + + // Then install selected packages + if ($packages !== []) { + $io->write(''); + $io->write('' . $msg('installing') . ' ' . implode(', ', $packages)); + $io->write(''); + self::runComposerRequire($packages, $io, $msg); + } elseif ($removePackages === []) { + $io->write('' . $msg('no_components') . ''); + } + } + + private static function renderTitle(): void + { + $output = new ConsoleOutput(); + $terminalWidth = (new Terminal())->getWidth(); + if ($terminalWidth <= 0) { + $terminalWidth = 80; + } + + $text = ' ' . self::SETUP_TITLE . ' '; + $minBoxWidth = 44; + $maxBoxWidth = min($terminalWidth, 96); + $boxWidth = min($maxBoxWidth, max($minBoxWidth, mb_strwidth($text) + 10)); + + $innerWidth = $boxWidth - 2; + $textWidth = mb_strwidth($text); + $pad = max(0, $innerWidth - $textWidth); + $left = intdiv($pad, 2); + $right = $pad - $left; + $line2 = '│' . str_repeat(' ', $left) . $text . str_repeat(' ', $right) . '│'; + $line1 = '┌' . str_repeat('─', $innerWidth) . '┐'; + $line3 = '└' . str_repeat('─', $innerWidth) . '┘'; + + $output->writeln(''); + $output->writeln('' . $line1 . ''); + $output->writeln('' . $line2 . ''); + $output->writeln('' . $line3 . ''); + $output->writeln(''); + } + + // ═══════════════════════════════════════════════════════════════ + // Signal handling (Ctrl+C) + // ═══════════════════════════════════════════════════════════════ + + /** + * Register Ctrl+C (SIGINT) handler to show a friendly message on interrupt. + * Gracefully skipped when the required extensions are unavailable. + */ + private static function registerInterruptHandler(): void + { + // Unix/Linux/Mac: pcntl extension with async signals for immediate delivery + /*if (function_exists('pcntl_async_signals') && function_exists('pcntl_signal')) { + pcntl_async_signals(true); + pcntl_signal(\SIGINT, [self::class, 'handleInterrupt']); + return; + }*/ + + // Windows: sapi ctrl handler (PHP >= 7.4) + if (function_exists('sapi_windows_set_ctrl_handler')) { + sapi_windows_set_ctrl_handler(static function (int $event) { + if ($event === \PHP_WINDOWS_EVENT_CTRL_C) { + self::handleInterrupt(); + } + }); + } + } + + /** + * Handle Ctrl+C: restore terminal, show tip, then exit. + */ + private static function handleInterrupt(): void + { + // Restore terminal if in raw mode + if (self::$sttyMode !== null && function_exists('shell_exec')) { + @shell_exec('stty ' . self::$sttyMode); + self::$sttyMode = null; + } + + $output = new ConsoleOutput(); + $output->writeln(''); + $output->writeln('' . (self::INTERRUPTED_MESSAGES[self::$interruptLocale] ?? self::INTERRUPTED_MESSAGES['en']) . ''); + exit(1); + } + + // ═══════════════════════════════════════════════════════════════ + // Interactive Menu System + // ═══════════════════════════════════════════════════════════════ + + /** + * Check if terminal supports interactive features (arrow keys, ANSI colors). + */ + private static function supportsInteractive(): bool + { + return function_exists('shell_exec') && Terminal::hasSttyAvailable(); + } + + /** + * Display a selection menu with arrow key navigation (if supported) or text input fallback. + * + * @param IOInterface $io Composer IO + * @param string $title Menu title + * @param array $items Indexed array of ['tag' => string, 'label' => string] + * @param int $default Default selected index (0-based) + * @return int Selected index + */ + private static function selectMenu(IOInterface $io, string $title, array $items, int $default = 0): int + { + // Append localized "default" hint to avoid ambiguity + // (Template should contain a single %s placeholder for the default tag.) + $defaultHintTemplate = null; + if (isset(self::MESSAGES[self::$interruptLocale]['default_choice'])) { + $defaultHintTemplate = self::MESSAGES[self::$interruptLocale]['default_choice']; + } + + $defaultTag = $items[$default]['tag'] ?? ''; + if ($defaultHintTemplate && $defaultTag !== '') { + $title .= sprintf($defaultHintTemplate, $defaultTag); + } elseif ($defaultTag !== '') { + // Fallback for early menus (e.g. locale selection) before locale is chosen. + $title .= sprintf(' (default %s)', $defaultTag); + } + + if (self::supportsInteractive()) { + return self::arrowKeySelect($title, $items, $default); + } + + return self::fallbackSelect($io, $title, $items, $default); + } + + /** + * Display a yes/no confirmation as a selection menu. + * + * @param IOInterface $io Composer IO + * @param string $title Menu title + * @param bool $default Default value (true = yes) + * @return bool User's choice + */ + private static function confirmMenu(IOInterface $io, string $title, bool $default = true): bool + { + $locale = self::$interruptLocale; + $yes = self::MESSAGES[$locale]['yes'] ?? self::MESSAGES['en']['yes'] ?? 'yes'; + $no = self::MESSAGES[$locale]['no'] ?? self::MESSAGES['en']['no'] ?? 'no'; + $items = $default + ? [['tag' => 'Y', 'label' => $yes], ['tag' => 'n', 'label' => $no]] + : [['tag' => 'y', 'label' => $yes], ['tag' => 'N', 'label' => $no]]; + $defaultIndex = $default ? 0 : 1; + + return self::selectMenu($io, $title, $items, $defaultIndex) === 0; + } + + /** + * Interactive select with arrow key navigation, manual input and ANSI reverse-video highlighting. + * Input area and option list highlighting are bidirectionally linked. + * Requires stty (Unix-like terminals). + */ + private static function arrowKeySelect(string $title, array $items, int $default): int + { + $output = new ConsoleOutput(); + $count = count($items); + $selected = $default; + + $maxTagWidth = max(array_map(fn(array $item) => mb_strlen($item['tag']), $items)); + $defaultTag = $items[$default]['tag']; + $input = $defaultTag; + + // Print title and initial options + $output->writeln(''); + $output->writeln('' . $title . ''); + self::drawMenuItems($output, $items, $selected, $maxTagWidth); + $output->write('> ' . $input); + + // Enter raw mode + self::$sttyMode = shell_exec('stty -g'); + shell_exec('stty -icanon -echo'); + + try { + while (!feof(STDIN)) { + $c = fread(STDIN, 1); + + if (false === $c || '' === $c) { + break; + } + + // ── Backspace ── + if ("\177" === $c || "\010" === $c) { + if ('' !== $input) { + $input = mb_substr($input, 0, -1); + } + $selected = self::findItemByTag($items, $input); + $output->write("\033[{$count}A"); + self::drawMenuItems($output, $items, $selected, $maxTagWidth); + $output->write("\033[2K\r> " . $input); + continue; + } + + // ── Escape sequences (arrow keys) ── + if ("\033" === $c) { + $seq = fread(STDIN, 2); + if (isset($seq[1])) { + $changed = false; + if ('A' === $seq[1]) { // Up + $selected = ($selected <= 0 ? $count : $selected) - 1; + $changed = true; + } elseif ('B' === $seq[1]) { // Down + $selected = ($selected + 1) % $count; + $changed = true; + } + if ($changed) { + // Sync input with selected item's tag + $input = $items[$selected]['tag']; + $output->write("\033[{$count}A"); + self::drawMenuItems($output, $items, $selected, $maxTagWidth); + $output->write("\033[2K\r> " . $input); + } + } + continue; + } + + // ── Enter: confirm selection ── + if ("\n" === $c || "\r" === $c) { + if ($selected < 0) { + $selected = $default; + } + $output->write("\033[2K\r> " . $items[$selected]['tag'] . ' ' . $items[$selected]['label'] . ''); + $output->writeln(''); + break; + } + + // ── Ignore other control characters ── + if (ord($c) < 32) { + continue; + } + + // ── Printable character (with UTF-8 multi-byte support) ── + if ("\x80" <= $c) { + $extra = ["\xC0" => 1, "\xD0" => 1, "\xE0" => 2, "\xF0" => 3]; + $c .= fread(STDIN, $extra[$c & "\xF0"] ?? 0); + } + $input .= $c; + $selected = self::findItemByTag($items, $input); + $output->write("\033[{$count}A"); + self::drawMenuItems($output, $items, $selected, $maxTagWidth); + $output->write("\033[2K\r> " . $input); + } + } finally { + if (self::$sttyMode !== null) { + shell_exec('stty ' . self::$sttyMode); + self::$sttyMode = null; + } + } + + return $selected < 0 ? $default : $selected; + } + + /** + * Fallback select for terminals without stty support. Uses plain text input. + */ + private static function fallbackSelect(IOInterface $io, string $title, array $items, int $default): int + { + $maxTagWidth = max(array_map(fn(array $item) => mb_strlen($item['tag']), $items)); + $defaultTag = $items[$default]['tag']; + + $io->write(''); + $io->write('' . $title . ''); + foreach ($items as $item) { + $tag = str_pad($item['tag'], $maxTagWidth); + $io->write(" [$tag] " . $item['label']); + } + + while (true) { + $io->write('> ', false); + $line = fgets(STDIN); + if ($line === false) { + return $default; + } + $answer = trim($line); + + if ($answer === '') { + $io->write('> ' . $items[$default]['tag'] . ' ' . $items[$default]['label'] . ''); + return $default; + } + + // Match by tag (case-insensitive) + foreach ($items as $i => $item) { + if (strcasecmp($item['tag'], $answer) === 0) { + $io->write('> ' . $items[$i]['tag'] . ' ' . $items[$i]['label'] . ''); + return $i; + } + } + } + } + + /** + * Render menu items with optional ANSI reverse-video highlighting for the selected item. + * When $selected is -1, no item is highlighted. + */ + private static function drawMenuItems(ConsoleOutput $output, array $items, int $selected, int $maxTagWidth): void + { + foreach ($items as $i => $item) { + $tag = str_pad($item['tag'], $maxTagWidth); + $line = " [$tag] " . $item['label']; + if ($i === $selected) { + $output->writeln("\033[2K\r\033[7m" . $line . "\033[0m"); + } else { + $output->writeln("\033[2K\r" . $line); + } + } + } + + /** + * Find item index by tag (case-insensitive exact match). + * Returns -1 if no match found or input is empty. + */ + private static function findItemByTag(array $items, string $input): int + { + if ($input === '') { + return -1; + } + foreach ($items as $i => $item) { + if (strcasecmp($item['tag'], $input) === 0) { + return $i; + } + } + return -1; + } + + // ═══════════════════════════════════════════════════════════════ + // Locale selection + // ═══════════════════════════════════════════════════════════════ + + private static function askLocale(IOInterface $io): string + { + $locales = array_keys(self::LOCALE_LABELS); + $items = []; + foreach ($locales as $i => $code) { + $items[] = ['tag' => (string) $i, 'label' => self::LOCALE_LABELS[$code] . " ($code)"]; + } + + $selected = self::selectMenu( + $io, + '语言 / Language / 言語 / 언어', + $items, + 0 + ); + + return $locales[$selected]; + } + + // ═══════════════════════════════════════════════════════════════ + // Timezone selection + // ═══════════════════════════════════════════════════════════════ + + private static function askTimezone(IOInterface $io, callable $msg, string $default): string + { + if (self::supportsInteractive()) { + return self::askTimezoneAutocomplete($msg, $default); + } + + return self::askTimezoneSelect($io, $msg, $default); + } + + /** + * Option A: when stty is available, custom character-by-character autocomplete + * (case-insensitive, substring match). Interaction: type to filter, hint on right; + * ↑↓ change candidate, Tab accept, Enter confirm; empty input = use default. + */ + private static function askTimezoneAutocomplete(callable $msg, string $default): string + { + $allTimezones = \DateTimeZone::listIdentifiers(); + $output = new ConsoleOutput(); + $cursor = new Cursor($output); + + $output->writeln(''); + $output->writeln('' . $msg('timezone_title', $default) . ''); + $output->writeln($msg('timezone_help')); + $output->write('> '); + + self::$sttyMode = shell_exec('stty -g'); + shell_exec('stty -icanon -echo'); + + // Auto-fill default timezone in the input area; user can edit it directly. + $input = $default; + $output->write($input); + + $ofs = 0; + $matches = self::filterTimezones($allTimezones, $input); + if (!empty($matches)) { + $hint = $matches[$ofs % count($matches)]; + // Avoid duplicating hint when input already fully matches the only candidate. + if (!(count($matches) === 1 && $hint === $input)) { + $cursor->clearLineAfter(); + $cursor->savePosition(); + $output->write(' ' . $hint . ''); + if (count($matches) > 1) { + $output->write(' (' . count($matches) . ' matches, ↑↓)'); + } + $cursor->restorePosition(); + } + } + + try { + while (!feof(STDIN)) { + $c = fread(STDIN, 1); + + if (false === $c || '' === $c) { + break; + } + + // ── Backspace ── + if ("\177" === $c || "\010" === $c) { + if ('' !== $input) { + $lastChar = mb_substr($input, -1); + $input = mb_substr($input, 0, -1); + $cursor->moveLeft(max(1, mb_strwidth($lastChar))); + } + $ofs = 0; + + // ── Escape sequences (arrows) ── + } elseif ("\033" === $c) { + $seq = fread(STDIN, 2); + if (isset($seq[1]) && !empty($matches)) { + if ('A' === $seq[1]) { + $ofs = ($ofs - 1 + count($matches)) % count($matches); + } elseif ('B' === $seq[1]) { + $ofs = ($ofs + 1) % count($matches); + } + } + + // ── Tab: accept current match ── + } elseif ("\t" === $c) { + if (isset($matches[$ofs])) { + self::replaceInput($output, $cursor, $input, $matches[$ofs]); + $input = $matches[$ofs]; + $matches = []; + } + $cursor->clearLineAfter(); + continue; + + // ── Enter: confirm ── + } elseif ("\n" === $c || "\r" === $c) { + if (isset($matches[$ofs])) { + self::replaceInput($output, $cursor, $input, $matches[$ofs]); + $input = $matches[$ofs]; + } + if ($input === '') { + $input = $default; + } + // Re-render user input with style + $cursor->moveToColumn(1); + $cursor->clearLine(); + $output->write('> ' . $input . ''); + $output->writeln(''); + break; + + // ── Other control chars: ignore ── + } elseif (ord($c) < 32) { + continue; + + // ── Printable character ── + } else { + if ("\x80" <= $c) { + $extra = ["\xC0" => 1, "\xD0" => 1, "\xE0" => 2, "\xF0" => 3]; + $c .= fread(STDIN, $extra[$c & "\xF0"] ?? 0); + } + $output->write($c); + $input .= $c; + $ofs = 0; + } + + // Update match list + $matches = self::filterTimezones($allTimezones, $input); + + // Show autocomplete hint + $cursor->clearLineAfter(); + if (!empty($matches)) { + $hint = $matches[$ofs % count($matches)]; + $cursor->savePosition(); + $output->write(' ' . $hint . ''); + if (count($matches) > 1) { + $output->write(' (' . count($matches) . ' matches, ↑↓)'); + } + $cursor->restorePosition(); + } + } + } finally { + if (self::$sttyMode !== null) { + shell_exec('stty ' . self::$sttyMode); + self::$sttyMode = null; + } + } + + $result = '' === $input ? $default : $input; + + if (!in_array($result, $allTimezones, true)) { + $output->writeln('' . $msg('timezone_invalid', $default) . ''); + return $default; + } + + return $result; + } + + /** + * Clear current input and replace with new text. + */ + private static function replaceInput(ConsoleOutput $output, Cursor $cursor, string $oldInput, string $newInput): void + { + if ('' !== $oldInput) { + $cursor->moveLeft(mb_strwidth($oldInput)); + } + $cursor->clearLineAfter(); + $output->write($newInput); + } + + /** + * Case-insensitive substring match for timezones. + */ + private static function filterTimezones(array $timezones, string $input): array + { + if ('' === $input) { + return []; + } + $lower = mb_strtolower($input); + return array_values(array_filter( + $timezones, + fn(string $tz) => str_contains(mb_strtolower($tz), $lower) + )); + } + + /** + * Find an exact timezone match (case-insensitive). + * Returns the correctly-cased system timezone name, or null if not found. + */ + private static function findExactTimezone(array $allTimezones, string $input): ?string + { + $lower = mb_strtolower($input); + foreach ($allTimezones as $tz) { + if (mb_strtolower($tz) === $lower) { + return $tz; + } + } + return null; + } + + /** + * Search timezones by keyword (substring) and similarity. + * Returns combined results: substring matches first, then similarity matches (>=50%). + * + * @param string[] $allTimezones All valid timezone identifiers + * @param string $keyword User input to search for + * @param int $limit Maximum number of results + * @return string[] Matched timezone identifiers + */ + private static function searchTimezones(array $allTimezones, string $keyword, int $limit = 15): array + { + // 1. Substring matches (higher priority) + $substringMatches = self::filterTimezones($allTimezones, $keyword); + if (count($substringMatches) >= $limit) { + return array_slice($substringMatches, 0, $limit); + } + + // 2. Similarity matches for remaining slots (normalized: strip _ and /) + $substringSet = array_flip($substringMatches); + $normalizedKeyword = str_replace(['_', '/'], ' ', mb_strtolower($keyword)); + $similarityMatches = []; + + foreach ($allTimezones as $tz) { + if (isset($substringSet[$tz])) { + continue; + } + $parts = explode('/', $tz); + $city = str_replace('_', ' ', mb_strtolower(end($parts))); + $normalizedTz = str_replace(['_', '/'], ' ', mb_strtolower($tz)); + + similar_text($normalizedKeyword, $city, $cityPercent); + similar_text($normalizedKeyword, $normalizedTz, $fullPercent); + + $bestPercent = max($cityPercent, $fullPercent); + if ($bestPercent >= 50.0) { + $similarityMatches[] = ['tz' => $tz, 'score' => $bestPercent]; + } + } + + usort($similarityMatches, fn(array $a, array $b) => $b['score'] <=> $a['score']); + + $results = $substringMatches; + foreach ($similarityMatches as $item) { + $results[] = $item['tz']; + if (count($results) >= $limit) { + break; + } + } + + return $results; + } + + /** + * Option B: when stty is not available (e.g. Windows), keyword search with numbered list. + * Flow: enter timezone/keyword → exact match uses it directly; otherwise show + * numbered results (substring + similarity) → pick by number or refine keyword. + */ + private static function askTimezoneSelect(IOInterface $io, callable $msg, string $default): string + { + $allTimezones = \DateTimeZone::listIdentifiers(); + + $io->write(''); + $io->write('' . $msg('timezone_title', $default) . ''); + $io->write($msg('timezone_input_prompt')); + + /** @var string[]|null Currently displayed search result list */ + $currentList = null; + + while (true) { + $io->write('> ', false); + $line = fgets(STDIN); + if ($line === false) { + return $default; + } + $answer = trim($line); + + // Empty input → use default + if ($answer === '') { + $io->write('> ' . $default . ''); + return $default; + } + + // If a numbered list is displayed and input is a pure number + if ($currentList !== null && ctype_digit($answer)) { + $idx = (int) $answer; + if (isset($currentList[$idx])) { + $io->write('> ' . $currentList[$idx] . ''); + return $currentList[$idx]; + } + $io->write('' . $msg('timezone_invalid_index') . ''); + continue; + } + + // Exact case-insensitive match → return the correctly-cased system value + $exact = self::findExactTimezone($allTimezones, $answer); + if ($exact !== null) { + $io->write('> ' . $exact . ''); + return $exact; + } + + // Keyword + similarity search + $results = self::searchTimezones($allTimezones, $answer); + + if (empty($results)) { + $io->write('' . $msg('timezone_no_match') . ''); + $currentList = null; + continue; + } + + // Single result → use it directly + if (count($results) === 1) { + $io->write('> ' . $results[0] . ''); + return $results[0]; + } + + // Display numbered list + $currentList = $results; + $padWidth = strlen((string) (count($results) - 1)); + foreach ($results as $i => $tz) { + $io->write(' [' . str_pad((string) $i, $padWidth) . '] ' . $tz); + } + $io->write($msg('timezone_pick_prompt')); + } + } + + // ═══════════════════════════════════════════════════════════════ + // Optional component selection + // ═══════════════════════════════════════════════════════════════ + + private static function askComponents(IOInterface $io, callable $msg): array + { + $packages = []; + $addPackage = static function (string $package) use (&$packages, $io, $msg): void { + if (in_array($package, $packages, true)) { + return; + } + $packages[] = $package; + $io->write($msg('adding_package', '' . $package . '')); + }; + + // Console (default: yes) + if (self::confirmMenu($io, $msg('console_question'), true)) { + $addPackage(self::PACKAGE_CONSOLE); + } + + // Database + $dbItems = [ + ['tag' => '0', 'label' => $msg('db_none')], + ['tag' => '1', 'label' => 'webman/database'], + ['tag' => '2', 'label' => 'webman/think-orm'], + ['tag' => '3', 'label' => 'webman/database && webman/think-orm'], + ]; + $dbChoice = self::selectMenu($io, $msg('db_question'), $dbItems, 0); + if ($dbChoice === 1) { + $addPackage(self::PACKAGE_DATABASE); + } elseif ($dbChoice === 2) { + $addPackage(self::PACKAGE_THINK_ORM); + } elseif ($dbChoice === 3) { + $addPackage(self::PACKAGE_DATABASE); + $addPackage(self::PACKAGE_THINK_ORM); + } + + // If webman/database is selected, add required dependencies automatically + if (in_array(self::PACKAGE_DATABASE, $packages, true)) { + $addPackage(self::PACKAGE_ILLUMINATE_PAGINATION); + $addPackage(self::PACKAGE_ILLUMINATE_EVENTS); + $addPackage(self::PACKAGE_SYMFONY_VAR_DUMPER); + } + + // Redis (default: no) + if (self::confirmMenu($io, $msg('redis_question'), false)) { + $addPackage(self::PACKAGE_REDIS); + $addPackage(self::PACKAGE_ILLUMINATE_EVENTS); + } + + // Validation (default: no) + if (self::confirmMenu($io, $msg('validation_question'), false)) { + $addPackage(self::PACKAGE_VALIDATION); + } + + // Template engine + $tplItems = [ + ['tag' => '0', 'label' => $msg('template_none')], + ['tag' => '1', 'label' => 'webman/blade'], + ['tag' => '2', 'label' => 'twig/twig'], + ['tag' => '3', 'label' => 'topthink/think-template'], + ]; + $tplChoice = self::selectMenu($io, $msg('template_question'), $tplItems, 0); + if ($tplChoice === 1) { + $addPackage(self::PACKAGE_BLADE); + } elseif ($tplChoice === 2) { + $addPackage(self::PACKAGE_TWIG); + } elseif ($tplChoice === 3) { + $addPackage(self::PACKAGE_THINK_TEMPLATE); + } + + return $packages; + } + + // ═══════════════════════════════════════════════════════════════ + // Config file update + // ═══════════════════════════════════════════════════════════════ + + /** + * Update a config value like 'key' => 'old_value' in the given file. + */ + private static function updateConfig(Event $event, string $relativePath, string $key, string $newValue): void + { + $root = dirname($event->getComposer()->getConfig()->get('vendor-dir')); + $file = $root . DIRECTORY_SEPARATOR . str_replace('/', DIRECTORY_SEPARATOR, $relativePath); + if (!is_readable($file)) { + return; + } + $content = file_get_contents($file); + if ($content === false) { + return; + } + $pattern = '/' . preg_quote($key, '/') . "\s*=>\s*'[^']*'/"; + $replacement = $key . " => '" . $newValue . "'"; + $newContent = preg_replace($pattern, $replacement, $content); + if ($newContent !== null && $newContent !== $content) { + file_put_contents($file, $newContent); + } + } + + // ═══════════════════════════════════════════════════════════════ + // Composer require + // ═══════════════════════════════════════════════════════════════ + + private static function runComposerRequire(array $packages, IOInterface $io, callable $msg): void + { + $io->write('' . $msg('running') . ' composer require ' . implode(' ', $packages)); + $io->write(''); + + $code = self::runComposerCommand('require', $packages); + + if ($code !== 0) { + $io->writeError('' . $msg('error_install', implode(' ', $packages)) . ''); + } else { + $io->write('' . $msg('done') . ''); + } + } + + private static function askRemoveComponents(Event $event, array $selectedPackages, IOInterface $io, callable $msg): array + { + $requires = $event->getComposer()->getPackage()->getRequires(); + $allOptionalPackages = [ + self::PACKAGE_CONSOLE, + self::PACKAGE_DATABASE, + self::PACKAGE_THINK_ORM, + self::PACKAGE_REDIS, + self::PACKAGE_ILLUMINATE_EVENTS, + self::PACKAGE_ILLUMINATE_PAGINATION, + self::PACKAGE_SYMFONY_VAR_DUMPER, + self::PACKAGE_VALIDATION, + self::PACKAGE_BLADE, + self::PACKAGE_TWIG, + self::PACKAGE_THINK_TEMPLATE, + ]; + + $secondaryPackages = [ + self::PACKAGE_ILLUMINATE_EVENTS, + self::PACKAGE_ILLUMINATE_PAGINATION, + self::PACKAGE_SYMFONY_VAR_DUMPER, + ]; + + $installedOptionalPackages = []; + foreach ($allOptionalPackages as $pkg) { + if (isset($requires[$pkg])) { + $installedOptionalPackages[] = $pkg; + } + } + + $allPackagesToRemove = array_diff($installedOptionalPackages, $selectedPackages); + + if (count($allPackagesToRemove) === 0) { + return []; + } + + $displayPackagesToRemove = array_diff($allPackagesToRemove, $secondaryPackages); + + if (count($displayPackagesToRemove) === 0) { + return $allPackagesToRemove; + } + + $pkgListStr = ""; + foreach ($displayPackagesToRemove as $pkg) { + $pkgListStr .= "\n - {$pkg}"; + } + $pkgListStr .= "\n"; + + $title = '' . $msg('remove_package_question', '') . '' . $pkgListStr; + if (self::confirmMenu($io, $title, false)) { + return $allPackagesToRemove; + } + + return []; + } + + private static function runComposerRemove(array $packages, IOInterface $io, callable $msg): void + { + $io->write('' . $msg('running') . ' composer remove ' . implode(' ', $packages)); + $io->write(''); + + $code = self::runComposerCommand('remove', $packages); + + if ($code !== 0) { + $io->writeError('' . $msg('error_remove', implode(' ', $packages)) . ''); + } else { + $io->write('' . $msg('done_remove') . ''); + } + } + + /** + * Run a Composer command (require/remove) in-process via Composer's Application API. + * No shell execution functions needed — works even when passthru/exec/shell_exec are disabled. + */ + private static function runComposerCommand(string $command, array $packages): int + { + try { + // Already inside a user-initiated Composer session — suppress duplicate root/superuser warnings + $_SERVER['COMPOSER_ALLOW_SUPERUSER'] = '1'; + if (function_exists('putenv')) { + putenv('COMPOSER_ALLOW_SUPERUSER=1'); + } + + $application = new ComposerApplication(); + $application->setAutoExit(false); + + return $application->run( + new ArrayInput([ + 'command' => $command, + 'packages' => $packages, + '--no-interaction' => true, + '--update-with-all-dependencies' => true, + ]), + new ConsoleOutput() + ); + } catch (\Throwable) { + return 1; + } + } +} diff --git a/support/bootstrap.php b/support/bootstrap.php new file mode 100644 index 0000000..d913def --- /dev/null +++ b/support/bootstrap.php @@ -0,0 +1,139 @@ + + * @copyright walkor + * @link http://www.workerman.net/ + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ + +use Dotenv\Dotenv; +use support\Log; +use Webman\Bootstrap; +use Webman\Config; +use Webman\Middleware; +use Webman\Route; +use Webman\Util; +use Workerman\Events\Select; +use Workerman\Worker; + +$worker = $worker ?? null; + +if (empty(Worker::$eventLoopClass)) { + Worker::$eventLoopClass = Select::class; +} + +set_error_handler(function ($level, $message, $file = '', $line = 0) { + if (error_reporting() & $level) { + throw new ErrorException($message, 0, $level, $file, $line); + } +}); + +if ($worker) { + register_shutdown_function(function ($startTime) { + if (time() - $startTime <= 0.1) { + sleep(1); + } + }, time()); +} + +if (class_exists('Dotenv\Dotenv') && file_exists(base_path(false) . '/.env')) { + if (method_exists('Dotenv\Dotenv', 'createUnsafeMutable')) { + Dotenv::createUnsafeMutable(base_path(false))->load(); + } else { + Dotenv::createMutable(base_path(false))->load(); + } +} + +Config::clear(); +support\App::loadAllConfig(['route']); +if ($timezone = config('app.default_timezone')) { + date_default_timezone_set($timezone); +} + +foreach (config('autoload.files', []) as $file) { + include_once $file; +} +foreach (config('plugin', []) as $firm => $projects) { + foreach ($projects as $name => $project) { + if (!is_array($project)) { + continue; + } + foreach ($project['autoload']['files'] ?? [] as $file) { + include_once $file; + } + } + foreach ($projects['autoload']['files'] ?? [] as $file) { + include_once $file; + } +} + +Middleware::load(config('middleware', [])); +foreach (config('plugin', []) as $firm => $projects) { + foreach ($projects as $name => $project) { + if (!is_array($project) || $name === 'static') { + continue; + } + Middleware::load($project['middleware'] ?? []); + } + Middleware::load($projects['middleware'] ?? [], $firm); + if ($staticMiddlewares = config("plugin.$firm.static.middleware")) { + Middleware::load(['__static__' => $staticMiddlewares], $firm); + } +} +Middleware::load(['__static__' => config('static.middleware', [])]); + +foreach (config('bootstrap', []) as $className) { + if (!class_exists($className)) { + $log = "Warning: Class $className setting in config/bootstrap.php not found\r\n"; + echo $log; + Log::error($log); + continue; + } + /** @var Bootstrap $className */ + $className::start($worker); +} + +foreach (config('plugin', []) as $firm => $projects) { + foreach ($projects as $name => $project) { + if (!is_array($project)) { + continue; + } + foreach ($project['bootstrap'] ?? [] as $className) { + if (!class_exists($className)) { + $log = "Warning: Class $className setting in config/plugin/$firm/$name/bootstrap.php not found\r\n"; + echo $log; + Log::error($log); + continue; + } + /** @var Bootstrap $className */ + $className::start($worker); + } + } + foreach ($projects['bootstrap'] ?? [] as $className) { + /** @var string $className */ + if (!class_exists($className)) { + $log = "Warning: Class $className setting in plugin/$firm/config/bootstrap.php not found\r\n"; + echo $log; + Log::error($log); + continue; + } + /** @var Bootstrap $className */ + $className::start($worker); + } +} + +$directory = base_path() . '/plugin'; +$paths = [config_path()]; +foreach (Util::scanDir($directory) as $path) { + if (is_dir($path = "$path/config")) { + $paths[] = $path; + } +} +Route::load($paths); + diff --git a/tests/db/DBC.php b/tests/db/DBC.php new file mode 100644 index 0000000..566162a --- /dev/null +++ b/tests/db/DBC.php @@ -0,0 +1,51 @@ +getConnection(); + $this->assertNotNull($db); + } + + /** + * 测试生成所有表 + */ +// public function testGenTables() +// { +// $result = ModelAutoGenerator::generate_all(true); +// +// // 2. 断言返回结果结构正确 +// $this->assertArrayHasKey('success', $result, '批量生成结果缺少 success 字段'); +// $this->assertArrayHasKey('fail', $result, '批量生成结果缺少 fail 字段'); +// // 3. 断言 success 是整数类型 +// $this->assertIsInt($result['success'], 'success 字段不是整数'); +// // 4. 断言 fail 是数组类型 +// $this->assertIsArray($result['fail'], 'fail 字段不是数组'); +// // 5. 断言 fail 数组是空的 +// if (!empty($result['fail'])){ +// foreach ($result['fail'] as $key => $value){ +// Logger::error($value['table']."表生成失败:".$value['reason']); +// } +// } +// $this->assertEmpty($result['fail'], 'fail 数组不是空的,说明有表生成失败'); +// } + + public function testModel() + { + $users = EctUser::all(); + $this->assertNotEmpty($users, '用户列表不能为空'); + } +} \ No newline at end of file diff --git a/tests/resources/default_environment.json b/tests/resources/default_environment.json new file mode 100644 index 0000000..ccc1055 --- /dev/null +++ b/tests/resources/default_environment.json @@ -0,0 +1,95 @@ +{ + "description": "虚拟测试环境默认配置", + "version": "1.0", + + "readers": { + "清洗": { + "no": "R001", + "id": "1", + "type": "清洗" + }, + "漂洗": { + "no": "R002", + "id": "2", + "type": "漂洗" + }, + "消毒": { + "no": "R003", + "id": "3", + "type": "消毒" + }, + "终末漂洗": { + "no": "R004", + "id": "4", + "type": "终末漂洗" + }, + "干燥": { + "no": "R005", + "id": "5", + "type": "干燥" + }, + "机洗": { + "no": "R006", + "id": "6", + "type": "机洗" + }, + "存储": { + "no": "R007", + "id": "7", + "type": "存储" + }, + "未绑定": { + "no": "R999", + "id": "", + "type": "" + } + }, + + "endoscopes": { + "胃镜1": { + "id": "E001", + "name": "胃镜1号", + "cardNo": "CARD_E001", + "type": "胃镜" + }, + "肠镜1": { + "id": "E002", + "name": "肠镜1号", + "cardNo": "CARD_E002", + "type": "肠镜" + }, + "未绑定卡": { + "id": "", + "name": "", + "cardNo": "CARD_UNKNOWN", + "type": "" + } + }, + + "operators": { + "操作员1": { + "id": "U001", + "name": "张三", + "rfid": "RFID_U001" + }, + "操作员2": { + "id": "U002", + "name": "李四", + "rfid": "RFID_U002" + } + }, + + "config": { + "machineId": "01", + "morningWashStartTime": "06:00:00", + "morningWashMode": "standard" + }, + + "stepDurations": { + "清洗": 60, + "漂洗": 60, + "消毒": 180, + "终末漂洗": 60, + "干燥": 60 + } +} diff --git a/webman b/webman new file mode 100644 index 0000000..3f55062 --- /dev/null +++ b/webman @@ -0,0 +1,71 @@ +#!/usr/bin/env php +load(); + } else { + Dotenv::createMutable(run_path())->load(); + } +} + +$appConfig = require $appConfigFile; +if ($timezone = $appConfig['default_timezone'] ?? '') { + date_default_timezone_set($timezone); +} + +if ($errorReporting = $appConfig['error_reporting'] ?? '') { + error_reporting($errorReporting); +} + +if (!in_array($argv[1] ?? '', ['start', 'restart', 'stop', 'status', 'reload', 'connections'])) { + require_once __DIR__ . '/support/bootstrap.php'; +} else { + if (class_exists('Support\App')) { + Support\App::loadAllConfig(['route']); + } else { + Config::reload(config_path(), ['route', 'container']); + } +} + +$cli = new Command(); +$cli->setName('webman cli'); +$cli->installInternalCommands(); +if (is_dir($command_path = Util::guessPath(app_path(), '/command', true))) { + $cli->installCommands($command_path); +} + +foreach (config('plugin', []) as $firm => $projects) { + if (isset($projects['app'])) { + foreach (['', '/app'] as $app) { + if ($command_str = Util::guessPath(base_path() . "/plugin/$firm{$app}", 'command')) { + $command_path = base_path() . "/plugin/$firm{$app}/$command_str"; + $cli->installCommands($command_path, "plugin\\$firm" . str_replace('/', '\\', $app) . "\\$command_str"); + } + } + } + foreach ($projects as $name => $project) { + if (!is_array($project)) { + continue; + } + $project['command'] ??= []; + array_walk($project['command'], [$cli, 'createCommandInstance']); + } +} + +$cli->run(); diff --git a/windows.bat b/windows.bat new file mode 100644 index 0000000..f07ce53 --- /dev/null +++ b/windows.bat @@ -0,0 +1,3 @@ +CHCP 65001 +php windows.php +pause \ No newline at end of file diff --git a/windows.php b/windows.php new file mode 100644 index 0000000..f37a72c --- /dev/null +++ b/windows.php @@ -0,0 +1,136 @@ +load(); + } else { + Dotenv::createMutable(base_path())->load(); + } +} + +App::loadAllConfig(['route']); + +$errorReporting = config('app.error_reporting'); +if (isset($errorReporting)) { + error_reporting($errorReporting); +} + +$runtimeProcessPath = runtime_path() . DIRECTORY_SEPARATOR . '/windows'; +$paths = [ + $runtimeProcessPath, + runtime_path('logs'), + runtime_path('views') +]; +foreach ($paths as $path) { + if (!is_dir($path)) { + mkdir($path, 0777, true); + } +} + +$processFiles = []; +if (config('server.listen')) { + $processFiles[] = __DIR__ . DIRECTORY_SEPARATOR . 'start.php'; +} +foreach (config('process', []) as $processName => $config) { + $processFiles[] = write_process_file($runtimeProcessPath, $processName, ''); +} + +foreach (config('plugin', []) as $firm => $projects) { + foreach ($projects as $name => $project) { + if (!is_array($project)) { + continue; + } + foreach ($project['process'] ?? [] as $processName => $config) { + $processFiles[] = write_process_file($runtimeProcessPath, $processName, "$firm.$name"); + } + } + foreach ($projects['process'] ?? [] as $processName => $config) { + $processFiles[] = write_process_file($runtimeProcessPath, $processName, $firm); + } +} + +function write_process_file($runtimeProcessPath, $processName, $firm): string +{ + $processParam = $firm ? "plugin.$firm.$processName" : $processName; + $configParam = $firm ? "config('plugin.$firm.process')['$processName']" : "config('process')['$processName']"; + $fileContent = << true]); + if (!$resource) { + exit("Can not execute $cmd\r\n"); + } + return $resource; +} + +$resource = popen_processes($processFiles); +echo "\r\n"; +while (1) { + sleep(1); + if (!empty($monitor) && $monitor->checkAllFilesChange()) { + $status = proc_get_status($resource); + $pid = $status['pid']; + shell_exec("taskkill /F /T /PID $pid"); + proc_close($resource); + $resource = popen_processes($processFiles); + } +}