feat(webman): 初始化webman框架基础配置与核心代码

- 添加基础配置文件,包括app、autoload、bootstrap、container、database、dependence、exception、log、middleware
- 新增完整Dockerfile和docker-compose.yml,支持容器化运行
- 增加webman控制台插件配置及依赖管理文件composer.json
- 创建app目录下核心控制器IndexController及进程Http和Monitor
- 实现日志工具类Logger,支持多级别及异常格式化
- 配置.gitignore优化忽略无关文件和目录
- 初始化.idea项目配置文件,支持开发环境集成
- 完善启动流程,支持自动加载配置、路由及中间件
- 增加文件变更监控和内存监控机制,提升开发体验
- 设定默认时区为Asia/Shanghai,开启调试模式
- 配置日志输出格式,支持多种日志处理器与颜色高亮
- 提供公共函数文件框架,便于扩展自定义功能
This commit is contained in:
zimoyin
2026-03-02 21:55:33 +08:00
commit 043306819b
48 changed files with 8550 additions and 0 deletions
+42
View File
@@ -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']);
}
}
+4
View File
@@ -0,0 +1,4 @@
<?php
/**
* Here is your custom functions.
*/
+42
View File
@@ -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;
}
}
+29
View File
@@ -0,0 +1,29 @@
<?php
namespace app\model;
use support\Model;
class Test extends Model
{
/**
* The table associated with the model.
*
* @var string
*/
protected $table = 'test';
/**
* The primary key associated with the table.
*
* @var string
*/
protected $primaryKey = 'id';
/**
* Indicates if the model should be timestamped.
*
* @var bool
*/
public $timestamps = false;
}
+10
View File
@@ -0,0 +1,10 @@
<?php
namespace app\process;
use Webman\App;
class Http extends App
{
}
+305
View File
@@ -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;
}
}
+39
View File
@@ -0,0 +1,39 @@
<?php
namespace app\process;
use app\utils\Logger;
use support\Log;
use Workerman\Connection\TcpConnection;
class TcpServer
{
public function onWorkerStart($worker)
{
Logger::debug("TcpServer start");
Log::info("TcpServer start");
Log::warning("TcpServer start");
Log::error("TcpServer start");
Logger::new('Test')->warning("TcpServer start");
}
// 连接
public function onConnect(TcpConnection $connection)
{
$connection->send('Hello ' . $connection->getRemoteIp());
Log::info("客户端链接到主机: {$connection->getRemoteIp()}");
}
// 接收数据
public function onMessage(TcpConnection $connection, $data)
{
$connection->send('Server: ' . $data);
}
// 连接关闭
public function onClose(TcpConnection $connection)
{
echo "Connection closed\n";
}
}
+264
View File
@@ -0,0 +1,264 @@
<?php
namespace app\utils;
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 string $message 日志消息
* @param array|Throwable $context 占位符参数或异常对象
* @param string $name 日志器名称
*/
public static function InfoMsg(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);
Log::info($processedMessage);
}
/**
* 调试级别日志
* @param string $message 日志消息(支持 {} 占位符)
* @param array|Throwable $context 占位符替换参数或异常对象
*/
public function debug(string $message, array|Throwable $context = []): void
{
$processedMessage = $this->buildMessage($message, $context);
Log::debug($processedMessage);
}
/**
* 警告级别日志
* @param string $message 日志消息(支持 {} 占位符)
* @param array|Throwable $context 占位符替换参数或异常对象
*/
public function warning(string $message, array|Throwable $context = []): void
{
$processedMessage = $this->buildMessage($message, $context);
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);
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);
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}>";
return $namePrefix . $processedMessage;
}
/**
* 格式化异常堆栈:让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;
}
}
+14
View File
@@ -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>