init
This commit is contained in:
@@ -0,0 +1,82 @@
|
||||
<?php
|
||||
|
||||
namespace app\bootstrap;
|
||||
|
||||
use app\config\Config;
|
||||
use Illuminate\Database\Events\QueryExecuted;
|
||||
use support\Db;
|
||||
use support\Log;
|
||||
|
||||
// 引入webman内置日志类
|
||||
use Webman\Bootstrap;
|
||||
|
||||
/**
|
||||
* 在控制台/日志文件打印执行的SQL语句
|
||||
*/
|
||||
class SqlDebug implements Bootstrap
|
||||
{
|
||||
public static function start($worker)
|
||||
{
|
||||
// 判断是否为控制台环境
|
||||
$is_console = !$worker;
|
||||
if ($is_console) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 仅在调试模式下开启
|
||||
$debug = config("app.debug");
|
||||
if (!$debug || $debug === 'false') {
|
||||
return;
|
||||
}
|
||||
|
||||
$appPath = app_path();
|
||||
|
||||
if (Config::getInstance()->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(
|
||||
'<SQL_LOG_F>' . $file . "</SQL_LOG_F><SQL_LOG_L>{$trace['line']}</SQL_LOG_L><SQL_LOG_M>{$trace['function']}</SQL_LOG_M>[$queryExecuted->time ms] " . $sqlLog
|
||||
);
|
||||
break; // 只打印第一个匹配的业务文件信息,避免重复输出
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
<?php
|
||||
|
||||
namespace app\config;
|
||||
|
||||
|
||||
class Config
|
||||
{
|
||||
/**
|
||||
* @var int 普通日志轮转时间默认 14 天
|
||||
*/
|
||||
public int $logRotationTimeByDay {
|
||||
get => $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));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
namespace app\config;
|
||||
|
||||
|
||||
use Illuminate\Database\Connection;
|
||||
use support\Db;
|
||||
|
||||
class DatabaseConfig
|
||||
{
|
||||
public string $host = 'localhost' {
|
||||
get {
|
||||
return $this->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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace app\controller;
|
||||
|
||||
use support\Request;
|
||||
|
||||
class IndexController
|
||||
{
|
||||
public function index(Request $request)
|
||||
{
|
||||
return <<<EOF
|
||||
<style>
|
||||
* {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
iframe {
|
||||
border: none;
|
||||
overflow: scroll;
|
||||
}
|
||||
</style>
|
||||
<iframe
|
||||
src="https://www.workerman.net/wellcome"
|
||||
width="100%"
|
||||
height="100%"
|
||||
allow="clipboard-write"
|
||||
sandbox="allow-scripts allow-same-origin allow-popups allow-downloads"
|
||||
></iframe>
|
||||
EOF;
|
||||
}
|
||||
|
||||
public function view(Request $request)
|
||||
{
|
||||
return view('index/view', ['name' => 'webman']);
|
||||
}
|
||||
|
||||
public function json(Request $request)
|
||||
{
|
||||
return json(['code' => 0, 'msg' => 'ok']);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
<?php
|
||||
/**
|
||||
* Here is your custom functions.
|
||||
*/
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
/**
|
||||
* This file is part of webman.
|
||||
*
|
||||
* Licensed under The MIT License
|
||||
* For full copyright and license information, please see the MIT-LICENSE.txt
|
||||
* Redistributions of files must retain the above copyright notice.
|
||||
*
|
||||
* @author walkor<walkor@workerman.net>
|
||||
* @copyright walkor<walkor@workerman.net>
|
||||
* @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('<h1>403 forbidden</h1>', 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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace app\process;
|
||||
|
||||
use Webman\App;
|
||||
|
||||
class Http extends App
|
||||
{
|
||||
|
||||
}
|
||||
@@ -0,0 +1,305 @@
|
||||
<?php
|
||||
/**
|
||||
* This file is part of webman.
|
||||
*
|
||||
* Licensed under The MIT License
|
||||
* For full copyright and license information, please see the MIT-LICENSE.txt
|
||||
* Redistributions of files must retain the above copyright notice.
|
||||
*
|
||||
* @author walkor<walkor@workerman.net>
|
||||
* @copyright walkor<walkor@workerman.net>
|
||||
* @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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
<?php
|
||||
|
||||
namespace app\process;
|
||||
|
||||
use app\flow\FlowMain;
|
||||
use app\net\PacketContext;
|
||||
use app\net\parsers\PacketParserFactory;
|
||||
use app\utils\Logger;
|
||||
use support\Db;
|
||||
use support\Log;
|
||||
use Workerman\Connection\TcpConnection;
|
||||
|
||||
class TcpServer
|
||||
{
|
||||
/**
|
||||
* @var array 连接池 map<ip, connection> 连接池,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}"
|
||||
]));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
namespace app\repository;
|
||||
|
||||
use support\Model;
|
||||
|
||||
abstract class BaseRepository
|
||||
{
|
||||
protected Model $model;
|
||||
|
||||
public static abstract function new(): BaseRepository;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace app\repository\exception;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
class NotFoundUserException extends RuntimeException
|
||||
{
|
||||
public function __construct(
|
||||
$message = 'Not Found User in database',
|
||||
int $code = 404,
|
||||
?\Throwable $previous = null
|
||||
)
|
||||
{
|
||||
parent::__construct($message, $code, $previous);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace app\repository\exception;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
class ResultNotAsExpectedException extends RuntimeException
|
||||
{
|
||||
public function __construct(
|
||||
$message = 'Result not as expected',
|
||||
int $code = 404,
|
||||
?\Throwable $previous = null
|
||||
)
|
||||
{
|
||||
parent::__construct($message, $code, $previous);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,379 @@
|
||||
<?php
|
||||
|
||||
namespace app\utils;
|
||||
|
||||
use app\config\Config;
|
||||
use support\Log;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* 日志工具类(仅提供静态方法)
|
||||
*/
|
||||
class Logger
|
||||
{
|
||||
/**
|
||||
* 获取日志实例(核心入口)
|
||||
* @param string $name 日志器名称,默认 '__default'
|
||||
* @return LoggerIns
|
||||
*/
|
||||
public static function new(string $name = '__default'): LoggerIns
|
||||
{
|
||||
return new LoggerIns($name);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成自适应宽度的文本框(精准空格版,绝对对齐)
|
||||
* @param array $rows 文本框内的行内容数组
|
||||
* @param string $borderChar 边框字符(默认横线)
|
||||
* @return string 格式化后的文本框字符串
|
||||
*/
|
||||
public static function generateTextBox(array $rows, string $borderChar = '-'): string
|
||||
{
|
||||
// 过滤空行
|
||||
$validRows = array_filter($rows, fn($row) => 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 = "<RP_NAME:{$this->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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,337 @@
|
||||
<?php
|
||||
|
||||
namespace app\utils;
|
||||
|
||||
use support\Db;
|
||||
use RuntimeException;
|
||||
|
||||
class ModelAutoGenerator
|
||||
{
|
||||
/**
|
||||
* 生成单个数据表对应的模型文件
|
||||
*
|
||||
* @param string $tableName 数据库表名(如:user)
|
||||
* @param string $modelName 模型类名(如:User)
|
||||
* @param bool $force 是否强制覆盖已存在的模型文件
|
||||
* @return array 生成结果 ['status' => 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 <<<PHP
|
||||
<?php
|
||||
|
||||
namespace app\model;
|
||||
|
||||
use support\Model;
|
||||
|
||||
/**
|
||||
* {$modelClassName} 模型
|
||||
* 表说明:{$tableCommentStr}
|
||||
* 对应数据表:{$tableName}
|
||||
{$fieldCommentsStr}
|
||||
*/
|
||||
class {$modelClassName} extends Model
|
||||
{
|
||||
/**
|
||||
* 数据表名
|
||||
* @var string
|
||||
*/
|
||||
protected \$table = '{$tableName}';
|
||||
|
||||
/**
|
||||
* 主键字段名
|
||||
* @var string
|
||||
*/
|
||||
protected \$pk = '{$primaryKey}';
|
||||
|
||||
/**
|
||||
* 关闭自动时间戳(如需开启请改为 true,需确保表有 create_time/update_time 字段)
|
||||
* @var bool
|
||||
*/
|
||||
protected \$autoWriteTimestamp = false;
|
||||
|
||||
/**
|
||||
* 字段严格检查(false=允许操作未定义字段)
|
||||
* @var bool
|
||||
*/
|
||||
protected \$strict = false;
|
||||
}
|
||||
PHP;
|
||||
}
|
||||
|
||||
/**
|
||||
* 数据表名转模型名(下划线转驼峰)
|
||||
*
|
||||
* @param string $tableName 数据表名(如:user_info)
|
||||
* @return string 模型名(如:UserInfo)
|
||||
*/
|
||||
private static function tableNameToModelName(string $tableName): string
|
||||
{
|
||||
// 移除表前缀(如需处理前缀,可在此添加逻辑)
|
||||
// $prefix = config('database.connections.mysql.prefix');
|
||||
// $tableName = str_replace($prefix, '', $tableName);
|
||||
|
||||
// 下划线转驼峰并首字母大写
|
||||
return str_replace(' ', '', ucwords(str_replace('_', ' ', $tableName)));
|
||||
}
|
||||
|
||||
/**
|
||||
* 数据库字段类型转PHP类型注释
|
||||
*
|
||||
* @param string $dbType 数据库类型(如:int(11), varchar(255))
|
||||
* @return string PHP类型(如:int, string)
|
||||
*/
|
||||
private static function dbTypeToPhpType(string $dbType): string
|
||||
{
|
||||
$type = strtolower(explode('(', $dbType)[0]);
|
||||
return match ($type) {
|
||||
'int', 'tinyint', 'smallint', 'mediumint', 'bigint' => 'int',
|
||||
'float', 'double', 'decimal' => 'float',
|
||||
'date', 'time', 'datetime', 'timestamp' => 'string',
|
||||
'bool', 'boolean' => 'bool',
|
||||
default => 'string'
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="shortcut icon" href="/favicon.ico"/>
|
||||
<title>webman</title>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
hello <?=htmlspecialchars($name)?>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user