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-db/composer.json
Normal file
24
fendx-framework/fendx-db/composer.json
Normal 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
|
||||
}
|
||||
23
fendx-framework/fendx-db/src/Annotation/Column.php
Normal file
23
fendx-framework/fendx-db/src/Annotation/Column.php
Normal 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;
|
||||
}
|
||||
}
|
||||
15
fendx-framework/fendx-db/src/Annotation/Id.php
Normal file
15
fendx-framework/fendx-db/src/Annotation/Id.php
Normal 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;
|
||||
}
|
||||
}
|
||||
17
fendx-framework/fendx-db/src/Annotation/Table.php
Normal file
17
fendx-framework/fendx-db/src/Annotation/Table.php
Normal 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;
|
||||
}
|
||||
}
|
||||
17
fendx-framework/fendx-db/src/Annotation/Transactional.php
Normal file
17
fendx-framework/fendx-db/src/Annotation/Transactional.php
Normal 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;
|
||||
}
|
||||
}
|
||||
171
fendx-framework/fendx-db/src/DB.php
Normal file
171
fendx-framework/fendx-db/src/DB.php
Normal 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 = [];
|
||||
}
|
||||
}
|
||||
439
fendx-framework/fendx-db/src/ORM/Entity.php
Normal file
439
fendx-framework/fendx-db/src/ORM/Entity.php
Normal 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();
|
||||
}
|
||||
}
|
||||
189
fendx-framework/fendx-db/src/ORM/Model.php
Normal file
189
fendx-framework/fendx-db/src/ORM/Model.php
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
251
fendx-framework/fendx-db/src/ORM/QueryBuilder.php
Normal file
251
fendx-framework/fendx-db/src/ORM/QueryBuilder.php
Normal 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;
|
||||
}
|
||||
}
|
||||
144
fendx-framework/fendx-db/src/Transaction/TransactionManager.php
Normal file
144
fendx-framework/fendx-db/src/Transaction/TransactionManager.php
Normal 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]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user