feat(database): 添加用户角色权限系统及相关监控功能

- 创建用户表(users)包含基本信息和认证字段
- 创建角色表(roles)用于权限控制
- 创建权限表(permissions)定义系统权限
- 创建用户角色关联表(user_roles)建立用户与角色关系
- 创建角色权限关联表(role_permissions)建立角色与权限关系
- 创建迁移记录表(migrations)追踪数据库变更
- 添加AdminController提供管理员面板功能
- 实现系统监控、配置管理、缓存清理等功能
- 添加AOP切面编程支持的各种通知类型
- 实现告警管理AlertManager支持多渠道告警
- 添加文档注解接口规范
This commit is contained in:
Lawson
2026-04-08 17:00:28 +08:00
commit 2782d765fb
270 changed files with 107192 additions and 0 deletions

View File

@@ -0,0 +1,24 @@
{
"name": "fendx/db",
"description": "FendxPHP Database Module - ORM、多数据源、事务",
"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\\Db\\": "src/"
}
},
"minimum-stability": "stable",
"prefer-stable": true
}

View File

@@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
namespace Fendx\Db\Annotation;
#[\Attribute(\Attribute::TARGET_PROPERTY)]
final class Column
{
public string $name;
public string $type;
public ?int $length;
public bool $nullable;
public mixed $default;
public function __construct(string $name, string $type = 'varchar', ?int $length = null, bool $nullable = true, mixed $default = null)
{
$this->name = $name;
$this->type = $type;
$this->length = $length;
$this->nullable = $nullable;
$this->default = $default;
}
}

View File

@@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace Fendx\Db\Annotation;
#[\Attribute(\Attribute::TARGET_PROPERTY)]
final class Id
{
public bool $autoIncrement;
public function __construct(bool $autoIncrement = true)
{
$this->autoIncrement = $autoIncrement;
}
}

View File

@@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace Fendx\Db\Annotation;
#[\Attribute(\Attribute::TARGET_CLASS)]
final class Table
{
public string $name;
public string $database;
public function __construct(string $name, string $database = '')
{
$this->name = $name;
$this->database = $database;
}
}

View File

@@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace Fendx\Db\Annotation;
#[\Attribute(\Attribute::TARGET_METHOD)]
final class Transactional
{
public string $connection;
public int $isolationLevel;
public function __construct(string $connection = 'default', int $isolationLevel = 0)
{
$this->connection = $connection;
$this->isolationLevel = $isolationLevel;
}
}

View File

@@ -0,0 +1,171 @@
<?php
declare(strict_types=1);
namespace Fendx\Db;
use PDO;
use PDOException;
use Fendx\Core\Config\Config;
use Fendx\Common\Exception\BusinessException;
final class DB
{
private static array $connections = [];
private static array $config = [];
public static function configure(array $config): void
{
self::$config = $config;
}
public static function connection(string $name = 'default'): PDO
{
if (!isset(self::$connections[$name])) {
self::$connections[$name] = self::createConnection($name);
}
return self::$connections[$name];
}
private static function createConnection(string $name): PDO
{
$config = self::$config[$name] ?? self::$config['default'] ?? [];
$host = $config['host'] ?? '127.0.0.1';
$port = $config['port'] ?? 3306;
$dbname = $config['dbname'] ?? '';
$username = $config['username'] ?? '';
$password = $config['password'] ?? '';
$charset = $config['charset'] ?? 'utf8mb4';
$options = $config['options'] ?? [];
$dsn = "mysql:host={$host};port={$port};dbname={$dbname};charset={$charset}";
$defaultOptions = [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
];
$options = array_replace($defaultOptions, $options);
try {
return new PDO($dsn, $username, $password, $options);
} catch (PDOException $e) {
throw new BusinessException(500, 'DB_CONNECT_FAILED', [
'connection' => $name,
'message' => $e->getMessage()
]);
}
}
public static function beginTransaction(string $connection = 'default'): void
{
self::connection($connection)->beginTransaction();
}
public static function commit(string $connection = 'default'): void
{
self::connection($connection)->commit();
}
public static function rollback(string $connection = 'default'): void
{
self::connection($connection)->rollback();
}
public static function execute(string $sql, array $params = [], string $connection = 'default'): \PDOStatement
{
$stmt = self::connection($connection)->prepare($sql);
$stmt->execute($params);
return $stmt;
}
public static function fetch(string $sql, array $params = [], string $connection = 'default'): ?array
{
$stmt = self::execute($sql, $params, $connection);
$result = $stmt->fetch();
return $result !== false ? $result : null;
}
public static function fetchAll(string $sql, array $params = [], string $connection = 'default'): array
{
$stmt = self::execute($sql, $params, $connection);
return $stmt->fetchAll();
}
public static function insert(string $table, array $data, string $connection = 'default'): string
{
$columns = array_keys($data);
$placeholders = array_fill(0, count($columns), '?');
$sql = sprintf(
'INSERT INTO %s (%s) VALUES (%s)',
$table,
implode(', ', $columns),
implode(', ', $placeholders)
);
self::execute($sql, array_values($data), $connection);
return self::connection($connection)->lastInsertId();
}
public static function update(string $table, array $data, array $where, string $connection = 'default'): int
{
$setParts = [];
$whereParts = [];
$params = [];
foreach ($data as $column => $value) {
$setParts[] = "{$column} = ?";
$params[] = $value;
}
foreach ($where as $column => $value) {
$whereParts[] = "{$column} = ?";
$params[] = $value;
}
$sql = sprintf(
'UPDATE %s SET %s WHERE %s',
$table,
implode(', ', $setParts),
implode(' AND ', $whereParts)
);
$stmt = self::execute($sql, $params, $connection);
return $stmt->rowCount();
}
public static function delete(string $table, array $where, string $connection = 'default'): int
{
$whereParts = [];
$params = [];
foreach ($where as $column => $value) {
$whereParts[] = "{$column} = ?";
$params[] = $value;
}
$sql = sprintf(
'DELETE FROM %s WHERE %s',
$table,
implode(' AND ', $whereParts)
);
$stmt = self::execute($sql, $params, $connection);
return $stmt->rowCount();
}
public static function close(string $name = 'default'): void
{
if (isset(self::$connections[$name])) {
self::$connections[$name] = null;
unset(self::$connections[$name]);
}
}
public static function closeAll(): void
{
self::$connections = [];
}
}

View File

@@ -0,0 +1,439 @@
<?php
declare(strict_types=1);
namespace Fendx\Db\ORM;
/**
* 实体基类
* 所有ORM实体都应该继承此类
*/
abstract class Entity
{
/**
* 主键字段名
*/
protected string $primaryKey = 'id';
/**
* 表名
*/
protected string $table;
/**
* 字段映射
*/
protected array $fields = [];
/**
* 隐藏字段
*/
protected array $hidden = [];
/**
* 可填充字段
*/
protected array $fillable = [];
/**
* 字段类型转换
*/
protected array $casts = [];
/**
* 创建时间字段
*/
protected ?string $createdAt = null;
/**
* 更新时间字段
*/
protected ?string $updatedAt = null;
/**
* 原始数据
*/
private array $original = [];
/**
* 脏数据
*/
private array $dirty = [];
public function __construct(array $attributes = [])
{
$this->fill($attributes);
$this->syncOriginal();
}
/**
* 填充属性
*/
public function fill(array $attributes): self
{
foreach ($attributes as $key => $value) {
$this->setAttribute($key, $value);
}
return $this;
}
/**
* 设置属性
*/
public function setAttribute(string $key, mixed $value): void
{
// 检查是否在可填充字段中
if (!empty($this->fillable) && !in_array($key, $this->fillable)) {
return;
}
// 类型转换
$value = $this->castAttribute($key, $value);
// 检查是否有变化
if (!$this->hasAttribute($key) || $this->getAttribute($key) !== $value) {
$this->dirty[$key] = $value;
}
$this->attributes[$key] = $value;
}
/**
* 获取属性
*/
public function getAttribute(string $key): mixed
{
$value = $this->attributes[$key] ?? null;
// 处理访问器
$method = 'get' . str_replace('_', '', ucwords($key, '_')) . 'Attribute';
if (method_exists($this, $method)) {
return $this->$method($value);
}
return $value;
}
/**
* 检查属性是否存在
*/
public function hasAttribute(string $key): bool
{
return array_key_exists($key, $this->attributes);
}
/**
* 类型转换
*/
private function castAttribute(string $key, mixed $value): mixed
{
if (!isset($this->casts[$key])) {
return $value;
}
$cast = $this->casts[$key];
switch ($cast) {
case 'int':
case 'integer':
return (int) $value;
case 'float':
case 'double':
return (float) $value;
case 'string':
return (string) $value;
case 'bool':
case 'boolean':
return (bool) $value;
case 'array':
return is_array($value) ? $value : json_decode($value, true) ?: [];
case 'json':
return json_encode($value, JSON_UNESCAPED_UNICODE);
case 'date':
return $value ? date('Y-m-d', strtotime($value)) : null;
case 'datetime':
return $value ? date('Y-m-d H:i:s', strtotime($value)) : null;
case 'timestamp':
return $value ? strtotime($value) : null;
default:
return $value;
}
}
/**
* 获取主键值
*/
public function getKey(): mixed
{
return $this->getAttribute($this->getKeyName());
}
/**
* 获取主键字段名
*/
public function getKeyName(): string
{
return $this->primaryKey;
}
/**
* 获取表名
*/
public function getTable(): string
{
if (isset($this->table)) {
return $this->table;
}
// 从类名推导表名
$className = static::class;
$shortName = substr($className, strrpos($className, '\\') + 1);
// 转换为蛇形命名
return strtolower(preg_replace('/([A-Z])/', '_$1', $shortName));
}
/**
* 获取所有属性
*/
public function getAttributes(): array
{
return $this->attributes;
}
/**
* 获取可见属性
*/
public function getVisible(): array
{
$visible = array_keys($this->attributes);
if (!empty($this->hidden)) {
$visible = array_diff($visible, $this->hidden);
}
return $visible;
}
/**
* 获取脏数据
*/
public function getDirty(): array
{
return $this->dirty;
}
/**
* 是否有脏数据
*/
public function isDirty(): bool
{
return !empty($this->dirty);
}
/**
* 检查指定字段是否为脏数据
*/
public function isDirtyAttribute(string $key): bool
{
return array_key_exists($key, $this->dirty);
}
/**
* 同步原始数据
*/
public function syncOriginal(): void
{
$this->original = $this->attributes;
$this->dirty = [];
}
/**
* 获取原始数据
*/
public function getOriginal(): array
{
return $this->original;
}
/**
* 获取原始属性值
*/
public function getOriginalAttribute(string $key): mixed
{
return $this->original[$key] ?? null;
}
/**
* 保存实体
*/
public function save(): bool
{
if ($this->getKey()) {
return $this->update();
} else {
return $this->insert();
}
}
/**
* 插入实体
*/
protected function insert(): bool
{
// 设置创建时间和更新时间
if ($this->createdAt) {
$this->setAttribute($this->createdAt, date('Y-m-d H:i:s'));
}
if ($this->updatedAt) {
$this->setAttribute($this->updatedAt, date('Y-m-d H:i:s'));
}
// 这里应该调用实际的数据库插入逻辑
// 简化实现实际应该使用QueryBuilder
$data = $this->getDirty();
// 模拟插入
$id = $this->performInsert($data);
if ($id) {
$this->setAttribute($this->primaryKey, $id);
$this->syncOriginal();
return true;
}
return false;
}
/**
* 更新实体
*/
protected function update(): bool
{
if (!$this->isDirty()) {
return true;
}
// 设置更新时间
if ($this->updatedAt) {
$this->setAttribute($this->updatedAt, date('Y-m-d H:i:s'));
}
$data = $this->getDirty();
// 模拟更新
$affected = $this->performUpdate($data);
if ($affected > 0) {
$this->syncOriginal();
return true;
}
return false;
}
/**
* 删除实体
*/
public function delete(): bool
{
if (!$this->getKey()) {
return false;
}
// 模拟删除
$affected = $this->performDelete();
return $affected > 0;
}
/**
* 执行插入(需要子类实现)
*/
protected function performInsert(array $data): mixed
{
// 实际实现应该使用QueryBuilder
return null;
}
/**
* 执行更新(需要子类实现)
*/
protected function performUpdate(array $data): int
{
// 实际实现应该使用QueryBuilder
return 0;
}
/**
* 执行删除(需要子类实现)
*/
protected function performDelete(): int
{
// 实际实现应该使用QueryBuilder
return 0;
}
/**
* 转换为数组
*/
public function toArray(): array
{
$attributes = [];
foreach ($this->getVisible() as $key) {
$attributes[$key] = $this->getAttribute($key);
}
return $attributes;
}
/**
* 转换为JSON
*/
public function toJson(): string
{
return json_encode($this->toArray(), JSON_UNESCAPED_UNICODE);
}
/**
* 魔术方法:获取属性
*/
public function __get(string $name): mixed
{
return $this->getAttribute($name);
}
/**
* 魔术方法:设置属性
*/
public function __set(string $name, mixed $value): void
{
$this->setAttribute($name, $value);
}
/**
* 魔术方法:检查属性是否存在
*/
public function __isset(string $name): bool
{
return $this->hasAttribute($name);
}
/**
* 魔术方法:取消属性
*/
public function __unset(string $name): void
{
unset($this->attributes[$name]);
unset($this->dirty[$name]);
}
/**
* 魔术方法:转换为字符串
*/
public function __toString(): string
{
return $this->toJson();
}
}

View File

@@ -0,0 +1,189 @@
<?php
declare(strict_types=1);
namespace Fendx\Db\ORM;
use Fendx\Db\DB;
use Fendx\Db\Annotation\Table;
use Fendx\Db\Annotation\Id;
use Fendx\Db\Annotation\Column;
use ReflectionClass;
use ReflectionProperty;
abstract class Model
{
protected static ?string $tableName = null;
protected static ?string $connection = 'default';
public static function getTableName(): string
{
if (static::$tableName !== null) {
return static::$tableName;
}
$reflection = new ReflectionClass(static::class);
$tableAttribute = $reflection->getAttributes(Table::class);
if (!empty($tableAttribute)) {
$table = $tableAttribute[0]->newInstance();
return $table->name;
}
// 默认使用类名的蛇形命名
return strtolower(preg_replace('/([A-Z])/', '_$1', lcfirst(substr(static::class, strrpos(static::class, '\\') + 1))));
}
public static function getConnection(): string
{
return static::$connection;
}
public static function find(int $id): ?static
{
$sql = "SELECT * FROM " . static::getTableName() . " WHERE id = :id LIMIT 1";
$data = DB::fetch($sql, ['id' => $id], static::getConnection());
if ($data === null) {
return null;
}
return static::hydrate($data);
}
public static function all(): array
{
$sql = "SELECT * FROM " . static::getTableName();
$data = DB::fetchAll($sql, [], static::getConnection());
return array_map(fn($item) => static::hydrate($item), $data);
}
public static function where(string $column, mixed $value, string $operator = '='): QueryBuilder
{
return new QueryBuilder(static::class)->where($column, $value, $operator);
}
public static function create(array $data): static
{
$model = new static();
$model->fill($data);
$model->save();
return $model;
}
public function save(): bool
{
$reflection = new ReflectionClass($this);
$properties = $reflection->getProperties(ReflectionProperty::IS_PUBLIC);
$data = [];
$hasId = false;
foreach ($properties as $property) {
$idAttribute = $property->getAttributes(Id::class);
if (!empty($idAttribute)) {
if ($property->getValue($this) !== null) {
$hasId = true;
}
continue;
}
$data[$property->getName()] = $property->getValue($this);
}
if ($hasId) {
// 更新
$idProperty = $reflection->getProperty('id');
$id = $idProperty->getValue($this);
return DB::update(static::getTableName(), $data, ['id' => $id], static::getConnection()) > 0;
} else {
// 插入
$insertId = DB::insert(static::getTableName(), $data, static::getConnection());
if ($insertId) {
$idProperty = $reflection->getProperty('id');
$idProperty->setValue($this, $insertId);
return true;
}
}
return false;
}
public function update(array $data): bool
{
$reflection = new ReflectionClass($this);
$idProperty = $reflection->getProperty('id');
$id = $idProperty->getValue($this);
if ($id === null) {
return false;
}
return DB::update(static::getTableName(), $data, ['id' => $id], static::getConnection()) > 0;
}
public function delete(): bool
{
$reflection = new ReflectionClass($this);
$idProperty = $reflection->getProperty('id');
$id = $idProperty->getValue($this);
if ($id === null) {
return false;
}
return DB::delete(static::getTableName(), ['id' => $id], static::getConnection()) > 0;
}
public function fill(array $data): void
{
$reflection = new ReflectionClass($this);
$properties = $reflection->getProperties(ReflectionProperty::IS_PUBLIC);
foreach ($properties as $property) {
$propertyName = $property->getName();
if (isset($data[$propertyName])) {
$property->setValue($this, $data[$propertyName]);
}
}
}
public function toArray(): array
{
$reflection = new ReflectionClass($this);
$properties = $reflection->getProperties(ReflectionProperty::IS_PUBLIC);
$data = [];
foreach ($properties as $property) {
$data[$property->getName()] = $property->getValue($this);
}
return $data;
}
protected static function hydrate(array $data): static
{
$model = new static();
$model->fill($data);
return $model;
}
public function getAttribute(string $key): mixed
{
$reflection = new ReflectionClass($this);
if ($reflection->hasProperty($key)) {
$property = $reflection->getProperty($key);
return $property->getValue($this);
}
return null;
}
public function setAttribute(string $key, mixed $value): void
{
$reflection = new ReflectionClass($this);
if ($reflection->hasProperty($key)) {
$property = $reflection->getProperty($key);
$property->setValue($this, $value);
}
}
}

View File

@@ -0,0 +1,251 @@
<?php
declare(strict_types=1);
namespace Fendx\Db\ORM;
use Fendx\Db\DB;
final class QueryBuilder
{
private string $modelClass;
private array $wheres = [];
private array $orders = [];
private ?int $limit = null;
private ?int $offset = null;
private array $joins = [];
public function __construct(string $modelClass)
{
$this->modelClass = $modelClass;
}
public function where(string $column, mixed $value, string $operator = '='): self
{
$this->wheres[] = [
'column' => $column,
'operator' => $operator,
'value' => $value,
'type' => 'and'
];
return $this;
}
public function whereIn(string $column, array $values): self
{
$this->wheres[] = [
'column' => $column,
'operator' => 'IN',
'value' => $values,
'type' => 'and'
];
return $this;
}
public function whereNotIn(string $column, array $values): self
{
$this->wheres[] = [
'column' => $column,
'operator' => 'NOT IN',
'value' => $values,
'type' => 'and'
];
return $this;
}
public function whereLike(string $column, string $value): self
{
$this->wheres[] = [
'column' => $column,
'operator' => 'LIKE',
'value' => $value,
'type' => 'and'
];
return $this;
}
public function orWhere(string $column, mixed $value, string $operator = '='): self
{
$this->wheres[] = [
'column' => $column,
'operator' => $operator,
'value' => $value,
'type' => 'or'
];
return $this;
}
public function orderBy(string $column, string $direction = 'ASC'): self
{
$this->orders[] = [
'column' => $column,
'direction' => strtoupper($direction)
];
return $this;
}
public function orderByDesc(string $column): self
{
return $this->orderBy($column, 'DESC');
}
public function limit(int $limit): self
{
$this->limit = $limit;
return $this;
}
public function offset(int $offset): self
{
$this->offset = $offset;
return $this;
}
public function first(): ?Model
{
$this->limit(1);
$results = $this->get();
return $results[0] ?? null;
}
public function get(): array
{
$sql = $this->buildQuery();
$params = $this->buildParams();
$data = DB::fetchAll($sql, $params, $this->modelClass::getConnection());
return array_map(fn($item) => $this->modelClass::hydrate($item), $data);
}
public function count(): int
{
$originalSelect = $this->select;
$this->select = 'COUNT(*) as count';
$sql = $this->buildQuery();
$params = $this->buildParams();
$result = DB::fetch($sql, $params, $this->modelClass::getConnection());
$this->select = $originalSelect;
return (int)($result['count'] ?? 0);
}
public function exists(): bool
{
return $this->count() > 0;
}
public function delete(): int
{
$sql = "DELETE FROM " . $this->modelClass::getTableName();
if (!empty($this->wheres)) {
$sql .= " WHERE " . $this->buildWhereClause();
}
$params = $this->buildParams();
return DB::execute($sql, $params, $this->modelClass::getConnection())->rowCount();
}
public function update(array $data): int
{
$setParts = [];
$params = [];
foreach ($data as $column => $value) {
$setParts[] = "{$column} = ?";
$params[] = $value;
}
$sql = "UPDATE " . $this->modelClass::getTableName() . " SET " . implode(', ', $setParts);
if (!empty($this->wheres)) {
$sql .= " WHERE " . $this->buildWhereClause();
}
$params = array_merge($params, $this->buildParams());
return DB::execute($sql, $params, $this->modelClass::getConnection())->rowCount();
}
private function buildQuery(): string
{
$sql = "SELECT * FROM " . $this->modelClass::getTableName();
if (!empty($this->joins)) {
foreach ($this->joins as $join) {
$sql .= " {$join['type']} JOIN {$join['table']} ON {$join['on']}";
}
}
if (!empty($this->wheres)) {
$sql .= " WHERE " . $this->buildWhereClause();
}
if (!empty($this->orders)) {
$orderParts = [];
foreach ($this->orders as $order) {
$orderParts[] = "{$order['column']} {$order['direction']}";
}
$sql .= " ORDER BY " . implode(', ', $orderParts);
}
if ($this->limit !== null) {
$sql .= " LIMIT {$this->limit}";
}
if ($this->offset !== null) {
$sql .= " OFFSET {$this->offset}";
}
return $sql;
}
private function buildWhereClause(): string
{
$clauses = [];
foreach ($this->wheres as $index => $where) {
$clause = $this->buildWhereClausePart($where);
if ($index > 0) {
$clause = strtoupper($where['type']) . ' ' . $clause;
}
$clauses[] = $clause;
}
return implode(' ', $clauses);
}
private function buildWhereClausePart(array $where): string
{
$column = $where['column'];
$operator = $where['operator'];
if ($operator === 'IN' || $operator === 'NOT IN') {
$placeholders = str_repeat('?,', count($where['value']) - 1) . '?';
return "{$column} {$operator} ({$placeholders})";
}
return "{$column} {$operator} ?";
}
private function buildParams(): array
{
$params = [];
foreach ($this->wheres as $where) {
if ($where['operator'] === 'IN' || $where['operator'] === 'NOT IN') {
$params = array_merge($params, $where['value']);
} else {
$params[] = $where['value'];
}
}
return $params;
}
}

View File

@@ -0,0 +1,144 @@
<?php
declare(strict_types=1);
namespace Fendx\Db\Transaction;
use Fendx\Db\DB;
use Fendx\Db\Annotation\Transactional;
use Fendx\Core\Aop\Aspect;
use Closure;
final class TransactionManager
{
private static array $transactions = [];
private static array $rollbackOnly = [];
public static function begin(string $connection = 'default'): void
{
$key = self::getTransactionKey($connection);
if (!isset(self::$transactions[$key])) {
DB::beginTransaction($connection);
self::$transactions[$key] = true;
}
}
public static function commit(string $connection = 'default'): void
{
$key = self::getTransactionKey($connection);
if (isset(self::$transactions[$key])) {
if (!isset(self::$rollbackOnly[$key])) {
DB::commit($connection);
} else {
DB::rollback($connection);
}
unset(self::$transactions[$key]);
unset(self::$rollbackOnly[$key]);
}
}
public static function rollback(string $connection = 'default'): void
{
$key = self::getTransactionKey($connection);
if (isset(self::$transactions[$key])) {
DB::rollback($connection);
unset(self::$transactions[$key]);
unset(self::$rollbackOnly[$key]);
}
}
public static function setRollbackOnly(string $connection = 'default'): void
{
$key = self::getTransactionKey($connection);
self::$rollbackOnly[$key] = true;
}
public static function isInTransaction(string $connection = 'default'): bool
{
$key = self::getTransactionKey($connection);
return isset(self::$transactions[$key]);
}
public static function executeInTransaction(callable $callback, string $connection = 'default'): mixed
{
$key = self::getTransactionKey($connection);
$isNewTransaction = !isset(self::$transactions[$key]);
if ($isNewTransaction) {
self::begin($connection);
}
try {
$result = $callback();
if ($isNewTransaction) {
self::commit($connection);
}
return $result;
} catch (\Throwable $e) {
if ($isNewTransaction) {
self::rollback($connection);
} else {
self::setRollbackOnly($connection);
}
throw $e;
}
}
public static function createTransactionalAspect(): Aspect
{
return new Aspect()
->around(function (object $target, string $method, array $args, Closure $next) {
$reflection = new \ReflectionClass($target);
$methodReflection = $reflection->getMethod($method);
$transactionalAttributes = $methodReflection->getAttributes(Transactional::class);
if (empty($transactionalAttributes)) {
return $next();
}
$transactional = $transactionalAttributes[0]->newInstance();
$connection = $transactional->connection;
return self::executeInTransaction(function () use ($next) {
return $next();
}, $connection);
});
}
private static function getTransactionKey(string $connection): string
{
return $connection;
}
public static function clear(): void
{
foreach (array_keys(self::$transactions) as $connection) {
try {
self::rollback($connection);
} catch (\Throwable $e) {
// 忽略清理时的错误
}
}
self::$transactions = [];
self::$rollbackOnly = [];
}
public static function getActiveTransactions(): array
{
return array_keys(self::$transactions);
}
public static function isRollbackOnly(string $connection = 'default'): bool
{
$key = self::getTransactionKey($connection);
return isset(self::$rollbackOnly[$key]);
}
}