mirror of
https://devops.lemonos.cn/lawson/FendxPHP.git
synced 2026-06-15 23:12:49 +08:00
feat(database): 添加用户角色权限系统及相关监控功能
- 创建用户表(users)包含基本信息和认证字段 - 创建角色表(roles)用于权限控制 - 创建权限表(permissions)定义系统权限 - 创建用户角色关联表(user_roles)建立用户与角色关系 - 创建角色权限关联表(role_permissions)建立角色与权限关系 - 创建迁移记录表(migrations)追踪数据库变更 - 添加AdminController提供管理员面板功能 - 实现系统监控、配置管理、缓存清理等功能 - 添加AOP切面编程支持的各种通知类型 - 实现告警管理AlertManager支持多渠道告警 - 添加文档注解接口规范
This commit is contained in:
24
fendx-framework/fendx-web/composer.json
Normal file
24
fendx-framework/fendx-web/composer.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "fendx/web",
|
||||
"description": "FendxPHP Web Module - 路由、请求、响应、拦截器",
|
||||
"type": "library",
|
||||
"license": "MIT",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Lawson",
|
||||
"email": "lawson@fendx.cn"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": ">=8.1",
|
||||
"fendx/common": "^1.0",
|
||||
"fendx/core": "^1.0"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Fendx\\Web\\": "src/"
|
||||
}
|
||||
},
|
||||
"minimum-stability": "stable",
|
||||
"prefer-stable": true
|
||||
}
|
||||
15
fendx-framework/fendx-web/src/Annotation/GetRoute.php
Normal file
15
fendx-framework/fendx-web/src/Annotation/GetRoute.php
Normal file
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Fendx\Web\Annotation;
|
||||
|
||||
#[\Attribute(\Attribute::TARGET_METHOD)]
|
||||
final class GetRoute
|
||||
{
|
||||
public string $path;
|
||||
|
||||
public function __construct(string $path)
|
||||
{
|
||||
$this->path = $path;
|
||||
}
|
||||
}
|
||||
15
fendx-framework/fendx-web/src/Annotation/PostRoute.php
Normal file
15
fendx-framework/fendx-web/src/Annotation/PostRoute.php
Normal file
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Fendx\Web\Annotation;
|
||||
|
||||
#[\Attribute(\Attribute::TARGET_METHOD)]
|
||||
final class PostRoute
|
||||
{
|
||||
public string $path;
|
||||
|
||||
public function __construct(string $path)
|
||||
{
|
||||
$this->path = $path;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Fendx\Web\Interceptor;
|
||||
|
||||
use Fendx\Web\Request\Request;
|
||||
use Fendx\Security\Auth\Auth;
|
||||
|
||||
final class AuthInterceptor implements Interceptor
|
||||
{
|
||||
private array $excludeRoutes = [
|
||||
'/login',
|
||||
'/register',
|
||||
'/health',
|
||||
'/',
|
||||
];
|
||||
|
||||
public function before(Request $request): bool
|
||||
{
|
||||
$path = $request->path();
|
||||
|
||||
// 跳过不需要认证的路由
|
||||
if (in_array($path, $this->excludeRoutes)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 检查Authorization头
|
||||
$token = $request->header('Authorization');
|
||||
if (!$token) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 移除Bearer前缀
|
||||
if (str_starts_with($token, 'Bearer ')) {
|
||||
$token = substr($token, 7);
|
||||
}
|
||||
|
||||
// 验证token
|
||||
$user = Auth::authenticate($token);
|
||||
if (!$user) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function after(Request $request, mixed $result): mixed
|
||||
{
|
||||
// 可以在这里添加响应头信息
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function afterCompletion(Request $request, ?\Throwable $exception): void
|
||||
{
|
||||
// 记录请求日志
|
||||
if ($exception) {
|
||||
error_log("Request failed: " . $request->path() . " - " . $exception->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public function addExcludeRoute(string $route): void
|
||||
{
|
||||
if (!in_array($route, $this->excludeRoutes)) {
|
||||
$this->excludeRoutes[] = $route;
|
||||
}
|
||||
}
|
||||
|
||||
public function removeExcludeRoute(string $route): void
|
||||
{
|
||||
$key = array_search($route, $this->excludeRoutes);
|
||||
if ($key !== false) {
|
||||
unset($this->excludeRoutes[$key]);
|
||||
$this->excludeRoutes = array_values($this->excludeRoutes);
|
||||
}
|
||||
}
|
||||
}
|
||||
15
fendx-framework/fendx-web/src/Interceptor/Interceptor.php
Normal file
15
fendx-framework/fendx-web/src/Interceptor/Interceptor.php
Normal file
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Fendx\Web\Interceptor;
|
||||
|
||||
use Fendx\Web\Request\Request;
|
||||
|
||||
interface Interceptor
|
||||
{
|
||||
public function before(Request $request): bool;
|
||||
|
||||
public function after(Request $request, mixed $result): mixed;
|
||||
|
||||
public function afterCompletion(Request $request, ?\Throwable $exception): void;
|
||||
}
|
||||
163
fendx-framework/fendx-web/src/Interceptor/InterceptorManager.php
Normal file
163
fendx-framework/fendx-web/src/Interceptor/InterceptorManager.php
Normal file
@@ -0,0 +1,163 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Fendx\Web\Interceptor;
|
||||
|
||||
use Fendx\Web\Request\Request;
|
||||
use Fendx\Common\Exception\BusinessException;
|
||||
|
||||
final class InterceptorManager
|
||||
{
|
||||
private array $globalInterceptors = [];
|
||||
private array $routeInterceptors = [];
|
||||
private array $groupInterceptors = [];
|
||||
|
||||
public function addGlobalInterceptor(Interceptor $interceptor, int $priority = 0): void
|
||||
{
|
||||
$this->globalInterceptors[] = [
|
||||
'interceptor' => $interceptor,
|
||||
'priority' => $priority
|
||||
];
|
||||
|
||||
// 按优先级排序
|
||||
usort($this->globalInterceptors, fn($a, $b) => $b['priority'] - $a['priority']);
|
||||
}
|
||||
|
||||
public function addRouteInterceptor(string $route, Interceptor $interceptor, int $priority = 0): void
|
||||
{
|
||||
if (!isset($this->routeInterceptors[$route])) {
|
||||
$this->routeInterceptors[$route] = [];
|
||||
}
|
||||
|
||||
$this->routeInterceptors[$route][] = [
|
||||
'interceptor' => $interceptor,
|
||||
'priority' => $priority
|
||||
];
|
||||
|
||||
usort($this->routeInterceptors[$route], fn($a, $b) => $b['priority'] - $a['priority']);
|
||||
}
|
||||
|
||||
public function addGroupInterceptor(string $group, Interceptor $interceptor, int $priority = 0): void
|
||||
{
|
||||
if (!isset($this->groupInterceptors[$group])) {
|
||||
$this->groupInterceptors[$group] = [];
|
||||
}
|
||||
|
||||
$this->groupInterceptors[$group][] = [
|
||||
'interceptor' => $interceptor,
|
||||
'priority' => $priority
|
||||
];
|
||||
|
||||
usort($this->groupInterceptors[$group], fn($a, $b) => $b['priority'] - $a['priority']);
|
||||
}
|
||||
|
||||
public function executeBefore(Request $request, string $route, array $groups = []): void
|
||||
{
|
||||
$interceptors = $this->getApplicableInterceptors($route, $groups);
|
||||
|
||||
foreach ($interceptors as $interceptorData) {
|
||||
$interceptor = $interceptorData['interceptor'];
|
||||
|
||||
if (!$interceptor->before($request)) {
|
||||
throw new BusinessException(403, 'Request intercepted by ' . get_class($interceptor));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function executeAfter(Request $request, mixed $result, string $route, array $groups = []): mixed
|
||||
{
|
||||
$interceptors = $this->getApplicableInterceptors($route, $groups);
|
||||
|
||||
foreach ($interceptors as $interceptorData) {
|
||||
$interceptor = $interceptorData['interceptor'];
|
||||
$result = $interceptor->after($request, $result);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function executeAfterCompletion(Request $request, ?\Throwable $exception, string $route, array $groups = []): void
|
||||
{
|
||||
$interceptors = $this->getApplicableInterceptors($route, $groups);
|
||||
|
||||
foreach ($interceptors as $interceptorData) {
|
||||
$interceptor = $interceptorData['interceptor'];
|
||||
$interceptor->afterCompletion($request, $exception);
|
||||
}
|
||||
}
|
||||
|
||||
private function getApplicableInterceptors(string $route, array $groups): array
|
||||
{
|
||||
$interceptors = [];
|
||||
|
||||
// 添加全局拦截器
|
||||
$interceptors = array_merge($interceptors, $this->globalInterceptors);
|
||||
|
||||
// 添加分组拦截器
|
||||
foreach ($groups as $group) {
|
||||
if (isset($this->groupInterceptors[$group])) {
|
||||
$interceptors = array_merge($interceptors, $this->groupInterceptors[$group]);
|
||||
}
|
||||
}
|
||||
|
||||
// 添加路由拦截器
|
||||
if (isset($this->routeInterceptors[$route])) {
|
||||
$interceptors = array_merge($interceptors, $this->routeInterceptors[$route]);
|
||||
}
|
||||
|
||||
// 按优先级排序
|
||||
usort($interceptors, fn($a, $b) => $b['priority'] - $a['priority']);
|
||||
|
||||
return $interceptors;
|
||||
}
|
||||
|
||||
public function removeGlobalInterceptor(string $interceptorClass): void
|
||||
{
|
||||
$this->globalInterceptors = array_filter(
|
||||
$this->globalInterceptors,
|
||||
fn($item) => get_class($item['interceptor']) !== $interceptorClass
|
||||
);
|
||||
}
|
||||
|
||||
public function removeRouteInterceptor(string $route, string $interceptorClass): void
|
||||
{
|
||||
if (isset($this->routeInterceptors[$route])) {
|
||||
$this->routeInterceptors[$route] = array_filter(
|
||||
$this->routeInterceptors[$route],
|
||||
fn($item) => get_class($item['interceptor']) !== $interceptorClass
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public function removeGroupInterceptor(string $group, string $interceptorClass): void
|
||||
{
|
||||
if (isset($this->groupInterceptors[$group])) {
|
||||
$this->groupInterceptors[$group] = array_filter(
|
||||
$this->groupInterceptors[$group],
|
||||
fn($item) => get_class($item['interceptor']) !== $interceptorClass
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public function clear(): void
|
||||
{
|
||||
$this->globalInterceptors = [];
|
||||
$this->routeInterceptors = [];
|
||||
$this->groupInterceptors = [];
|
||||
}
|
||||
|
||||
public function getInterceptorsCount(): int
|
||||
{
|
||||
$count = count($this->globalInterceptors);
|
||||
|
||||
foreach ($this->routeInterceptors as $routeInterceptors) {
|
||||
$count += count($routeInterceptors);
|
||||
}
|
||||
|
||||
foreach ($this->groupInterceptors as $groupInterceptors) {
|
||||
$count += count($groupInterceptors);
|
||||
}
|
||||
|
||||
return $count;
|
||||
}
|
||||
}
|
||||
400
fendx-framework/fendx-web/src/Request/Request.php
Normal file
400
fendx-framework/fendx-web/src/Request/Request.php
Normal file
@@ -0,0 +1,400 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Fendx\Web\Request;
|
||||
|
||||
final class Request
|
||||
{
|
||||
private array $get;
|
||||
private array $post;
|
||||
private array $files;
|
||||
private array $server;
|
||||
private array $headers;
|
||||
private array $cookies;
|
||||
private ?string $rawBody;
|
||||
private array $attributes = [];
|
||||
private ?object $parsedBody = null;
|
||||
|
||||
public function __construct(
|
||||
array $get = [],
|
||||
array $post = [],
|
||||
array $files = [],
|
||||
array $server = [],
|
||||
array $cookies = [],
|
||||
array $headers = []
|
||||
) {
|
||||
$this->get = $get ?: $_GET;
|
||||
$this->post = $post ?: $_POST;
|
||||
$this->files = $files ?: $_FILES;
|
||||
$this->server = $server ?: $_SERVER;
|
||||
$this->cookies = $cookies ?: $_COOKIE;
|
||||
$this->headers = $headers ?: $this->getAllHeaders();
|
||||
$this->rawBody = file_get_contents('php://input');
|
||||
}
|
||||
|
||||
public static function createFromGlobals(): self
|
||||
{
|
||||
return new self();
|
||||
}
|
||||
|
||||
public function get(string $key, mixed $default = null): mixed
|
||||
{
|
||||
return $this->get[$key] ?? $default;
|
||||
}
|
||||
|
||||
public function post(string $key, mixed $default = null): mixed
|
||||
{
|
||||
return $this->post[$key] ?? $default;
|
||||
}
|
||||
|
||||
public function input(string $key, mixed $default = null): mixed
|
||||
{
|
||||
return $this->post($key) ?? $this->get($key) ?? $default;
|
||||
}
|
||||
|
||||
public function all(): array
|
||||
{
|
||||
return array_merge($this->get, $this->post);
|
||||
}
|
||||
|
||||
public function header(string $key, mixed $default = null): mixed
|
||||
{
|
||||
return $this->headers[strtolower($key)] ?? $default;
|
||||
}
|
||||
|
||||
public function cookie(string $key, mixed $default = null): mixed
|
||||
{
|
||||
return $this->cookies[$key] ?? $default;
|
||||
}
|
||||
|
||||
public function file(string $key): mixed
|
||||
{
|
||||
return $this->files[$key] ?? null;
|
||||
}
|
||||
|
||||
public function method(): string
|
||||
{
|
||||
return $this->server['REQUEST_METHOD'] ?? 'GET';
|
||||
}
|
||||
|
||||
public function uri(): string
|
||||
{
|
||||
return $this->server['REQUEST_URI'] ?? '/';
|
||||
}
|
||||
|
||||
public function path(): string
|
||||
{
|
||||
return parse_url($this->uri(), PHP_URL_PATH) ?: '/';
|
||||
}
|
||||
|
||||
public function isGet(): bool
|
||||
{
|
||||
return $this->method() === 'GET';
|
||||
}
|
||||
|
||||
public function isPost(): bool
|
||||
{
|
||||
return $this->method() === 'POST';
|
||||
}
|
||||
|
||||
public function isPut(): bool
|
||||
{
|
||||
return $this->method() === 'PUT';
|
||||
}
|
||||
|
||||
public function isDelete(): bool
|
||||
{
|
||||
return $this->method() === 'DELETE';
|
||||
}
|
||||
|
||||
public function isAjax(): bool
|
||||
{
|
||||
return strtolower($this->header('X-Requested-With') ?? '') === 'xmlhttprequest';
|
||||
}
|
||||
|
||||
public function wantsJson(): bool
|
||||
{
|
||||
return str_contains($this->header('Accept', ''), 'application/json');
|
||||
}
|
||||
|
||||
public function json(): mixed
|
||||
{
|
||||
if ($this->rawBody === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$decoded = json_decode($this->rawBody, true);
|
||||
return json_last_error() === JSON_ERROR_NONE ? $decoded : null;
|
||||
}
|
||||
|
||||
public function ip(): string
|
||||
{
|
||||
$ipKeys = ['HTTP_X_FORWARDED_FOR', 'HTTP_X_REAL_IP', 'HTTP_CLIENT_IP', 'REMOTE_ADDR'];
|
||||
|
||||
foreach ($ipKeys as $key) {
|
||||
$ip = $this->server[$key] ?? '';
|
||||
if ($ip && $ip !== 'unknown') {
|
||||
$ips = explode(',', $ip);
|
||||
return trim($ips[0]);
|
||||
}
|
||||
}
|
||||
|
||||
return '127.0.0.1';
|
||||
}
|
||||
|
||||
public function userAgent(): string
|
||||
{
|
||||
return $this->server['HTTP_USER_AGENT'] ?? '';
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置属性
|
||||
*/
|
||||
public function setAttribute(string $key, mixed $value): void
|
||||
{
|
||||
$this->attributes[$key] = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取属性
|
||||
*/
|
||||
public function getAttribute(string $key, mixed $default = null): mixed
|
||||
{
|
||||
return $this->attributes[$key] ?? $default;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有属性
|
||||
*/
|
||||
public function getAttributes(): array
|
||||
{
|
||||
return $this->attributes;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否有输入参数
|
||||
*/
|
||||
public function has(string $key): bool
|
||||
{
|
||||
return isset($this->get[$key]) || isset($this->post[$key]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有GET参数
|
||||
*/
|
||||
public function query(): array
|
||||
{
|
||||
return $this->get;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有POST参数
|
||||
*/
|
||||
public function body(): array
|
||||
{
|
||||
return $this->post;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取原始请求体
|
||||
*/
|
||||
public function getRawBody(): string
|
||||
{
|
||||
return $this->rawBody ?: '';
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取解析后的请求体
|
||||
*/
|
||||
public function getParsedBody(): ?object
|
||||
{
|
||||
if ($this->parsedBody === null) {
|
||||
$rawBody = $this->getRawBody();
|
||||
$contentType = $this->header('Content-Type', '');
|
||||
|
||||
if (str_contains($contentType, 'application/json')) {
|
||||
$this->parsedBody = json_decode($rawBody);
|
||||
} elseif (str_contains($contentType, 'application/xml')) {
|
||||
$this->parsedBody = simplexml_load_string($rawBody);
|
||||
} else {
|
||||
$this->parsedBody = (object) [];
|
||||
}
|
||||
}
|
||||
|
||||
return $this->parsedBody;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取内容类型
|
||||
*/
|
||||
public function getContentType(): string
|
||||
{
|
||||
return $this->header('Content-Type', '');
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否为HTTPS请求
|
||||
*/
|
||||
public function isSecure(): bool
|
||||
{
|
||||
return ($this->server['HTTPS'] ?? '') !== '' &&
|
||||
($this->server['HTTPS'] ?? '') !== 'off' ||
|
||||
($this->server['HTTP_X_FORWARDED_PROTO'] ?? '') === 'https' ||
|
||||
($this->server['HTTP_X_FORWARDED_SSL'] ?? '') === 'on';
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否为PATCH请求
|
||||
*/
|
||||
public function isPatch(): bool
|
||||
{
|
||||
return $this->method() === 'PATCH';
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否为OPTIONS请求
|
||||
*/
|
||||
public function isOptions(): bool
|
||||
{
|
||||
return $this->method() === 'OPTIONS';
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取基础URL
|
||||
*/
|
||||
public function getBaseUrl(): string
|
||||
{
|
||||
$scheme = $this->isSecure() ? 'https' : 'http';
|
||||
$host = $this->server['HTTP_HOST'] ?? 'localhost';
|
||||
$port = $this->server['SERVER_PORT'] ?? '';
|
||||
|
||||
$url = $scheme . '://' . $host;
|
||||
|
||||
if (($scheme === 'http' && $port !== '80') || ($scheme === 'https' && $port !== '443')) {
|
||||
$url .= ':' . $port;
|
||||
}
|
||||
|
||||
return $url;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取完整URL
|
||||
*/
|
||||
public function getFullUrl(): string
|
||||
{
|
||||
return $this->getBaseUrl() . $this->uri();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取查询字符串
|
||||
*/
|
||||
public function queryString(): string
|
||||
{
|
||||
return parse_url($this->uri(), PHP_URL_QUERY) ?: '';
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取服务器变量
|
||||
*/
|
||||
public function server(string $key, mixed $default = null): mixed
|
||||
{
|
||||
return $this->server[$key] ?? $default;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有请求头
|
||||
*/
|
||||
public function headers(): array
|
||||
{
|
||||
return $this->headers;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有Cookie
|
||||
*/
|
||||
public function cookies(): array
|
||||
{
|
||||
return $this->cookies;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有上传文件
|
||||
*/
|
||||
public function files(): array
|
||||
{
|
||||
return $this->files;
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证请求方法
|
||||
*/
|
||||
public function isMethod(string $method): bool
|
||||
{
|
||||
return strtoupper($this->method()) === strtoupper($method);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取客户端IP(更完善的版本)
|
||||
*/
|
||||
public function getClientIp(): string
|
||||
{
|
||||
$ipKeys = [
|
||||
'HTTP_CLIENT_IP',
|
||||
'HTTP_X_FORWARDED_FOR',
|
||||
'HTTP_X_FORWARDED',
|
||||
'HTTP_FORWARDED_FOR',
|
||||
'HTTP_FORWARDED',
|
||||
'REMOTE_ADDR'
|
||||
];
|
||||
|
||||
foreach ($ipKeys as $key) {
|
||||
if (array_key_exists($key, $this->server) === true) {
|
||||
foreach (explode(',', $this->server[$key]) as $ip) {
|
||||
$ip = trim($ip);
|
||||
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) !== false) {
|
||||
return $ip;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $this->server['REMOTE_ADDR'] ?? '127.0.0.1';
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换为数组
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'method' => $this->method(),
|
||||
'uri' => $this->uri(),
|
||||
'path' => $this->path(),
|
||||
'query_string' => $this->queryString(),
|
||||
'get' => $this->get,
|
||||
'post' => $this->post,
|
||||
'files' => $this->files,
|
||||
'cookies' => $this->cookies,
|
||||
'headers' => $this->headers,
|
||||
'attributes' => $this->attributes,
|
||||
'ip' => $this->ip(),
|
||||
'client_ip' => $this->getClientIp(),
|
||||
'user_agent' => $this->userAgent(),
|
||||
'is_ajax' => $this->isAjax(),
|
||||
'wants_json' => $this->wantsJson(),
|
||||
'is_secure' => $this->isSecure(),
|
||||
];
|
||||
}
|
||||
|
||||
private function getAllHeaders(): array
|
||||
{
|
||||
$headers = [];
|
||||
foreach ($this->server as $key => $value) {
|
||||
if (str_starts_with($key, 'HTTP_')) {
|
||||
$header = str_replace(' ', '-', ucwords(str_replace('_', ' ', strtolower(substr($key, 5)))));
|
||||
$headers[strtolower($header)] = $value;
|
||||
}
|
||||
}
|
||||
return $headers;
|
||||
}
|
||||
}
|
||||
398
fendx-framework/fendx-web/src/Response/HttpResponse.php
Normal file
398
fendx-framework/fendx-web/src/Response/HttpResponse.php
Normal file
@@ -0,0 +1,398 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Fendx\Web\Response;
|
||||
|
||||
/**
|
||||
* HTTP响应类
|
||||
* 封装HTTP响应的相关操作
|
||||
*/
|
||||
class HttpResponse
|
||||
{
|
||||
private string $content = '';
|
||||
private int $statusCode = 200;
|
||||
private array $headers = [];
|
||||
private string $version = '1.1';
|
||||
private ?string $charset = null;
|
||||
|
||||
/**
|
||||
* 标准HTTP状态码
|
||||
*/
|
||||
const STATUS_CODES = [
|
||||
100 => 'Continue',
|
||||
101 => 'Switching Protocols',
|
||||
200 => 'OK',
|
||||
201 => 'Created',
|
||||
202 => 'Accepted',
|
||||
204 => 'No Content',
|
||||
301 => 'Moved Permanently',
|
||||
302 => 'Found',
|
||||
304 => 'Not Modified',
|
||||
400 => 'Bad Request',
|
||||
401 => 'Unauthorized',
|
||||
403 => 'Forbidden',
|
||||
404 => 'Not Found',
|
||||
405 => 'Method Not Allowed',
|
||||
409 => 'Conflict',
|
||||
422 => 'Unprocessable Entity',
|
||||
429 => 'Too Many Requests',
|
||||
500 => 'Internal Server Error',
|
||||
502 => 'Bad Gateway',
|
||||
503 => 'Service Unavailable',
|
||||
504 => 'Gateway Timeout',
|
||||
];
|
||||
|
||||
public function __construct(string $content = '', int $statusCode = 200, array $headers = [])
|
||||
{
|
||||
$this->content = $content;
|
||||
$this->statusCode = $statusCode;
|
||||
$this->headers = $headers;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建响应
|
||||
*/
|
||||
public static function create(string $content = '', int $statusCode = 200, array $headers = []): self
|
||||
{
|
||||
return new self($content, $statusCode, $headers);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置内容
|
||||
*/
|
||||
public function setContent(string $content): self
|
||||
{
|
||||
$this->content = $content;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取内容
|
||||
*/
|
||||
public function getContent(): string
|
||||
{
|
||||
return $this->content;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置状态码
|
||||
*/
|
||||
public function setStatusCode(int $statusCode): self
|
||||
{
|
||||
$this->statusCode = $statusCode;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取状态码
|
||||
*/
|
||||
public function getStatusCode(): int
|
||||
{
|
||||
return $this->statusCode;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取状态文本
|
||||
*/
|
||||
public function getStatusText(): string
|
||||
{
|
||||
return self::STATUS_CODES[$this->statusCode] ?? 'Unknown';
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置请求头
|
||||
*/
|
||||
public function setHeader(string $name, string $value): self
|
||||
{
|
||||
$this->headers[$name] = $value;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量设置请求头
|
||||
*/
|
||||
public function setHeaders(array $headers): self
|
||||
{
|
||||
$this->headers = array_merge($this->headers, $headers);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取请求头
|
||||
*/
|
||||
public function getHeaders(): array
|
||||
{
|
||||
return $this->headers;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定请求头
|
||||
*/
|
||||
public function getHeader(string $name, string $default = ''): string
|
||||
{
|
||||
return $this->headers[$name] ?? $default;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置内容类型
|
||||
*/
|
||||
public function setContentType(string $contentType, ?string $charset = null): self
|
||||
{
|
||||
$header = $contentType;
|
||||
if ($charset) {
|
||||
$header .= '; charset=' . $charset;
|
||||
$this->charset = $charset;
|
||||
}
|
||||
|
||||
return $this->setHeader('Content-Type', $header);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取内容类型
|
||||
*/
|
||||
public function getContentType(): string
|
||||
{
|
||||
return $this->getHeader('Content-Type');
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置字符集
|
||||
*/
|
||||
public function setCharset(string $charset): self
|
||||
{
|
||||
$this->charset = $charset;
|
||||
|
||||
$contentType = $this->getContentType();
|
||||
if ($contentType && strpos($contentType, 'charset') === false) {
|
||||
$this->setContentType($contentType, $charset);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取字符集
|
||||
*/
|
||||
public function getCharset(): ?string
|
||||
{
|
||||
return $this->charset;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置HTTP版本
|
||||
*/
|
||||
public function setVersion(string $version): self
|
||||
{
|
||||
$this->version = $version;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取HTTP版本
|
||||
*/
|
||||
public function getVersion(): string
|
||||
{
|
||||
return $this->version;
|
||||
}
|
||||
|
||||
/**
|
||||
* JSON响应
|
||||
*/
|
||||
public function json($data, int $statusCode = 200, array $headers = []): self
|
||||
{
|
||||
$headers['Content-Type'] = 'application/json';
|
||||
|
||||
if (is_array($data) || is_object($data)) {
|
||||
$content = json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
} else {
|
||||
$content = json_encode(['data' => $data]);
|
||||
}
|
||||
|
||||
return $this->setContent($content)
|
||||
->setStatusCode($statusCode)
|
||||
->setHeaders($headers);
|
||||
}
|
||||
|
||||
/**
|
||||
* 统一格式响应
|
||||
*/
|
||||
public function api($data = null, string $message = 'success', int $code = 0, int $statusCode = 200): self
|
||||
{
|
||||
$response = [
|
||||
'code' => $code,
|
||||
'message' => $message,
|
||||
'data' => $data,
|
||||
'timestamp' => time(),
|
||||
'traceId' => \Fendx\Core\Context\Context::getTraceId(),
|
||||
];
|
||||
|
||||
return $this->json($response, $statusCode);
|
||||
}
|
||||
|
||||
/**
|
||||
* 成功响应
|
||||
*/
|
||||
public function success($data = null, string $message = 'success'): self
|
||||
{
|
||||
return $this->api($data, $message, 0, 200);
|
||||
}
|
||||
|
||||
/**
|
||||
* 错误响应
|
||||
*/
|
||||
public function error(string $message = 'error', int $code = 400, $data = null): self
|
||||
{
|
||||
return $this->api($data, $message, $code, 400);
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页响应
|
||||
*/
|
||||
public function paginate(array $items, int $total, int $page, int $pageSize, string $message = 'success'): self
|
||||
{
|
||||
$data = [
|
||||
'items' => $items,
|
||||
'total' => $total,
|
||||
'page' => $page,
|
||||
'page_size' => $pageSize,
|
||||
'total_pages' => ceil($total / $pageSize),
|
||||
'has_more' => $page * $pageSize < $total
|
||||
];
|
||||
|
||||
return $this->api($data, $message, 0, 200);
|
||||
}
|
||||
|
||||
/**
|
||||
* 重定向响应
|
||||
*/
|
||||
public function redirect(string $url, int $statusCode = 302): self
|
||||
{
|
||||
return $this->setHeader('Location', $url)
|
||||
->setStatusCode($statusCode);
|
||||
}
|
||||
|
||||
/**
|
||||
* 文件下载响应
|
||||
*/
|
||||
public function download(string $filePath, string $fileName = null): self
|
||||
{
|
||||
if (!file_exists($filePath)) {
|
||||
return $this->error('File not found', 404);
|
||||
}
|
||||
|
||||
$fileName = $fileName ?: basename($filePath);
|
||||
$fileSize = filesize($filePath);
|
||||
|
||||
$this->setHeader('Content-Type', 'application/octet-stream')
|
||||
->setHeader('Content-Disposition', 'attachment; filename="' . $fileName . '"')
|
||||
->setHeader('Content-Length', (string) $fileSize)
|
||||
->setHeader('Cache-Control', 'no-cache, must-revalidate')
|
||||
->setHeader('Pragma', 'no-cache');
|
||||
|
||||
$this->setContent(file_get_contents($filePath));
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 文件响应
|
||||
*/
|
||||
public function file(string $filePath, string $contentType = null): self
|
||||
{
|
||||
if (!file_exists($filePath)) {
|
||||
return $this->error('File not found', 404);
|
||||
}
|
||||
|
||||
$fileName = basename($filePath);
|
||||
$fileSize = filesize($filePath);
|
||||
|
||||
if (!$contentType) {
|
||||
$finfo = finfo_open(FILEINFO_MIME_TYPE);
|
||||
$contentType = finfo_file($finfo, $filePath);
|
||||
finfo_close($finfo);
|
||||
}
|
||||
|
||||
$this->setHeader('Content-Type', $contentType)
|
||||
->setHeader('Content-Length', (string) $fileSize)
|
||||
->setContent(file_get_contents($filePath));
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置缓存控制
|
||||
*/
|
||||
public function setCache(int $maxAge = 3600, bool $public = true): self
|
||||
{
|
||||
$control = $public ? 'public' : 'private';
|
||||
$control .= ', max-age=' . $maxAge;
|
||||
|
||||
return $this->setHeader('Cache-Control', $control)
|
||||
->setHeader('Expires', gmdate('D, d M Y H:i:s', time() + $maxAge) . ' GMT');
|
||||
}
|
||||
|
||||
/**
|
||||
* 禁用缓存
|
||||
*/
|
||||
public function setNoCache(): self
|
||||
{
|
||||
return $this->setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, post-check=0, pre-check=0')
|
||||
->setHeader('Pragma', 'no-cache')
|
||||
->setHeader('Expires', 'Thu, 19 Nov 1981 08:52:00 GMT');
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置CORS
|
||||
*/
|
||||
public function setCors(string $origin = '*', array $methods = ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], array $headers = []): self
|
||||
{
|
||||
return $this->setHeader('Access-Control-Allow-Origin', $origin)
|
||||
->setHeader('Access-Control-Allow-Methods', implode(', ', $methods))
|
||||
->setHeader('Access-Control-Allow-Headers', empty($headers) ? 'Content-Type, Authorization' : implode(', ', $headers));
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送响应
|
||||
*/
|
||||
public function send(): void
|
||||
{
|
||||
// 发送状态行
|
||||
header(sprintf('HTTP/%s %d %s', $this->version, $this->statusCode, $this->getStatusText()));
|
||||
|
||||
// 发送请求头
|
||||
foreach ($this->headers as $name => $value) {
|
||||
header($name . ': ' . $value);
|
||||
}
|
||||
|
||||
// 发送内容
|
||||
echo $this->content;
|
||||
|
||||
// 如果需要,可以在这里添加终止逻辑
|
||||
if (function_exists('fastcgi_finish_request')) {
|
||||
fastcgi_finish_request();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换为数组
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'content' => $this->content,
|
||||
'status_code' => $this->statusCode,
|
||||
'status_text' => $this->getStatusText(),
|
||||
'headers' => $this->headers,
|
||||
'version' => $this->version,
|
||||
'charset' => $this->charset,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换为字符串
|
||||
*/
|
||||
public function __toString(): string
|
||||
{
|
||||
return $this->content;
|
||||
}
|
||||
}
|
||||
70
fendx-framework/fendx-web/src/Response/Response.php
Normal file
70
fendx-framework/fendx-web/src/Response/Response.php
Normal file
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Fendx\Web\Response;
|
||||
|
||||
final class Response
|
||||
{
|
||||
public static function success(mixed $data = null, string $message = 'Success'): array
|
||||
{
|
||||
return [
|
||||
'code' => 200,
|
||||
'message' => $message,
|
||||
'data' => $data,
|
||||
'traceId' => \Fendx\Core\Context\Context::getTraceId(),
|
||||
];
|
||||
}
|
||||
|
||||
public static function error(int $code, string $message, mixed $data = null): array
|
||||
{
|
||||
return [
|
||||
'code' => $code,
|
||||
'message' => $message,
|
||||
'data' => $data,
|
||||
'traceId' => \Fendx\Core\Context\Context::getTraceId(),
|
||||
];
|
||||
}
|
||||
|
||||
public static function paginated(array $items, int $total, int $page, int $pageSize, string $message = 'Success'): array
|
||||
{
|
||||
return [
|
||||
'code' => 200,
|
||||
'message' => $message,
|
||||
'data' => [
|
||||
'items' => $items,
|
||||
'pagination' => [
|
||||
'total' => $total,
|
||||
'page' => $page,
|
||||
'pageSize' => $pageSize,
|
||||
'totalPages' => ceil($total / $pageSize),
|
||||
]
|
||||
],
|
||||
'traceId' => \Fendx\Core\Context\Context::getTraceId(),
|
||||
];
|
||||
}
|
||||
|
||||
public static function notFound(string $message = 'Resource not found'): array
|
||||
{
|
||||
return self::error(404, $message);
|
||||
}
|
||||
|
||||
public static function unauthorized(string $message = 'Unauthorized'): array
|
||||
{
|
||||
return self::error(401, $message);
|
||||
}
|
||||
|
||||
public static function forbidden(string $message = 'Forbidden'): array
|
||||
{
|
||||
return self::error(403, $message);
|
||||
}
|
||||
|
||||
public static function validationError(string $message = 'Validation failed', array $errors = []): array
|
||||
{
|
||||
return self::error(422, $message, $errors);
|
||||
}
|
||||
|
||||
public static function serverError(string $message = 'Internal server error'): array
|
||||
{
|
||||
return self::error(500, $message);
|
||||
}
|
||||
}
|
||||
197
fendx-framework/fendx-web/src/Route/Router.php
Normal file
197
fendx-framework/fendx-web/src/Route/Router.php
Normal file
@@ -0,0 +1,197 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Fendx\Web\Route;
|
||||
|
||||
use Fendx\Common\Exception\BusinessException;
|
||||
use Fendx\Web\Request\Request;
|
||||
use Fendx\Web\Response\Response;
|
||||
use Fendx\Web\Interceptor\InterceptorManager;
|
||||
|
||||
final class Router
|
||||
{
|
||||
private array $routes = [];
|
||||
private array $methods = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'];
|
||||
private ?InterceptorManager $interceptorManager = null;
|
||||
|
||||
public function get(string $path, mixed $handler): void
|
||||
{
|
||||
$this->addRoute('GET', $path, $handler);
|
||||
}
|
||||
|
||||
public function post(string $path, mixed $handler): void
|
||||
{
|
||||
$this->addRoute('POST', $path, $handler);
|
||||
}
|
||||
|
||||
public function put(string $path, mixed $handler): void
|
||||
{
|
||||
$this->addRoute('PUT', $path, $handler);
|
||||
}
|
||||
|
||||
public function delete(string $path, mixed $handler): void
|
||||
{
|
||||
$this->addRoute('DELETE', $path, $handler);
|
||||
}
|
||||
|
||||
public function patch(string $path, mixed $handler): void
|
||||
{
|
||||
$this->addRoute('PATCH', $path, $handler);
|
||||
}
|
||||
|
||||
public function options(string $path, mixed $handler): void
|
||||
{
|
||||
$this->addRoute('OPTIONS', $path, $handler);
|
||||
}
|
||||
|
||||
private function addRoute(string $method, string $path, mixed $handler): void
|
||||
{
|
||||
if (!in_array($method, $this->methods)) {
|
||||
throw new BusinessException(500, 'INVALID_HTTP_METHOD', ['method' => $method]);
|
||||
}
|
||||
|
||||
$this->routes[$method][$path] = $handler;
|
||||
}
|
||||
|
||||
public function dispatch(Request $request): Response
|
||||
{
|
||||
$method = $request->method();
|
||||
$path = $request->path();
|
||||
|
||||
if (!isset($this->routes[$method])) {
|
||||
return Response::error(405, 'Method not allowed');
|
||||
}
|
||||
|
||||
$handler = $this->findHandler($method, $path);
|
||||
if (!$handler) {
|
||||
return Response::error(404, 'Route not found');
|
||||
}
|
||||
|
||||
try {
|
||||
// 执行前置拦截器
|
||||
if ($this->interceptorManager) {
|
||||
$this->interceptorManager->executeBefore($request, $path);
|
||||
}
|
||||
|
||||
// 执行控制器方法
|
||||
$result = $this->executeHandler($handler, $request);
|
||||
|
||||
// 执行后置拦截器
|
||||
if ($this->interceptorManager) {
|
||||
$result = $this->interceptorManager->executeAfter($request, $result, $path);
|
||||
}
|
||||
|
||||
return $this->createResponse($result);
|
||||
|
||||
} catch (\Throwable $e) {
|
||||
// 执行完成拦截器
|
||||
if ($this->interceptorManager) {
|
||||
$this->interceptorManager->executeAfterCompletion($request, $e, $path);
|
||||
}
|
||||
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
private function findHandler(string $method, string $path): mixed
|
||||
{
|
||||
// 精确匹配
|
||||
if (isset($this->routes[$method][$path])) {
|
||||
return $this->routes[$method][$path];
|
||||
}
|
||||
|
||||
// 路径参数匹配
|
||||
foreach ($this->routes[$method] as $route => $handler) {
|
||||
if ($this->matchRoute($route, $path)) {
|
||||
return $handler;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function matchRoute(string $route, string $path): bool
|
||||
{
|
||||
// 简单的路径参数匹配,如 /user/{id}
|
||||
$routeParts = explode('/', trim($route, '/'));
|
||||
$pathParts = explode('/', trim($path, '/'));
|
||||
|
||||
if (count($routeParts) !== count($pathParts)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach ($routeParts as $i => $routePart) {
|
||||
if (str_starts_with($routePart, '{') && str_ends_with($routePart, '}')) {
|
||||
continue; // 路径参数
|
||||
}
|
||||
if ($routePart !== $pathParts[$i]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private function executeHandler(mixed $handler, Request $request): mixed
|
||||
{
|
||||
if (is_callable($handler)) {
|
||||
return $handler($request);
|
||||
}
|
||||
|
||||
if (is_array($handler) && count($handler) === 2) {
|
||||
[$controller, $method] = $handler;
|
||||
$controllerInstance = new $controller();
|
||||
return $controllerInstance->$method($request);
|
||||
}
|
||||
|
||||
throw new BusinessException(500, 'Invalid route handler');
|
||||
}
|
||||
|
||||
private function createResponse(mixed $result): Response
|
||||
{
|
||||
if ($result instanceof Response) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
if (is_array($result)) {
|
||||
return Response::success($result);
|
||||
}
|
||||
|
||||
return Response::success($result);
|
||||
}
|
||||
|
||||
public function setInterceptorManager(InterceptorManager $interceptorManager): void
|
||||
{
|
||||
$this->interceptorManager = $interceptorManager;
|
||||
}
|
||||
|
||||
public function getInterceptorManager(): ?InterceptorManager
|
||||
{
|
||||
return $this->interceptorManager;
|
||||
}
|
||||
|
||||
public function loadRoutes(string $routesFile): void
|
||||
{
|
||||
if (file_exists($routesFile)) {
|
||||
$routes = require $routesFile;
|
||||
if (is_array($routes)) {
|
||||
foreach ($routes as $method => $methodRoutes) {
|
||||
foreach ($methodRoutes as $path => $handler) {
|
||||
$this->addRoute(strtoupper($method), $path, $handler);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function getRoutes(): array
|
||||
{
|
||||
return $this->routes;
|
||||
}
|
||||
|
||||
public function clear(): void
|
||||
{
|
||||
$this->routes = [];
|
||||
$this->interceptorManager = null;
|
||||
}
|
||||
}
|
||||
292
fendx-framework/fendx-web/src/Router/Route.php
Normal file
292
fendx-framework/fendx-web/src/Router/Route.php
Normal file
@@ -0,0 +1,292 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Fendx\Web\Router;
|
||||
|
||||
/**
|
||||
* 路由类
|
||||
* 定义单个路由的信息
|
||||
*/
|
||||
class Route
|
||||
{
|
||||
private string $method;
|
||||
private string $path;
|
||||
private $handler;
|
||||
private array $middlewares = [];
|
||||
private string $name;
|
||||
private array $parameters = [];
|
||||
private array $where = [];
|
||||
|
||||
public function __construct(string $method, string $path, $handler, string $name = '')
|
||||
{
|
||||
$this->method = strtoupper($method);
|
||||
$this->path = $path;
|
||||
$this->handler = $handler;
|
||||
$this->name = $name ?: $this->generateDefaultName();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取HTTP方法
|
||||
*/
|
||||
public function getMethod(): string
|
||||
{
|
||||
return $this->method;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取路径
|
||||
*/
|
||||
public function getPath(): string
|
||||
{
|
||||
return $this->path;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取处理器
|
||||
*/
|
||||
public function getHandler()
|
||||
{
|
||||
return $this->handler;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取中间件
|
||||
*/
|
||||
public function getMiddlewares(): array
|
||||
{
|
||||
return $this->middlewares;
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加中间件
|
||||
*/
|
||||
public function middleware($middleware): self
|
||||
{
|
||||
$this->middlewares[] = $middleware;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量添加中间件
|
||||
*/
|
||||
public function middlewares(array $middlewares): self
|
||||
{
|
||||
$this->middlewares = array_merge($this->middlewares, $middlewares);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取路由名称
|
||||
*/
|
||||
public function getName(): string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置路由名称
|
||||
*/
|
||||
public function name(string $name): self
|
||||
{
|
||||
$this->name = $name;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取参数
|
||||
*/
|
||||
public function getParameters(): array
|
||||
{
|
||||
return $this->parameters;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置参数
|
||||
*/
|
||||
public function setParameters(array $parameters): void
|
||||
{
|
||||
$this->parameters = $parameters;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置参数约束
|
||||
*/
|
||||
public function where(string $param, string $pattern): self
|
||||
{
|
||||
$this->where[$param] = $pattern;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量设置参数约束
|
||||
*/
|
||||
public function wheres(array $wheres): self
|
||||
{
|
||||
$this->where = array_merge($this->where, $wheres);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取参数约束
|
||||
*/
|
||||
public function getWhere(): array
|
||||
{
|
||||
return $this->where;
|
||||
}
|
||||
|
||||
/**
|
||||
* 编译路由为正则表达式
|
||||
*/
|
||||
public function compile(): string
|
||||
{
|
||||
$pattern = $this->path;
|
||||
|
||||
// 转换路径参数为正则表达式
|
||||
$pattern = preg_replace_callback('/\{(\w+)(?:\?|:([^}]+))?\}/', function ($matches) {
|
||||
$param = $matches[1];
|
||||
$constraint = $matches[2] ?? '[^/]+';
|
||||
|
||||
// 如果有问号,表示可选参数
|
||||
$optional = strpos($matches[0], '?') !== false;
|
||||
|
||||
if ($optional) {
|
||||
return '(?:/([^/]+))?';
|
||||
} else {
|
||||
return '/([^/]+)';
|
||||
}
|
||||
}, $pattern);
|
||||
|
||||
// 处理通配符
|
||||
$pattern = str_replace('*', '.*', $pattern);
|
||||
|
||||
return '#^' . $pattern . '$#';
|
||||
}
|
||||
|
||||
/**
|
||||
* 匹配请求路径
|
||||
*/
|
||||
public function match(string $method, string $path): bool
|
||||
{
|
||||
if ($this->method !== '*' && $this->method !== strtoupper($method)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$pattern = $this->compile();
|
||||
|
||||
if (!preg_match($pattern, $path, $matches)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 提取参数
|
||||
$this->extractParameters($path, $matches);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 提取路径参数
|
||||
*/
|
||||
private function extractParameters(string $path, array $matches): void
|
||||
{
|
||||
$this->parameters = [];
|
||||
|
||||
// 解析路径中的参数名
|
||||
preg_match_all('/\{(\w+)(?:\?|:[^}]+)?\}/', $this->path, $paramMatches);
|
||||
$paramNames = $paramMatches[1];
|
||||
|
||||
// 提取参数值
|
||||
for ($i = 1; $i < count($matches); $i++) {
|
||||
if (isset($paramNames[$i - 1]) && $matches[$i] !== null) {
|
||||
$paramName = $paramNames[$i - 1];
|
||||
$paramValue = $matches[$i];
|
||||
|
||||
// 验证参数约束
|
||||
if (isset($this->where[$paramName])) {
|
||||
$pattern = '/^' . $this->where[$paramName] . '$/';
|
||||
if (!preg_match($pattern, $paramValue)) {
|
||||
return; // 参数不匹配
|
||||
}
|
||||
}
|
||||
|
||||
$this->parameters[$paramName] = $paramValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成默认路由名称
|
||||
*/
|
||||
private function generateDefaultName(): string
|
||||
{
|
||||
$handler = $this->handler;
|
||||
|
||||
if (is_string($handler)) {
|
||||
return $handler;
|
||||
}
|
||||
|
||||
if (is_array($handler)) {
|
||||
$class = is_object($handler[0]) ? get_class($handler[0]) : $handler[0];
|
||||
$method = $handler[1] ?? '__invoke';
|
||||
return $class . '@' . $method;
|
||||
}
|
||||
|
||||
if (is_object($handler)) {
|
||||
return get_class($handler) . '@__invoke';
|
||||
}
|
||||
|
||||
return 'route_' . uniqid();
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成URL
|
||||
*/
|
||||
public function url(array $parameters = []): string
|
||||
{
|
||||
$url = $this->path;
|
||||
|
||||
// 替换路径参数
|
||||
foreach ($parameters as $key => $value) {
|
||||
$url = str_replace('{' . $key . '}', (string) $value, $url);
|
||||
$url = str_replace('{' . $key . '?}', (string) $value, $url);
|
||||
}
|
||||
|
||||
// 移除未替换的可选参数
|
||||
$url = preg_replace('/\{\w+\?\}/', '', $url);
|
||||
|
||||
// 移除未替换的必需参数(应该抛出异常)
|
||||
if (preg_match('/\{\w+\}/', $url)) {
|
||||
throw new \InvalidArgumentException('Missing required parameters for route: ' . $this->name);
|
||||
}
|
||||
|
||||
return $url;
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换为数组
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'method' => $this->method,
|
||||
'path' => $this->path,
|
||||
'handler' => $this->handler,
|
||||
'name' => $this->name,
|
||||
'middlewares' => $this->middlewares,
|
||||
'where' => $this->where,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 路由信息调试输出
|
||||
*/
|
||||
public function __toString(): string
|
||||
{
|
||||
return sprintf(
|
||||
'%s %s -> %s [%s]',
|
||||
$this->method,
|
||||
$this->path,
|
||||
$this->name,
|
||||
implode(', ', $this->middlewares)
|
||||
);
|
||||
}
|
||||
}
|
||||
326
fendx-framework/fendx-web/src/Router/RouteCollection.php
Normal file
326
fendx-framework/fendx-web/src/Router/RouteCollection.php
Normal file
@@ -0,0 +1,326 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Fendx\Web\Router;
|
||||
|
||||
/**
|
||||
* 路由集合类
|
||||
* 管理所有路由
|
||||
*/
|
||||
class RouteCollection
|
||||
{
|
||||
private array $routes = [];
|
||||
private array $namedRoutes = [];
|
||||
private array $groupStack = [];
|
||||
|
||||
/**
|
||||
* 添加路由
|
||||
*/
|
||||
public function add(Route $route): Route
|
||||
{
|
||||
$this->routes[] = $route;
|
||||
|
||||
// 添加到命名路由索引
|
||||
if ($route->getName()) {
|
||||
$this->namedRoutes[$route->getName()] = $route;
|
||||
}
|
||||
|
||||
return $route;
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加GET路由
|
||||
*/
|
||||
public function get(string $path, $handler, string $name = ''): Route
|
||||
{
|
||||
$route = new Route('GET', $path, $handler, $name);
|
||||
return $this->add($this->applyGroupAttributes($route));
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加POST路由
|
||||
*/
|
||||
public function post(string $path, $handler, string $name = ''): Route
|
||||
{
|
||||
$route = new Route('POST', $path, $handler, $name);
|
||||
return $this->add($this->applyGroupAttributes($route));
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加PUT路由
|
||||
*/
|
||||
public function put(string $path, $handler, string $name = ''): Route
|
||||
{
|
||||
$route = new Route('PUT', $path, $handler, $name);
|
||||
return $this->add($this->applyGroupAttributes($route));
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加DELETE路由
|
||||
*/
|
||||
public function delete(string $path, $handler, string $name = ''): Route
|
||||
{
|
||||
$route = new Route('DELETE', $path, $handler, $name);
|
||||
return $this->add($this->applyGroupAttributes($route));
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加PATCH路由
|
||||
*/
|
||||
public function patch(string $path, $handler, string $name = ''): Route
|
||||
{
|
||||
$route = new Route('PATCH', $path, $handler, $name);
|
||||
return $this->add($this->applyGroupAttributes($route));
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加OPTIONS路由
|
||||
*/
|
||||
public function options(string $path, $handler, string $name = ''): Route
|
||||
{
|
||||
$route = new Route('OPTIONS', $path, $handler, $name);
|
||||
return $this->add($this->applyGroupAttributes($route));
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加任意方法路由
|
||||
*/
|
||||
public function any(string $path, $handler, string $name = ''): Route
|
||||
{
|
||||
$route = new Route('*', $path, $handler, $name);
|
||||
return $this->add($this->applyGroupAttributes($route));
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加匹配多种方法的路由
|
||||
*/
|
||||
public function match(array $methods, string $path, $handler, string $name = ''): Route
|
||||
{
|
||||
$methods = array_map('strtoupper', $methods);
|
||||
$method = implode('|', $methods);
|
||||
$route = new Route($method, $path, $handler, $name);
|
||||
return $this->add($this->applyGroupAttributes($route));
|
||||
}
|
||||
|
||||
/**
|
||||
* 路由分组
|
||||
*/
|
||||
public function group(array $attributes, callable $callback): void
|
||||
{
|
||||
$this->updateGroupStack($attributes);
|
||||
|
||||
$callback($this);
|
||||
|
||||
array_pop($this->groupStack);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新分组栈
|
||||
*/
|
||||
protected function updateGroupStack(array $attributes): void
|
||||
{
|
||||
if (!empty($this->groupStack)) {
|
||||
$attributes = $this->mergeGroupAttributes($attributes, end($this->groupStack));
|
||||
}
|
||||
|
||||
$this->groupStack[] = $attributes;
|
||||
}
|
||||
|
||||
/**
|
||||
* 合并分组属性
|
||||
*/
|
||||
protected function mergeGroupAttributes(array $new, array $old): array
|
||||
{
|
||||
$attributes = [
|
||||
'namespace' => $new['namespace'] ?? $old['namespace'] ?? null,
|
||||
'prefix' => $new['prefix'] ?? $old['prefix'] ?? null,
|
||||
'middleware' => $new['middleware'] ?? $old['middleware'] ?? [],
|
||||
];
|
||||
|
||||
// 合并中间件
|
||||
if (isset($old['middleware']) && isset($new['middleware'])) {
|
||||
$attributes['middleware'] = array_merge(
|
||||
(array) $old['middleware'],
|
||||
(array) $new['middleware']
|
||||
);
|
||||
}
|
||||
|
||||
return array_filter($attributes);
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用分组属性到路由
|
||||
*/
|
||||
protected function applyGroupAttributes(Route $route): Route
|
||||
{
|
||||
if (empty($this->groupStack)) {
|
||||
return $route;
|
||||
}
|
||||
|
||||
$attributes = end($this->groupStack);
|
||||
|
||||
// 应用前缀
|
||||
if (isset($attributes['prefix'])) {
|
||||
$path = rtrim($attributes['prefix'], '/') . '/' . ltrim($route->getPath(), '/');
|
||||
$route = new Route(
|
||||
$route->getMethod(),
|
||||
$path,
|
||||
$route->getHandler(),
|
||||
$route->getName()
|
||||
);
|
||||
}
|
||||
|
||||
// 应用中间件
|
||||
if (isset($attributes['middleware'])) {
|
||||
$route->middlewares((array) $attributes['middleware']);
|
||||
}
|
||||
|
||||
// 应用命名空间
|
||||
if (isset($attributes['namespace'])) {
|
||||
$handler = $route->getHandler();
|
||||
if (is_string($handler) && strpos($handler, '\\') === false) {
|
||||
$handler = $attributes['namespace'] . '\\' . $handler;
|
||||
$route = new Route(
|
||||
$route->getMethod(),
|
||||
$route->getPath(),
|
||||
$handler,
|
||||
$route->getName()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return $route;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据名称获取路由
|
||||
*/
|
||||
public function getByName(string $name): ?Route
|
||||
{
|
||||
return $this->namedRoutes[$name] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 匹配路由
|
||||
*/
|
||||
public function match(string $method, string $path): ?Route
|
||||
{
|
||||
foreach ($this->routes as $route) {
|
||||
if ($route->match($method, $path)) {
|
||||
return $route;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有路由
|
||||
*/
|
||||
public function getRoutes(): array
|
||||
{
|
||||
return $this->routes;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取命名路由
|
||||
*/
|
||||
public function getNamedRoutes(): array
|
||||
{
|
||||
return $this->namedRoutes;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定方法的路由
|
||||
*/
|
||||
public function getRoutesByMethod(string $method): array
|
||||
{
|
||||
return array_filter($this->routes, function ($route) use ($method) {
|
||||
return $route->getMethod() === '*' ||
|
||||
$route->getMethod() === strtoupper($method) ||
|
||||
strpos($route->getMethod(), strtoupper($method)) !== false;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查路由是否存在
|
||||
*/
|
||||
public function has(string $name): bool
|
||||
{
|
||||
return isset($this->namedRoutes[$name]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成URL
|
||||
*/
|
||||
public function url(string $name, array $parameters = []): string
|
||||
{
|
||||
$route = $this->getByName($name);
|
||||
if (!$route) {
|
||||
throw new \InvalidArgumentException("Route '{$name}' not found.");
|
||||
}
|
||||
|
||||
return $route->url($parameters);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空所有路由
|
||||
*/
|
||||
public function clear(): void
|
||||
{
|
||||
$this->routes = [];
|
||||
$this->namedRoutes = [];
|
||||
$this->groupStack = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取路由数量
|
||||
*/
|
||||
public function count(): int
|
||||
{
|
||||
return count($this->routes);
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换为数组
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
return array_map(function ($route) {
|
||||
return $route->toArray();
|
||||
}, $this->routes);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取路由统计信息
|
||||
*/
|
||||
public function getStatistics(): array
|
||||
{
|
||||
$stats = [
|
||||
'total' => count($this->routes),
|
||||
'named' => count($this->namedRoutes),
|
||||
'by_method' => [],
|
||||
'with_middleware' => 0,
|
||||
];
|
||||
|
||||
foreach ($this->routes as $route) {
|
||||
$method = $route->getMethod();
|
||||
if (strpos($method, '|') !== false) {
|
||||
$methods = explode('|', $method);
|
||||
foreach ($methods as $m) {
|
||||
$stats['by_method'][$m] = ($stats['by_method'][$m] ?? 0) + 1;
|
||||
}
|
||||
} else {
|
||||
$stats['by_method'][$method] = ($stats['by_method'][$method] ?? 0) + 1;
|
||||
}
|
||||
|
||||
if (!empty($route->getMiddlewares())) {
|
||||
$stats['with_middleware']++;
|
||||
}
|
||||
}
|
||||
|
||||
ksort($stats['by_method']);
|
||||
|
||||
return $stats;
|
||||
}
|
||||
}
|
||||
152
fendx-framework/fendx-web/src/Scanner/RouteScanner.php
Normal file
152
fendx-framework/fendx-web/src/Scanner/RouteScanner.php
Normal file
@@ -0,0 +1,152 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Fendx\Web\Scanner;
|
||||
|
||||
use ReflectionClass;
|
||||
use ReflectionMethod;
|
||||
use ReflectionException;
|
||||
use Fendx\Core\Annotation\Controller;
|
||||
use Fendx\Web\Annotation\GetRoute;
|
||||
use Fendx\Web\Annotation\PostRoute;
|
||||
use Fendx\Web\Route\Router;
|
||||
|
||||
final class RouteScanner
|
||||
{
|
||||
private Router $router;
|
||||
private array $controllers = [];
|
||||
|
||||
public function __construct(Router $router)
|
||||
{
|
||||
$this->router = $router;
|
||||
}
|
||||
|
||||
public function scan(string $scanPath): void
|
||||
{
|
||||
$this->scanControllers($scanPath);
|
||||
$this->registerRoutes();
|
||||
}
|
||||
|
||||
private function scanControllers(string $path): void
|
||||
{
|
||||
if (!is_dir($path)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$files = glob($path . '/**/*.php');
|
||||
|
||||
foreach ($files as $file) {
|
||||
$this->scanControllerFile($file);
|
||||
}
|
||||
}
|
||||
|
||||
private function scanControllerFile(string $file): void
|
||||
{
|
||||
$className = $this->getClassNameFromFile($file);
|
||||
|
||||
if ($className === null || class_exists($className) === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$reflection = new ReflectionClass($className);
|
||||
|
||||
// 检查是否有Controller注解
|
||||
$controllerAttributes = $reflection->getAttributes(Controller::class);
|
||||
if (empty($controllerAttributes)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$controllerAttribute = $controllerAttributes[0]->newInstance();
|
||||
$prefix = $controllerAttribute->prefix;
|
||||
|
||||
// 扫描方法上的路由注解
|
||||
$this->scanControllerMethods($className, $reflection, $prefix);
|
||||
|
||||
} catch (ReflectionException $e) {
|
||||
error_log("Failed to scan controller $className: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private function scanControllerMethods(string $className, ReflectionClass $reflection, string $prefix): void
|
||||
{
|
||||
$methods = $reflection->getMethods(ReflectionMethod::IS_PUBLIC);
|
||||
|
||||
foreach ($methods as $method) {
|
||||
if ($method->getName() === '__construct') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->scanMethodRoutes($className, $method, $prefix);
|
||||
}
|
||||
}
|
||||
|
||||
private function scanMethodRoutes(string $className, ReflectionMethod $method, string $prefix): void
|
||||
{
|
||||
// 扫描GetRoute注解
|
||||
$getRouteAttributes = $method->getAttributes(GetRoute::class);
|
||||
foreach ($getRouteAttributes as $attribute) {
|
||||
$routeAttribute = $attribute->newInstance();
|
||||
$path = $prefix . $routeAttribute->path;
|
||||
$this->controllers[] = [
|
||||
'method' => 'GET',
|
||||
'path' => $path,
|
||||
'controller' => $className,
|
||||
'action' => $method->getName()
|
||||
];
|
||||
}
|
||||
|
||||
// 扫描PostRoute注解
|
||||
$postRouteAttributes = $method->getAttributes(PostRoute::class);
|
||||
foreach ($postRouteAttributes as $attribute) {
|
||||
$routeAttribute = $attribute->newInstance();
|
||||
$path = $prefix . $routeAttribute->path;
|
||||
$this->controllers[] = [
|
||||
'method' => 'POST',
|
||||
'path' => $path,
|
||||
'controller' => $className,
|
||||
'action' => $method->getName()
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
private function registerRoutes(): void
|
||||
{
|
||||
foreach ($this->controllers as $route) {
|
||||
$handler = [$route['controller'], $route['action']];
|
||||
|
||||
switch ($route['method']) {
|
||||
case 'GET':
|
||||
$this->router->get($route['path'], $handler);
|
||||
break;
|
||||
case 'POST':
|
||||
$this->router->post($route['path'], $handler);
|
||||
break;
|
||||
case 'PUT':
|
||||
$this->router->put($route['path'], $handler);
|
||||
break;
|
||||
case 'DELETE':
|
||||
$this->router->delete($route['path'], $handler);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function getClassNameFromFile(string $file): ?string
|
||||
{
|
||||
$content = file_get_contents($file);
|
||||
|
||||
if (preg_match('/namespace\s+([^;]+);/', $content, $matches)) {
|
||||
$namespace = trim($matches[1]);
|
||||
$className = basename($file, '.php');
|
||||
return $namespace . '\\' . $className;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function getRoutes(): array
|
||||
{
|
||||
return $this->controllers;
|
||||
}
|
||||
}
|
||||
196
fendx-framework/fendx-web/src/Validator/Validator.php
Normal file
196
fendx-framework/fendx-web/src/Validator/Validator.php
Normal file
@@ -0,0 +1,196 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Fendx\Web\Validator;
|
||||
|
||||
use Fendx\Common\Exception\BusinessException;
|
||||
|
||||
final class Validator
|
||||
{
|
||||
private array $rules = [];
|
||||
private array $messages = [];
|
||||
private array $data = [];
|
||||
private array $errors = [];
|
||||
|
||||
public static function make(array $data, array $rules, array $messages = []): self
|
||||
{
|
||||
$validator = new self();
|
||||
$validator->data = $data;
|
||||
$validator->rules = $rules;
|
||||
$validator->messages = $messages;
|
||||
return $validator;
|
||||
}
|
||||
|
||||
public function validate(): bool
|
||||
{
|
||||
$this->errors = [];
|
||||
|
||||
foreach ($this->rules as $field => $fieldRules) {
|
||||
$value = $this->data[$field] ?? null;
|
||||
$rules = is_string($fieldRules) ? explode('|', $fieldRules) : $fieldRules;
|
||||
|
||||
foreach ($rules as $rule) {
|
||||
if (!$this->validateRule($field, $value, $rule)) {
|
||||
break; // 如果某个规则失败,停止验证该字段的后续规则
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return empty($this->errors);
|
||||
}
|
||||
|
||||
private function validateRule(string $field, mixed $value, string $rule): bool
|
||||
{
|
||||
// 解析规则参数,如 "max:255"
|
||||
if (str_contains($rule, ':')) {
|
||||
[$ruleName, $parameter] = explode(':', $rule, 2);
|
||||
} else {
|
||||
$ruleName = $rule;
|
||||
$parameter = null;
|
||||
}
|
||||
|
||||
return match ($ruleName) {
|
||||
'required' => $this->validateRequired($field, $value),
|
||||
'email' => $this->validateEmail($field, $value),
|
||||
'mobile' => $this->validateMobile($field, $value),
|
||||
'id_card' => $this->validateIdCard($field, $value),
|
||||
'min' => $this->validateMin($field, $value, (int)$parameter),
|
||||
'max' => $this->validateMax($field, $value, (int)$parameter),
|
||||
'length' => $this->validateLength($field, $value, (int)$parameter),
|
||||
'in' => $this->validateIn($field, $value, explode(',', $parameter)),
|
||||
'regex' => $this->validateRegex($field, $value, $parameter),
|
||||
default => true,
|
||||
};
|
||||
}
|
||||
|
||||
private function validateRequired(string $field, mixed $value): bool
|
||||
{
|
||||
if ($value === null || $value === '' || $value === []) {
|
||||
$this->addError($field, 'required', 'The :field field is required.');
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private function validateEmail(string $field, mixed $value): bool
|
||||
{
|
||||
if ($value !== null && $value !== '' && !filter_var($value, FILTER_VALIDATE_EMAIL)) {
|
||||
$this->addError($field, 'email', 'The :field must be a valid email address.');
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private function validateMobile(string $field, mixed $value): bool
|
||||
{
|
||||
if ($value !== null && $value !== '' && !preg_match('/^1[3-9]\d{9}$/', (string)$value)) {
|
||||
$this->addError($field, 'mobile', 'The :field must be a valid mobile number.');
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private function validateIdCard(string $field, mixed $value): bool
|
||||
{
|
||||
if ($value !== null && $value !== '') {
|
||||
$pattern = '/^[1-9]\d{5}(18|19|20)\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])\d{3}[\dXx]$/';
|
||||
if (!preg_match($pattern, (string)$value)) {
|
||||
$this->addError($field, 'id_card', 'The :field must be a valid ID card number.');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private function validateMin(string $field, mixed $value, int $min): bool
|
||||
{
|
||||
if (is_string($value) && strlen($value) < $min) {
|
||||
$this->addError($field, 'min', "The :field must be at least :min characters.", ['min' => $min]);
|
||||
return false;
|
||||
}
|
||||
if (is_numeric($value) && $value < $min) {
|
||||
$this->addError($field, 'min', "The :field must be at least :min.", ['min' => $min]);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private function validateMax(string $field, mixed $value, int $max): bool
|
||||
{
|
||||
if (is_string($value) && strlen($value) > $max) {
|
||||
$this->addError($field, 'max', "The :field may not be greater than :max characters.", ['max' => $max]);
|
||||
return false;
|
||||
}
|
||||
if (is_numeric($value) && $value > $max) {
|
||||
$this->addError($field, 'max', "The :field may not be greater than :max.", ['max' => $max]);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private function validateLength(string $field, mixed $value, int $length): bool
|
||||
{
|
||||
if (is_string($value) && strlen($value) !== $length) {
|
||||
$this->addError($field, 'length', "The :field must be :length characters.", ['length' => $length]);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private function validateIn(string $field, mixed $value, array $values): bool
|
||||
{
|
||||
if ($value !== null && !in_array($value, $values)) {
|
||||
$this->addError($field, 'in', "The selected :field is invalid.");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private function validateRegex(string $field, mixed $value, string $pattern): bool
|
||||
{
|
||||
if ($value !== null && $value !== '' && !preg_match($pattern, (string)$value)) {
|
||||
$this->addError($field, 'regex', "The :field format is invalid.");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private function addError(string $field, string $rule, string $message, array $parameters = []): void
|
||||
{
|
||||
$key = "{$field}.{$rule}";
|
||||
|
||||
if (isset($this->messages[$key])) {
|
||||
$message = $this->messages[$key];
|
||||
}
|
||||
|
||||
// 替换消息中的占位符
|
||||
$message = str_replace(':field', $field, $message);
|
||||
foreach ($parameters as $key => $value) {
|
||||
$message = str_replace(":{$key}", (string)$value, $message);
|
||||
}
|
||||
|
||||
$this->errors[$field][] = $message;
|
||||
}
|
||||
|
||||
public function errors(): array
|
||||
{
|
||||
return $this->errors;
|
||||
}
|
||||
|
||||
public function firstError(string $field): ?string
|
||||
{
|
||||
return $this->errors[$field][0] ?? null;
|
||||
}
|
||||
|
||||
public function fails(): bool
|
||||
{
|
||||
return !empty($this->errors);
|
||||
}
|
||||
|
||||
public function validateOrThrow(): void
|
||||
{
|
||||
if (!$this->validate()) {
|
||||
throw new BusinessException(422, 'Validation failed', $this->errors());
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user