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:
23
fendx-framework/fendx-core/composer.json
Normal file
23
fendx-framework/fendx-core/composer.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"name": "fendx/core",
|
||||
"description": "FendxPHP Core Module - IOC、AOP、上下文、配置",
|
||||
"type": "library",
|
||||
"license": "MIT",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Lawson",
|
||||
"email": "lawson@fendx.cn"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": ">=8.1",
|
||||
"fendx/common": "^1.0"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Fendx\\Core\\": "src/"
|
||||
}
|
||||
},
|
||||
"minimum-stability": "stable",
|
||||
"prefer-stable": true
|
||||
}
|
||||
15
fendx-framework/fendx-core/src/Annotation/Controller.php
Normal file
15
fendx-framework/fendx-core/src/Annotation/Controller.php
Normal file
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Fendx\Core\Annotation;
|
||||
|
||||
#[\Attribute(\Attribute::TARGET_CLASS)]
|
||||
final class Controller
|
||||
{
|
||||
public string $prefix;
|
||||
|
||||
public function __construct(string $prefix = '')
|
||||
{
|
||||
$this->prefix = $prefix;
|
||||
}
|
||||
}
|
||||
15
fendx-framework/fendx-core/src/Annotation/Dao.php
Normal file
15
fendx-framework/fendx-core/src/Annotation/Dao.php
Normal file
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Fendx\Core\Annotation;
|
||||
|
||||
#[\Attribute(\Attribute::TARGET_CLASS)]
|
||||
final class Dao
|
||||
{
|
||||
public string $name;
|
||||
|
||||
public function __construct(string $name = '')
|
||||
{
|
||||
$this->name = $name;
|
||||
}
|
||||
}
|
||||
15
fendx-framework/fendx-core/src/Annotation/Inject.php
Normal file
15
fendx-framework/fendx-core/src/Annotation/Inject.php
Normal file
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Fendx\Core\Annotation;
|
||||
|
||||
#[\Attribute(\Attribute::TARGET_PROPERTY)]
|
||||
final class Inject
|
||||
{
|
||||
public ?string $name;
|
||||
|
||||
public function __construct(?string $name = null)
|
||||
{
|
||||
$this->name = $name;
|
||||
}
|
||||
}
|
||||
15
fendx-framework/fendx-core/src/Annotation/Service.php
Normal file
15
fendx-framework/fendx-core/src/Annotation/Service.php
Normal file
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Fendx\Core\Annotation;
|
||||
|
||||
#[\Attribute(\Attribute::TARGET_CLASS)]
|
||||
final class Service
|
||||
{
|
||||
public string $name;
|
||||
|
||||
public function __construct(string $name = '')
|
||||
{
|
||||
$this->name = $name;
|
||||
}
|
||||
}
|
||||
122
fendx-framework/fendx-core/src/Aop/Advice.php
Normal file
122
fendx-framework/fendx-core/src/Aop/Advice.php
Normal file
@@ -0,0 +1,122 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Fendx\Core\Aop;
|
||||
|
||||
/**
|
||||
* AOP通知接口
|
||||
* 定义各种通知类型的基础接口
|
||||
*/
|
||||
interface Advice
|
||||
{
|
||||
/**
|
||||
* 执行通知
|
||||
*/
|
||||
public function invoke(JoinPoint $joinPoint): mixed;
|
||||
}
|
||||
|
||||
/**
|
||||
* 前置通知
|
||||
*/
|
||||
class BeforeAdvice implements Advice
|
||||
{
|
||||
private callable $callback;
|
||||
|
||||
public function __construct(callable $callback)
|
||||
{
|
||||
$this->callback = $callback;
|
||||
}
|
||||
|
||||
public function invoke(JoinPoint $joinPoint): mixed
|
||||
{
|
||||
return ($this->callback)($joinPoint);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 后置通知
|
||||
*/
|
||||
class AfterAdvice implements Advice
|
||||
{
|
||||
private callable $callback;
|
||||
|
||||
public function __construct(callable $callback)
|
||||
{
|
||||
$this->callback = $callback;
|
||||
}
|
||||
|
||||
public function invoke(JoinPoint $joinPoint): mixed
|
||||
{
|
||||
try {
|
||||
$result = $joinPoint->proceed();
|
||||
($this->callback)($joinPoint);
|
||||
return $result;
|
||||
} catch (\Throwable $e) {
|
||||
($this->callback)($joinPoint);
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 环绕通知
|
||||
*/
|
||||
class AroundAdvice implements Advice
|
||||
{
|
||||
private callable $callback;
|
||||
|
||||
public function __construct(callable $callback)
|
||||
{
|
||||
$this->callback = $callback;
|
||||
}
|
||||
|
||||
public function invoke(JoinPoint $joinPoint): mixed
|
||||
{
|
||||
return ($this->callback)($joinPoint);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回后通知
|
||||
*/
|
||||
class AfterReturningAdvice implements Advice
|
||||
{
|
||||
private callable $callback;
|
||||
|
||||
public function __construct(callable $callback)
|
||||
{
|
||||
$this->callback = $callback;
|
||||
}
|
||||
|
||||
public function invoke(JoinPoint $joinPoint): mixed
|
||||
{
|
||||
$result = $joinPoint->proceed();
|
||||
if (!$joinPoint->hasException()) {
|
||||
($this->callback)($joinPoint, $result);
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 异常后通知
|
||||
*/
|
||||
class AfterThrowingAdvice implements Advice
|
||||
{
|
||||
private callable $callback;
|
||||
|
||||
public function __construct(callable $callback)
|
||||
{
|
||||
$this->callback = $callback;
|
||||
}
|
||||
|
||||
public function invoke(JoinPoint $joinPoint): mixed
|
||||
{
|
||||
try {
|
||||
return $joinPoint->proceed();
|
||||
} catch (\Throwable $e) {
|
||||
($this->callback)($joinPoint, $e);
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
}
|
||||
149
fendx-framework/fendx-core/src/Aop/AopManager.php
Normal file
149
fendx-framework/fendx-core/src/Aop/AopManager.php
Normal file
@@ -0,0 +1,149 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Fendx\Core\Aop;
|
||||
|
||||
use Fendx\Core\Container\Container;
|
||||
use ReflectionClass;
|
||||
use ReflectionMethod;
|
||||
use ReflectionException;
|
||||
|
||||
final class AopManager
|
||||
{
|
||||
private static ?self $instance = null;
|
||||
private array $aspects = [];
|
||||
private Container $container;
|
||||
|
||||
private function __construct(Container $container)
|
||||
{
|
||||
$this->container = $container;
|
||||
}
|
||||
|
||||
public static function getInstance(Container $container): self
|
||||
{
|
||||
if (self::$instance === null) {
|
||||
self::$instance = new self($container);
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
public function registerAspect(string $aspectClass, array $pointcuts): void
|
||||
{
|
||||
$aspect = $this->container->make($aspectClass);
|
||||
$this->aspects[$aspectClass] = [
|
||||
'aspect' => $aspect,
|
||||
'pointcuts' => $pointcuts
|
||||
];
|
||||
}
|
||||
|
||||
public function createProxy(object $target): object
|
||||
{
|
||||
$className = get_class($target);
|
||||
$proxyClass = $this->generateProxyClass($className);
|
||||
|
||||
return new $proxyClass($target, $this);
|
||||
}
|
||||
|
||||
private function generateProxyClass(string $className): string
|
||||
{
|
||||
$proxyClassName = $className . '_AopProxy';
|
||||
|
||||
if (class_exists($proxyClassName)) {
|
||||
return $proxyClassName;
|
||||
}
|
||||
|
||||
$reflection = new ReflectionClass($className);
|
||||
$methods = $reflection->getMethods(ReflectionMethod::IS_PUBLIC);
|
||||
|
||||
$classCode = "<?php\n";
|
||||
$classCode .= "declare(strict_types=1);\n\n";
|
||||
$classCode .= "class {$proxyClassName}\n";
|
||||
$classCode .= "{\n";
|
||||
$classCode .= " private object \$target;\n";
|
||||
$classCode .= " private Fendx\\Core\\Aop\\AopManager \$aopManager;\n\n";
|
||||
|
||||
$classCode .= " public function __construct(object \$target, Fendx\\Core\\Aop\\AopManager \$aopManager)\n";
|
||||
$classCode .= " {\n";
|
||||
$classCode .= " \$this->target = \$target;\n";
|
||||
$classCode .= " \$this->aopManager = \$aopManager;\n";
|
||||
$classCode .= " }\n\n";
|
||||
|
||||
foreach ($methods as $method) {
|
||||
if ($method->getName() === '__construct') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$methodName = $method->getName();
|
||||
$params = $this->generateMethodParameters($method);
|
||||
|
||||
$classCode .= " public function {$methodName}({$params}): mixed\n";
|
||||
$classCode .= " {\n";
|
||||
$classCode .= " \$args = func_get_args();\n";
|
||||
$classCode .= " \$aspect = \$this->aopManager->getMatchingAspect(\$this->target, '{$methodName}');\n";
|
||||
$classCode .= " \n";
|
||||
$classCode .= " if (\$aspect) {\n";
|
||||
$classCode .= " return \$aspect->weave(\$this->target, '{$methodName}', \$args);\n";
|
||||
$classCode .= " }\n";
|
||||
$classCode .= " \n";
|
||||
$classCode .= " return \$this->target->{$methodName}(...\$args);\n";
|
||||
$classCode .= " }\n\n";
|
||||
}
|
||||
|
||||
$classCode .= "}\n";
|
||||
|
||||
eval($classCode);
|
||||
return $proxyClassName;
|
||||
}
|
||||
|
||||
private function generateMethodParameters(ReflectionMethod $method): string
|
||||
{
|
||||
$params = [];
|
||||
foreach ($method->getParameters() as $param) {
|
||||
$type = $param->getType();
|
||||
$typeStr = $type ? $type->getName() . ' ' : '';
|
||||
$default = $param->isDefaultValueAvailable() ? ' = ' . var_export($param->getDefaultValue(), true) : '';
|
||||
$params[] = $typeStr . '$' . $param->getName() . $default;
|
||||
}
|
||||
return implode(', ', $params);
|
||||
}
|
||||
|
||||
public function getMatchingAspect(object $target, string $method): ?Aspect
|
||||
{
|
||||
foreach ($this->aspects as $aspectData) {
|
||||
foreach ($aspectData['pointcuts'] as $pointcut) {
|
||||
if ($this->matchesPointcut($target, $method, $pointcut)) {
|
||||
return $aspectData['aspect'];
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private function matchesPointcut(object $target, string $method, array $pointcut): bool
|
||||
{
|
||||
$className = get_class($target);
|
||||
|
||||
// 注解切点
|
||||
if (isset($pointcut['annotation'])) {
|
||||
$reflection = new ReflectionClass($className);
|
||||
if ($reflection->getAttributes($pointcut['annotation'])) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// 方法名匹配切点
|
||||
if (isset($pointcut['method'])) {
|
||||
$pattern = str_replace('*', '.*', $pointcut['method']);
|
||||
if (preg_match("/^{$pattern}$/", $method)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// 路由匹配切点
|
||||
if (isset($pointcut['route'])) {
|
||||
// TODO: 实现路由匹配逻辑
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
92
fendx-framework/fendx-core/src/Aop/Aspect.php
Normal file
92
fendx-framework/fendx-core/src/Aop/Aspect.php
Normal file
@@ -0,0 +1,92 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Fendx\Core\Aop;
|
||||
|
||||
use Closure;
|
||||
|
||||
final class Aspect
|
||||
{
|
||||
private array $beforeAdvice = [];
|
||||
private array $afterAdvice = [];
|
||||
private array $aroundAdvice = [];
|
||||
private array $afterReturningAdvice = [];
|
||||
private array $afterThrowingAdvice = [];
|
||||
|
||||
public function before(Closure $advice): self
|
||||
{
|
||||
$this->beforeAdvice[] = $advice;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function after(Closure $advice): self
|
||||
{
|
||||
$this->afterAdvice[] = $advice;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function around(Closure $advice): self
|
||||
{
|
||||
$this->aroundAdvice[] = $advice;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function afterReturning(Closure $advice): self
|
||||
{
|
||||
$this->afterReturningAdvice[] = $advice;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function afterThrowing(Closure $advice): self
|
||||
{
|
||||
$this->afterThrowingAdvice[] = $advice;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function weave(object $target, string $method, array $args = []): mixed
|
||||
{
|
||||
// 执行Before通知
|
||||
foreach ($this->beforeAdvice as $advice) {
|
||||
$advice($target, $method, $args);
|
||||
}
|
||||
|
||||
try {
|
||||
// 如果有Around通知,使用Around包装
|
||||
if (!empty($this->aroundAdvice)) {
|
||||
$result = $this->executeAroundAdvice($target, $method, $args, 0);
|
||||
} else {
|
||||
$result = $target->$method(...$args);
|
||||
}
|
||||
|
||||
// 执行AfterReturning通知
|
||||
foreach ($this->afterReturningAdvice as $advice) {
|
||||
$advice($target, $method, $args, $result);
|
||||
}
|
||||
|
||||
return $result;
|
||||
} catch (\Throwable $exception) {
|
||||
// 执行AfterThrowing通知
|
||||
foreach ($this->afterThrowingAdvice as $advice) {
|
||||
$advice($target, $method, $args, $exception);
|
||||
}
|
||||
throw $exception;
|
||||
} finally {
|
||||
// 执行After通知
|
||||
foreach ($this->afterAdvice as $advice) {
|
||||
$advice($target, $method, $args);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function executeAroundAdvice(object $target, string $method, array $args, int $index): mixed
|
||||
{
|
||||
if ($index >= count($this->aroundAdvice)) {
|
||||
return $target->$method(...$args);
|
||||
}
|
||||
|
||||
$advice = $this->aroundAdvice[$index];
|
||||
$next = fn() => $this->executeAroundAdvice($target, $method, $args, $index + 1);
|
||||
|
||||
return $advice($target, $method, $args, $next);
|
||||
}
|
||||
}
|
||||
166
fendx-framework/fendx-core/src/Aop/JoinPoint.php
Normal file
166
fendx-framework/fendx-core/src/Aop/JoinPoint.php
Normal file
@@ -0,0 +1,166 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Fendx\Core\Aop;
|
||||
|
||||
/**
|
||||
* AOP连接点类
|
||||
* 用于封装方法调用的上下文信息
|
||||
*/
|
||||
class JoinPoint
|
||||
{
|
||||
private object $target;
|
||||
private string $method;
|
||||
private array $arguments;
|
||||
private ?object $result = null;
|
||||
private ?\Throwable $exception = null;
|
||||
private bool $proceed = true;
|
||||
|
||||
public function __construct(object $target, string $method, array $arguments = [])
|
||||
{
|
||||
$this->target = $target;
|
||||
$this->method = $method;
|
||||
$this->arguments = $arguments;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取目标对象
|
||||
*/
|
||||
public function getTarget(): object
|
||||
{
|
||||
return $this->target;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取方法名
|
||||
*/
|
||||
public function getMethod(): string
|
||||
{
|
||||
return $this->method;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取方法参数
|
||||
*/
|
||||
public function getArguments(): array
|
||||
{
|
||||
return $this->arguments;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定参数
|
||||
*/
|
||||
public function getArgument(int $index): mixed
|
||||
{
|
||||
return $this->arguments[$index] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置参数
|
||||
*/
|
||||
public function setArgument(int $index, mixed $value): void
|
||||
{
|
||||
$this->arguments[$index] = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取执行结果
|
||||
*/
|
||||
public function getResult(): mixed
|
||||
{
|
||||
return $this->result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置执行结果
|
||||
*/
|
||||
public function setResult(mixed $result): void
|
||||
{
|
||||
$this->result = $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取异常
|
||||
*/
|
||||
public function getException(): ?\Throwable
|
||||
{
|
||||
return $this->exception;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置异常
|
||||
*/
|
||||
public function setException(?\Throwable $exception): void
|
||||
{
|
||||
$this->exception = $exception;
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否继续执行
|
||||
*/
|
||||
public function canProceed(): bool
|
||||
{
|
||||
return $this->proceed;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置是否继续执行
|
||||
*/
|
||||
public function setProceed(bool $proceed): void
|
||||
{
|
||||
$this->proceed = $proceed;
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行目标方法
|
||||
*/
|
||||
public function proceed(): mixed
|
||||
{
|
||||
if (!$this->proceed) {
|
||||
return $this->result;
|
||||
}
|
||||
|
||||
try {
|
||||
$result = call_user_func_array([$this->target, $this->method], $this->arguments);
|
||||
$this->result = $result;
|
||||
return $result;
|
||||
} catch (\Throwable $e) {
|
||||
$this->exception = $e;
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取方法签名
|
||||
*/
|
||||
public function getSignature(): string
|
||||
{
|
||||
return get_class($this->target) . '::' . $this->method;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取目标类名
|
||||
*/
|
||||
public function getTargetClass(): string
|
||||
{
|
||||
return get_class($this->target);
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否有异常
|
||||
*/
|
||||
public function hasException(): bool
|
||||
{
|
||||
return $this->exception !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理状态
|
||||
*/
|
||||
public function clear(): void
|
||||
{
|
||||
$this->result = null;
|
||||
$this->exception = null;
|
||||
$this->proceed = true;
|
||||
}
|
||||
}
|
||||
240
fendx-framework/fendx-core/src/Aop/Pointcut.php
Normal file
240
fendx-framework/fendx-core/src/Aop/Pointcut.php
Normal file
@@ -0,0 +1,240 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Fendx\Core\Aop;
|
||||
|
||||
/**
|
||||
* AOP切点类
|
||||
* 定义连接点的匹配规则
|
||||
*/
|
||||
class Pointcut
|
||||
{
|
||||
private string $expression;
|
||||
private array $patterns = [];
|
||||
private string $type;
|
||||
|
||||
public function __construct(string $expression, string $type = 'method')
|
||||
{
|
||||
$this->expression = $expression;
|
||||
$this->type = $type;
|
||||
$this->parseExpression();
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析表达式
|
||||
*/
|
||||
private function parseExpression(): void
|
||||
{
|
||||
// 支持多种切点表达式格式
|
||||
// 1. 注解切点: @annotation(Fendx\Core\Annotation\Cacheable)
|
||||
// 2. 方法名切点: execution(* UserService.*(..))
|
||||
// 3. 类名切点: within(Fendx\Service.*)
|
||||
// 4. 路由切点: @route(/api/*)
|
||||
|
||||
if (strpos($this->expression, '@annotation(') === 0) {
|
||||
$this->parseAnnotationExpression();
|
||||
} elseif (strpos($this->expression, 'execution(') === 0) {
|
||||
$this->parseExecutionExpression();
|
||||
} elseif (strpos($this->expression, 'within(') === 0) {
|
||||
$this->parseWithinExpression();
|
||||
} elseif (strpos($this->expression, '@route(') === 0) {
|
||||
$this->parseRouteExpression();
|
||||
} else {
|
||||
// 简单方法名匹配
|
||||
$this->patterns[] = $this->expression;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析注解表达式
|
||||
*/
|
||||
private function parseAnnotationExpression(): void
|
||||
{
|
||||
$annotation = str_replace(['@annotation(', ')'], '', $this->expression);
|
||||
$this->patterns[] = ['type' => 'annotation', 'value' => $annotation];
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析执行表达式
|
||||
*/
|
||||
private function parseExecutionExpression(): void
|
||||
{
|
||||
$pattern = str_replace(['execution(', ')'], '', $this->expression);
|
||||
// 转换通配符为正则表达式
|
||||
$pattern = str_replace('*', '.*', $pattern);
|
||||
$pattern = str_replace('..', '.*', $pattern);
|
||||
$this->patterns[] = ['type' => 'execution', 'pattern' => '/^' . $pattern . '$/'];
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析类内表达式
|
||||
*/
|
||||
private function parseWithinExpression(): void
|
||||
{
|
||||
$className = str_replace(['within(', ')'], '', $this->expression);
|
||||
$className = str_replace('*', '.*', $className);
|
||||
$this->patterns[] = ['type' => 'within', 'pattern' => '/^' . $className . '$/'];
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析路由表达式
|
||||
*/
|
||||
private function parseRouteExpression(): void
|
||||
{
|
||||
$route = str_replace(['@route(', ')'], '', $this->expression);
|
||||
$route = str_replace('*', '.*', $route);
|
||||
$this->patterns[] = ['type' => 'route', 'pattern' => '/^' . $route . '$/'];
|
||||
}
|
||||
|
||||
/**
|
||||
* 匹配连接点
|
||||
*/
|
||||
public function matches(object $target, string $method, array $context = []): bool
|
||||
{
|
||||
foreach ($this->patterns as $pattern) {
|
||||
if ($this->matchPattern($pattern, $target, $method, $context)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 匹配单个模式
|
||||
*/
|
||||
private function matchPattern(array $pattern, object $target, string $method, array $context): bool
|
||||
{
|
||||
$type = $pattern['type'] ?? 'simple';
|
||||
|
||||
switch ($type) {
|
||||
case 'annotation':
|
||||
return $this->matchAnnotation($pattern['value'], $target, $method);
|
||||
|
||||
case 'execution':
|
||||
return $this->matchExecution($pattern['pattern'], $target, $method);
|
||||
|
||||
case 'within':
|
||||
return $this->matchWithin($pattern['pattern'], $target);
|
||||
|
||||
case 'route':
|
||||
return $this->matchRoute($pattern['pattern'], $context);
|
||||
|
||||
default:
|
||||
return $this->matchSimple($pattern, $target, $method);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 匹配注解
|
||||
*/
|
||||
private function matchAnnotation(string $annotation, object $target, string $method): bool
|
||||
{
|
||||
$reflection = new \ReflectionClass($target);
|
||||
$methodReflection = $reflection->getMethod($method);
|
||||
|
||||
// 检查方法注解
|
||||
$methodAnnotations = $this->getAnnotations($methodReflection);
|
||||
if (in_array($annotation, $methodAnnotations)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 检查类注解
|
||||
$classAnnotations = $this->getAnnotations($reflection);
|
||||
return in_array($annotation, $classAnnotations);
|
||||
}
|
||||
|
||||
/**
|
||||
* 匹配执行表达式
|
||||
*/
|
||||
private function matchExecution(string $pattern, object $target, string $method): bool
|
||||
{
|
||||
$signature = get_class($target) . '::' . $method;
|
||||
return preg_match($pattern, $signature);
|
||||
}
|
||||
|
||||
/**
|
||||
* 匹配类内表达式
|
||||
*/
|
||||
private function matchWithin(string $pattern, object $target): bool
|
||||
{
|
||||
$className = get_class($target);
|
||||
return preg_match($pattern, $className);
|
||||
}
|
||||
|
||||
/**
|
||||
* 匹配路由表达式
|
||||
*/
|
||||
private function matchRoute(string $pattern, array $context): bool
|
||||
{
|
||||
if (!isset($context['route'])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return preg_match($pattern, $context['route']);
|
||||
}
|
||||
|
||||
/**
|
||||
* 简单匹配
|
||||
*/
|
||||
private function matchSimple(string $pattern, object $target, string $method): bool
|
||||
{
|
||||
// 支持通配符匹配
|
||||
if (strpos($pattern, '*') !== false) {
|
||||
$regex = str_replace('*', '.*', $pattern);
|
||||
return preg_match('/^' . $regex . '$/', $method);
|
||||
}
|
||||
|
||||
return $pattern === $method;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取注解
|
||||
*/
|
||||
private function getAnnotations(\ReflectionClass|\ReflectionMethod $reflection): array
|
||||
{
|
||||
$annotations = [];
|
||||
|
||||
// 这里需要根据实际使用的注解库来实现
|
||||
// 例如使用PHP8的Attributes或其他注解库
|
||||
|
||||
if (method_exists($reflection, 'getAttributes')) {
|
||||
$attributes = $reflection->getAttributes();
|
||||
foreach ($attributes as $attribute) {
|
||||
$annotations[] = $attribute->getName();
|
||||
}
|
||||
}
|
||||
|
||||
// 支持PHPDoc注释解析
|
||||
$docComment = $reflection->getDocComment();
|
||||
if ($docComment) {
|
||||
preg_match_all('/@(\w+)/', $docComment, $matches);
|
||||
$annotations = array_merge($annotations, $matches[1]);
|
||||
}
|
||||
|
||||
return array_unique($annotations);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取表达式
|
||||
*/
|
||||
public function getExpression(): string
|
||||
{
|
||||
return $this->expression;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取类型
|
||||
*/
|
||||
public function getType(): string
|
||||
{
|
||||
return $this->type;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取模式
|
||||
*/
|
||||
public function getPatterns(): array
|
||||
{
|
||||
return $this->patterns;
|
||||
}
|
||||
}
|
||||
85
fendx-framework/fendx-core/src/Config/Config.php
Normal file
85
fendx-framework/fendx-core/src/Config/Config.php
Normal file
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Fendx\Core\Config;
|
||||
|
||||
use Fendx\Common\Exception\BusinessException;
|
||||
|
||||
final class Config
|
||||
{
|
||||
private static array $data = [];
|
||||
private static array $cache = [];
|
||||
|
||||
public static function load(array $config): void
|
||||
{
|
||||
self::$data = $config;
|
||||
self::$cache = [];
|
||||
}
|
||||
|
||||
public static function get(string $key, mixed $default = null): mixed
|
||||
{
|
||||
if ($key === '') {
|
||||
return $default;
|
||||
}
|
||||
|
||||
// 使用缓存提高性能
|
||||
if (isset(self::$cache[$key])) {
|
||||
return self::$cache[$key];
|
||||
}
|
||||
|
||||
$parts = explode('.', $key);
|
||||
$cur = self::$data;
|
||||
|
||||
foreach ($parts as $part) {
|
||||
if (!is_array($cur) || !array_key_exists($part, $cur)) {
|
||||
self::$cache[$key] = $default;
|
||||
return $default;
|
||||
}
|
||||
$cur = $cur[$part];
|
||||
}
|
||||
|
||||
self::$cache[$key] = $cur;
|
||||
return $cur;
|
||||
}
|
||||
|
||||
public static function set(string $key, mixed $value): void
|
||||
{
|
||||
$parts = explode('.', $key);
|
||||
$cur = &self::$data;
|
||||
|
||||
foreach ($parts as $part) {
|
||||
if (!is_array($cur)) {
|
||||
$cur = [];
|
||||
}
|
||||
if (!array_key_exists($part, $cur)) {
|
||||
$cur[$part] = [];
|
||||
}
|
||||
$cur = &$cur[$part];
|
||||
}
|
||||
|
||||
$cur = $value;
|
||||
|
||||
// 清除相关缓存
|
||||
foreach (array_keys(self::$cache) as $cacheKey) {
|
||||
if (str_starts_with($cacheKey, $key)) {
|
||||
unset(self::$cache[$cacheKey]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static function has(string $key): bool
|
||||
{
|
||||
return self::get($key) !== null;
|
||||
}
|
||||
|
||||
public static function all(): array
|
||||
{
|
||||
return self::$data;
|
||||
}
|
||||
|
||||
public static function clear(): void
|
||||
{
|
||||
self::$data = [];
|
||||
self::$cache = [];
|
||||
}
|
||||
}
|
||||
31
fendx-framework/fendx-core/src/Config/LegacyConfig.php
Normal file
31
fendx-framework/fendx-core/src/Config/LegacyConfig.php
Normal file
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Fendx\Core;
|
||||
|
||||
final class Config
|
||||
{
|
||||
private static array $data = [];
|
||||
|
||||
public static function load(array $config): void
|
||||
{
|
||||
self::$data = $config;
|
||||
}
|
||||
|
||||
public static function get(string $key, mixed $default = null): mixed
|
||||
{
|
||||
if ($key === '') {
|
||||
return $default;
|
||||
}
|
||||
$parts = explode('.', $key);
|
||||
$cur = self::$data;
|
||||
foreach ($parts as $part) {
|
||||
if (!is_array($cur) || !array_key_exists($part, $cur)) {
|
||||
return $default;
|
||||
}
|
||||
$cur = $cur[$part];
|
||||
}
|
||||
return $cur;
|
||||
}
|
||||
}
|
||||
|
||||
126
fendx-framework/fendx-core/src/Container/Container.php
Normal file
126
fendx-framework/fendx-core/src/Container/Container.php
Normal file
@@ -0,0 +1,126 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Fendx\Core\Container;
|
||||
|
||||
use Fendx\Common\Exception\BusinessException;
|
||||
use ReflectionClass;
|
||||
use ReflectionException;
|
||||
|
||||
final class Container
|
||||
{
|
||||
private static ?Container $instance = null;
|
||||
private array $bindings = [];
|
||||
private array $instances = [];
|
||||
private array $singletons = [];
|
||||
|
||||
private function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
public static function getInstance(): self
|
||||
{
|
||||
if (self::$instance === null) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
public function bind(string $abstract, mixed $concrete = null, bool $singleton = false): void
|
||||
{
|
||||
if ($concrete === null) {
|
||||
$concrete = $abstract;
|
||||
}
|
||||
|
||||
$this->bindings[$abstract] = $concrete;
|
||||
$this->singletons[$abstract] = $singleton;
|
||||
}
|
||||
|
||||
public function singleton(string $abstract, mixed $concrete = null): void
|
||||
{
|
||||
$this->bind($abstract, $concrete, true);
|
||||
}
|
||||
|
||||
public function make(string $abstract, array $parameters = []): mixed
|
||||
{
|
||||
// 如果已存在实例且为单例
|
||||
if (isset($this->instances[$abstract])) {
|
||||
return $this->instances[$abstract];
|
||||
}
|
||||
|
||||
$concrete = $this->bindings[$abstract] ?? $abstract;
|
||||
|
||||
// 如果是闭包
|
||||
if ($concrete instanceof \Closure) {
|
||||
$object = $concrete($this, $parameters);
|
||||
} else {
|
||||
$object = $this->build($concrete, $parameters);
|
||||
}
|
||||
|
||||
// 如果是单例,缓存实例
|
||||
if (isset($this->singletons[$abstract]) && $this->singletons[$abstract]) {
|
||||
$this->instances[$abstract] = $object;
|
||||
}
|
||||
|
||||
return $object;
|
||||
}
|
||||
|
||||
private function build(string $concrete, array $parameters): mixed
|
||||
{
|
||||
try {
|
||||
$reflector = new ReflectionClass($concrete);
|
||||
} catch (ReflectionException $e) {
|
||||
throw new BusinessException(500, 'CLASS_NOT_FOUND', ['class' => $concrete]);
|
||||
}
|
||||
|
||||
if (!$reflector->isInstantiable()) {
|
||||
throw new BusinessException(500, 'CLASS_NOT_INSTANTIABLE', ['class' => $concrete]);
|
||||
}
|
||||
|
||||
$constructor = $reflector->getConstructor();
|
||||
|
||||
if ($constructor === null) {
|
||||
return new $concrete();
|
||||
}
|
||||
|
||||
$dependencies = $constructor->getParameters();
|
||||
$instances = $this->getDependencies($dependencies, $parameters);
|
||||
|
||||
return $reflector->newInstanceArgs($instances);
|
||||
}
|
||||
|
||||
private function getDependencies(array $parameters, array $primitives): array
|
||||
{
|
||||
$dependencies = [];
|
||||
|
||||
foreach ($parameters as $parameter) {
|
||||
$type = $parameter->getType();
|
||||
|
||||
if ($type === null || $type->isBuiltin()) {
|
||||
$name = $parameter->getName();
|
||||
$dependencies[] = $primitives[$name] ?? $parameter->getDefaultValue();
|
||||
} else {
|
||||
$dependencies[] = $this->make($type->getName());
|
||||
}
|
||||
}
|
||||
|
||||
return $dependencies;
|
||||
}
|
||||
|
||||
public function has(string $abstract): bool
|
||||
{
|
||||
return isset($this->bindings[$abstract]);
|
||||
}
|
||||
|
||||
public function forget(string $abstract): void
|
||||
{
|
||||
unset($this->bindings[$abstract], $this->instances[$abstract], $this->singletons[$abstract]);
|
||||
}
|
||||
|
||||
public function flush(): void
|
||||
{
|
||||
$this->bindings = [];
|
||||
$this->instances = [];
|
||||
$this->singletons = [];
|
||||
}
|
||||
}
|
||||
89
fendx-framework/fendx-core/src/Context/Context.php
Normal file
89
fendx-framework/fendx-core/src/Context/Context.php
Normal file
@@ -0,0 +1,89 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Fendx\Core\Context;
|
||||
|
||||
final class Context
|
||||
{
|
||||
private static ?string $traceId = null;
|
||||
private static array $data = [];
|
||||
private static array $user = [];
|
||||
|
||||
public static function init(): void
|
||||
{
|
||||
self::$traceId = self::generateTraceId();
|
||||
self::$data = [];
|
||||
self::$user = [];
|
||||
}
|
||||
|
||||
public static function getTraceId(): string
|
||||
{
|
||||
return self::$traceId ?? self::generateTraceId();
|
||||
}
|
||||
|
||||
public static function setTraceId(string $traceId): void
|
||||
{
|
||||
self::$traceId = $traceId;
|
||||
}
|
||||
|
||||
public static function set(string $key, mixed $value): void
|
||||
{
|
||||
self::$data[$key] = $value;
|
||||
}
|
||||
|
||||
public static function get(string $key, mixed $default = null): mixed
|
||||
{
|
||||
return self::$data[$key] ?? $default;
|
||||
}
|
||||
|
||||
public static function has(string $key): bool
|
||||
{
|
||||
return isset(self::$data[$key]);
|
||||
}
|
||||
|
||||
public static function remove(string $key): void
|
||||
{
|
||||
unset(self::$data[$key]);
|
||||
}
|
||||
|
||||
public static function setUser(array $user): void
|
||||
{
|
||||
self::$user = $user;
|
||||
}
|
||||
|
||||
public static function getUser(): array
|
||||
{
|
||||
return self::$user;
|
||||
}
|
||||
|
||||
public static function getUserId(): mixed
|
||||
{
|
||||
return self::$user['id'] ?? null;
|
||||
}
|
||||
|
||||
public static function getUsername(): string
|
||||
{
|
||||
return self::$user['username'] ?? '';
|
||||
}
|
||||
|
||||
public static function clear(): void
|
||||
{
|
||||
self::$traceId = null;
|
||||
self::$data = [];
|
||||
self::$user = [];
|
||||
}
|
||||
|
||||
public static function all(): array
|
||||
{
|
||||
return [
|
||||
'traceId' => self::getTraceId(),
|
||||
'data' => self::$data,
|
||||
'user' => self::$user,
|
||||
];
|
||||
}
|
||||
|
||||
private static function generateTraceId(): string
|
||||
{
|
||||
return uniqid('trace_', true) . '_' . bin2hex(random_bytes(8));
|
||||
}
|
||||
}
|
||||
80
fendx-framework/fendx-core/src/Event/EventDispatcher.php
Normal file
80
fendx-framework/fendx-core/src/Event/EventDispatcher.php
Normal file
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Fendx\Core\Event;
|
||||
|
||||
use Fendx\Core\Container\Container;
|
||||
|
||||
final class EventDispatcher
|
||||
{
|
||||
private static ?self $instance = null;
|
||||
private array $listeners = [];
|
||||
private Container $container;
|
||||
|
||||
private function __construct(Container $container)
|
||||
{
|
||||
$this->container = $container;
|
||||
}
|
||||
|
||||
public static function getInstance(Container $container): self
|
||||
{
|
||||
if (self::$instance === null) {
|
||||
self::$instance = new self($container);
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
public function listen(string $event, mixed $listener): void
|
||||
{
|
||||
$this->listeners[$event][] = $listener;
|
||||
}
|
||||
|
||||
public function dispatch(object $event): void
|
||||
{
|
||||
$eventName = get_class($event);
|
||||
|
||||
if (!isset($this->listeners[$eventName])) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($this->listeners[$eventName] as $listener) {
|
||||
if ($listener instanceof \Closure) {
|
||||
$listener($event);
|
||||
} elseif (is_string($listener)) {
|
||||
$instance = $this->container->make($listener);
|
||||
if (method_exists($instance, 'handle')) {
|
||||
$instance->handle($event);
|
||||
}
|
||||
} elseif (is_object($listener) && method_exists($listener, 'handle')) {
|
||||
$listener->handle($event);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function hasListeners(string $event): bool
|
||||
{
|
||||
return isset($this->listeners[$event]) && !empty($this->listeners[$event]);
|
||||
}
|
||||
|
||||
public function getListeners(string $event): array
|
||||
{
|
||||
return $this->listeners[$event] ?? [];
|
||||
}
|
||||
|
||||
public function removeListener(string $event, mixed $listener): void
|
||||
{
|
||||
if (!isset($this->listeners[$event])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->listeners[$event] = array_filter(
|
||||
$this->listeners[$event],
|
||||
fn($l) => $l !== $listener
|
||||
);
|
||||
}
|
||||
|
||||
public function clear(): void
|
||||
{
|
||||
$this->listeners = [];
|
||||
}
|
||||
}
|
||||
149
fendx-framework/fendx-core/src/Scanner/AnnotationScanner.php
Normal file
149
fendx-framework/fendx-core/src/Scanner/AnnotationScanner.php
Normal file
@@ -0,0 +1,149 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Fendx\Core\Scanner;
|
||||
|
||||
use ReflectionClass;
|
||||
use ReflectionException;
|
||||
use Fendx\Core\Annotation\Controller;
|
||||
use Fendx\Core\Annotation\Service;
|
||||
use Fendx\Core\Annotation\Dao;
|
||||
use Fendx\Core\Container\Container;
|
||||
|
||||
final class AnnotationScanner
|
||||
{
|
||||
private Container $container;
|
||||
private array $scannedClasses = [];
|
||||
|
||||
public function __construct(Container $container)
|
||||
{
|
||||
$this->container = $container;
|
||||
}
|
||||
|
||||
public function scan(string $scanPath): void
|
||||
{
|
||||
$this->scanDirectory($scanPath);
|
||||
$this->processDependencies();
|
||||
}
|
||||
|
||||
private function scanDirectory(string $path): void
|
||||
{
|
||||
if (!is_dir($path)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$files = glob($path . '/**/*.php');
|
||||
|
||||
foreach ($files as $file) {
|
||||
$this->scanFile($file);
|
||||
}
|
||||
}
|
||||
|
||||
private function scanFile(string $file): void
|
||||
{
|
||||
$className = $this->getClassNameFromFile($file);
|
||||
|
||||
if ($className === null || class_exists($className) === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->scannedClasses[] = $className;
|
||||
$this->processClassAnnotations($className);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
private function processClassAnnotations(string $className): void
|
||||
{
|
||||
try {
|
||||
$reflection = new ReflectionClass($className);
|
||||
|
||||
// 处理Controller注解
|
||||
$controllerAttributes = $reflection->getAttributes(Controller::class);
|
||||
if (!empty($controllerAttributes)) {
|
||||
$this->container->singleton($className);
|
||||
$this->processControllerProperties($reflection);
|
||||
}
|
||||
|
||||
// 处理Service注解
|
||||
$serviceAttributes = $reflection->getAttributes(Service::class);
|
||||
if (!empty($serviceAttributes)) {
|
||||
$attribute = $serviceAttributes[0]->newInstance();
|
||||
$beanName = $attribute->name ?: $className;
|
||||
$this->container->singleton($beanName, $className);
|
||||
$this->processServiceProperties($reflection);
|
||||
}
|
||||
|
||||
// 处理Dao注解
|
||||
$daoAttributes = $reflection->getAttributes(Dao::class);
|
||||
if (!empty($daoAttributes)) {
|
||||
$attribute = $daoAttributes[0]->newInstance();
|
||||
$beanName = $attribute->name ?: $className;
|
||||
$this->container->singleton($beanName, $className);
|
||||
$this->processDaoProperties($reflection);
|
||||
}
|
||||
|
||||
} catch (ReflectionException $e) {
|
||||
// 记录错误但继续处理其他类
|
||||
error_log("Failed to scan class $className: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private function processControllerProperties(ReflectionClass $reflection): void
|
||||
{
|
||||
$properties = $reflection->getProperties();
|
||||
|
||||
foreach ($properties as $property) {
|
||||
$injectAttributes = $property->getAttributes(\Fendx\Core\Annotation\Inject::class);
|
||||
|
||||
if (!empty($injectAttributes)) {
|
||||
$attribute = $injectAttributes[0]->newInstance();
|
||||
$this->registerPropertyInjection($reflection->getName(), $property->getName(), $attribute->name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function processServiceProperties(ReflectionClass $reflection): void
|
||||
{
|
||||
$this->processControllerProperties($reflection);
|
||||
}
|
||||
|
||||
private function processDaoProperties(ReflectionClass $reflection): void
|
||||
{
|
||||
$this->processControllerProperties($reflection);
|
||||
}
|
||||
|
||||
private function registerPropertyInjection(string $className, string $propertyName, ?string $beanName): void
|
||||
{
|
||||
// 这里可以注册属性注入信息,后续在实例化时处理
|
||||
// 暂时使用容器的make方法来处理依赖注入
|
||||
}
|
||||
|
||||
private function processDependencies(): void
|
||||
{
|
||||
// 处理所有扫描到的类的依赖关系
|
||||
foreach ($this->scannedClasses as $className) {
|
||||
try {
|
||||
$this->container->make($className);
|
||||
} catch (\Exception $e) {
|
||||
// 某些类可能需要延迟初始化
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function getScannedClasses(): array
|
||||
{
|
||||
return $this->scannedClasses;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user