PHP实战:从零到一构建企业级应用(二)

简介: 教程来源 https://fndvx.cn/category/jiankang.html 本架构实现轻量级ORM核心:基于PDO的单例数据库连接、支持填充/查询/软删除的Model基类,以及链式调用的QueryBuilder;配合User/Role模型,集成密码哈希、JWT鉴权、UUID生成与RBAC权限控制,兼顾安全与扩展性。

第三部分:核心架构实现

3.1 数据库连接与模型基类

// config/database.php
<?php
return [
    'default' => 'mysql',
    'connections' => [
        'mysql' => [
            'driver' => 'mysql',
            'host' => $_ENV['DB_HOST'] ?? 'localhost',
            'port' => $_ENV['DB_PORT'] ?? '3306',
            'database' => $_ENV['DB_NAME'] ?? 'user_system',
            'username' => $_ENV['DB_USER'] ?? 'root',
            'password' => $_ENV['DB_PASS'] ?? '',
            'charset' => 'utf8mb4',
            'collation' => 'utf8mb4_unicode_ci',
            'prefix' => '',
            'options' => [
                PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
                PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
                PDO::ATTR_EMULATE_PREPARES => false,
            ],
        ],
    ],
];
// app/Core/Database/Connection.php
<?php
namespace App\Core\Database;

use PDO;
use PDOException;

class Connection
{
    private static ?PDO $instance = null;
    private static array $config = [];

    public static function setConfig(array $config): void
    {
        self::$config = $config;
    }

    public static function getInstance(): PDO
    {
        if (self::$instance === null) {
            self::connect();
        }
        return self::$instance;
    }

    private static function connect(): void
    {
        $config = self::$config;
        $dsn = sprintf(
            "%s:host=%s;port=%s;dbname=%s;charset=%s",
            $config['driver'],
            $config['host'],
            $config['port'],
            $config['database'],
            $config['charset']
        );

        try {
            self::$instance = new PDO($dsn, $config['username'], $config['password'], $config['options']);
        } catch (PDOException $e) {
            throw new \RuntimeException("数据库连接失败: " . $e->getMessage());
        }
    }

    public static function beginTransaction(): bool
    {
        return self::getInstance()->beginTransaction();
    }

    public static function commit(): bool
    {
        return self::getInstance()->commit();
    }

    public static function rollback(): bool
    {
        return self::getInstance()->rollBack();
    }
}

3.2 Model基类

// app/Core/Model.php
<?php
namespace App\Core;

use App\Core\Database\Connection;
use PDO;

abstract class Model
{
    protected static string $table;
    protected static string $primaryKey = 'id';
    protected array $attributes = [];
    protected array $original = [];
    protected array $fillable = [];
    protected array $hidden = [];
    protected array $casts = [];

    public function __construct(array $attributes = [])
    {
        $this->fill($attributes);
    }

    public function fill(array $attributes): self
    {
        foreach ($attributes as $key => $value) {
            if (in_array($key, $this->fillable)) {
                $this->setAttribute($key, $value);
            }
        }
        return $this;
    }

    public function setAttribute(string $key, $value): self
    {
        $this->attributes[$key] = $value;
        return $this;
    }

    public function getAttribute(string $key)
    {
        return $this->attributes[$key] ?? null;
    }

    public function __get($key)
    {
        return $this->getAttribute($key);
    }

    public function __set($key, $value)
    {
        $this->setAttribute($key, $value);
    }

    public function save(): bool
    {
        if (isset($this->attributes[static::$primaryKey])) {
            return $this->update();
        }
        return $this->insert();
    }

    protected function insert(): bool
    {
        $fields = array_keys($this->attributes);
        $placeholders = ':' . implode(', :', $fields);
        $sql = sprintf(
            "INSERT INTO %s (%s) VALUES (%s)",
            static::$table,
            implode(', ', $fields),
            $placeholders
        );

        $stmt = Connection::getInstance()->prepare($sql);
        $result = $stmt->execute($this->attributes);

        if ($result) {
            $this->attributes[static::$primaryKey] = Connection::getInstance()->lastInsertId();
            $this->syncOriginal();
        }

        return $result;
    }

    protected function update(): bool
    {
        $fields = [];
        foreach ($this->attributes as $key => $value) {
            if ($key !== static::$primaryKey) {
                $fields[] = "{$key} = :{$key}";
            }
        }

        $sql = sprintf(
            "UPDATE %s SET %s WHERE %s = :primary_key",
            static::$table,
            implode(', ', $fields),
            static::$primaryKey
        );

        $params = $this->attributes;
        $params['primary_key'] = $this->attributes[static::$primaryKey];

        $stmt = Connection::getInstance()->prepare($sql);
        $result = $stmt->execute($params);

        if ($result) {
            $this->syncOriginal();
        }

        return $result;
    }

    public function delete(): bool
    {
        $sql = sprintf(
            "DELETE FROM %s WHERE %s = :primary_key",
            static::$table,
            static::$primaryKey
        );

        $stmt = Connection::getInstance()->prepare($sql);
        return $stmt->execute(['primary_key' => $this->attributes[static::$primaryKey]]);
    }

    public static function find($id): ?static
    {
        $sql = sprintf(
            "SELECT * FROM %s WHERE %s = :id LIMIT 1",
            static::$table,
            static::$primaryKey
        );

        $stmt = Connection::getInstance()->prepare($sql);
        $stmt->execute(['id' => $id]);
        $result = $stmt->fetch(PDO::FETCH_ASSOC);

        if ($result) {
            return (new static())->syncOriginal()->fill($result);
        }

        return null;
    }

    public static function where(string $column, string $operator, $value): QueryBuilder
    {
        return (new QueryBuilder(static::class))->where($column, $operator, $value);
    }

    public static function all(): array
    {
        $sql = sprintf("SELECT * FROM %s", static::$table);
        $stmt = Connection::getInstance()->query($sql);
        $results = $stmt->fetchAll(PDO::FETCH_ASSOC);

        return array_map(function($result) {
            return (new static())->syncOriginal()->fill($result);
        }, $results);
    }

    protected function syncOriginal(): self
    {
        $this->original = $this->attributes;
        return $this;
    }

    public function toArray(): array
    {
        $attributes = $this->attributes;

        foreach ($this->hidden as $key) {
            unset($attributes[$key]);
        }

        foreach ($this->casts as $key => $type) {
            if (isset($attributes[$key])) {
                $attributes[$key] = $this->cast($attributes[$key], $type);
            }
        }

        return $attributes;
    }

    protected function cast($value, string $type)
    {
        return match($type) {
            'int', 'integer' => (int) $value,
            'float', 'double' => (float) $value,
            'bool', 'boolean' => (bool) $value,
            'json', 'array' => json_decode($value, true),
            default => $value,
        };
    }
}

3.3 QueryBuilder实现

// app/Core/Database/QueryBuilder.php
<?php
namespace App\Core\Database;

use PDO;

class QueryBuilder
{
    private string $modelClass;
    private string $table;
    private array $wheres = [];
    private array $bindings = [];
    private array $orderBy = [];
    private ?int $limit = null;
    private ?int $offset = null;

    public function __construct(string $modelClass)
    {
        $this->modelClass = $modelClass;
        $this->table = $modelClass::$table;
    }

    public function where(string $column, string $operator, $value): self
    {
        $this->wheres[] = [
            'type' => 'basic',
            'column' => $column,
            'operator' => $operator,
            'value' => $value
        ];
        $this->bindings[] = $value;
        return $this;
    }

    public function whereIn(string $column, array $values): self
    {
        $placeholders = implode(',', array_fill(0, count($values), '?'));
        $this->wheres[] = [
            'type' => 'in',
            'column' => $column,
            'values' => $values,
            'placeholders' => $placeholders
        ];
        $this->bindings = array_merge($this->bindings, $values);
        return $this;
    }

    public function orderBy(string $column, string $direction = 'ASC'): self
    {
        $this->orderBy[] = "{$column} {$direction}";
        return $this;
    }

    public function limit(int $limit): self
    {
        $this->limit = $limit;
        return $this;
    }

    public function offset(int $offset): self
    {
        $this->offset = $offset;
        return $this;
    }

    public function get(): array
    {
        $sql = "SELECT * FROM {$this->table}";

        if (!empty($this->wheres)) {
            $sql .= " WHERE " . $this->buildWhereClause();
        }

        if (!empty($this->orderBy)) {
            $sql .= " ORDER BY " . implode(', ', $this->orderBy);
        }

        if ($this->limit !== null) {
            $sql .= " LIMIT {$this->limit}";
        }

        if ($this->offset !== null) {
            $sql .= " OFFSET {$this->offset}";
        }

        $stmt = Connection::getInstance()->prepare($sql);
        $stmt->execute($this->bindings);
        $results = $stmt->fetchAll(PDO::FETCH_ASSOC);

        return array_map(function($result) {
            return (new $this->modelClass())->syncOriginal()->fill($result);
        }, $results);
    }

    public function first(): ?object
    {
        $this->limit(1);
        $results = $this->get();
        return $results[0] ?? null;
    }

    public function count(): int
    {
        $sql = "SELECT COUNT(*) as count FROM {$this->table}";

        if (!empty($this->wheres)) {
            $sql .= " WHERE " . $this->buildWhereClause();
        }

        $stmt = Connection::getInstance()->prepare($sql);
        $stmt->execute($this->bindings);
        $result = $stmt->fetch(PDO::FETCH_ASSOC);

        return (int) $result['count'];
    }

    public function paginate(int $perPage = 15, int $page = 1): array
    {
        $total = $this->count();
        $lastPage = ceil($total / $perPage);
        $page = max(1, min($page, $lastPage));
        $offset = ($page - 1) * $perPage;

        $items = $this->limit($perPage)->offset($offset)->get();

        return [
            'data' => $items,
            'meta' => [
                'current_page' => $page,
                'per_page' => $perPage,
                'total' => $total,
                'last_page' => $lastPage,
            ]
        ];
    }

    private function buildWhereClause(): string
    {
        $clauses = [];
        foreach ($this->wheres as $where) {
            if ($where['type'] === 'basic') {
                $clauses[] = "{$where['column']} {$where['operator']} ?";
            } elseif ($where['type'] === 'in') {
                $clauses[] = "{$where['column']} IN ({$where['placeholders']})";
            }
        }
        return implode(' AND ', $clauses);
    }
}

第四部分:用户模型与业务逻辑

4.1 User模型

// app/Models/User.php
<?php
namespace App\Models;

use App\Core\Model;
use App\Core\Database\Connection;
use App\Services\HashService;
use App\Services\JWTService;
use Ramsey\Uuid\Uuid;

class User extends Model
{
    protected static string $table = 'users';
    protected static string $primaryKey = 'id';

    protected array $fillable = [
        'uuid', 'username', 'email', 'phone', 
        'password', 'avatar', 'role_id', 'status'
    ];

    protected array $hidden = [
        'password', 'deleted_at'
    ];

    protected array $casts = [
        'id' => 'int',
        'role_id' => 'int',
        'status' => 'int',
        'email_verified_at' => 'datetime',
        'last_login_at' => 'datetime',
        'created_at' => 'datetime',
        'updated_at' => 'datetime',
    ];

    public function __construct(array $attributes = [])
    {
        parent::__construct($attributes);

        if (empty($this->uuid)) {
            $this->uuid = Uuid::uuid4()->toString();
        }
    }

    /**
     * 创建新用户
     */
    public static function create(array $data): ?self
    {
        $user = new self();
        $user->fill($data);

        if (isset($data['password'])) {
            $user->password = HashService::make($data['password']);
        }

        return $user->save() ? $user : null;
    }

    /**
     * 验证用户凭证
     */
    public static function authenticate(string $login, string $password): ?self
    {
        $user = self::where('email', '=', $login)
            ->orWhere('username', '=', $login)
            ->first();

        if ($user && HashService::verify($password, $user->password)) {
            return $user;
        }

        return null;
    }

    /**
     * 更新最后登录信息
     */
    public function updateLoginInfo(string $ip): bool
    {
        $this->last_login_ip = $ip;
        $this->last_login_at = date('Y-m-d H:i:s');
        return $this->save();
    }

    /**
     * 检查用户权限
     */
    public function hasPermission(string $permission): bool
    {
        $role = Role::find($this->role_id);
        if (!$role) {
            return false;
        }

        return $role->hasPermission($permission);
    }

    /**
     * 获取用户角色
     */
    public function getRole(): ?Role
    {
        return Role::find($this->role_id);
    }

    /**
     * 生成API Token
     */
    public function generateApiToken(): string
    {
        return JWTService::generate([
            'user_id' => $this->id,
            'uuid' => $this->uuid,
            'email' => $this->email,
            'role_id' => $this->role_id
        ]);
    }

    /**
     * 软删除用户
     */
    public function softDelete(): bool
    {
        $sql = "UPDATE users SET deleted_at = NOW() WHERE id = :id";
        $stmt = Connection::getInstance()->prepare($sql);
        return $stmt->execute(['id' => $this->id]);
    }
}

4.2 Role模型

// app/Models/Role.php
<?php
namespace App\Models;

use App\Core\Model;

class Role extends Model
{
    protected static string $table = 'roles';

    protected array $fillable = ['name', 'slug', 'description', 'permissions'];

    protected array $casts = [
        'permissions' => 'array',
    ];

    /**
     * 检查是否有权限
     */
    public function hasPermission(string $permission): bool
    {
        $permissions = $this->permissions ?? [];

        // 超级管理员拥有所有权限
        if (in_array('*', $permissions)) {
            return true;
        }

        return in_array($permission, $permissions);
    }

    /**
     * 授予权限
     */
    public function givePermission(string $permission): self
    {
        $permissions = $this->permissions ?? [];
        if (!in_array($permission, $permissions)) {
            $permissions[] = $permission;
            $this->permissions = $permissions;
            $this->save();
        }
        return $this;
    }

    /**
     * 撤销权限
     */
    public function revokePermission(string $permission): self
    {
        $permissions = $this->permissions ?? [];
        $key = array_search($permission, $permissions);
        if ($key !== false) {
            unset($permissions[$key]);
            $this->permissions = array_values($permissions);
            $this->save();
        }
        return $this;
    }
}

来源:
https://fndvx.cn/category/jiankang.html

相关文章
|
6天前
|
人工智能 JSON 监控
Claude Code 源码泄露:一份价值亿元的 AI 工程公开课
我以为顶级 AI 产品的护城河是模型。读完这 51.2 万行泄露的源码,我发现自己错了。
4310 17
|
16天前
|
人工智能 JSON 机器人
让龙虾成为你的“公众号分身” | 阿里云服务器玩Openclaw
本文带你零成本玩转OpenClaw:学生认证白嫖6个月阿里云服务器,手把手配置飞书机器人、接入免费/高性价比AI模型(NVIDIA/通义),并打造微信公众号“全自动分身”——实时抓热榜、AI选题拆解、一键发布草稿,5分钟完成热点→文章全流程!
14940 138
让龙虾成为你的“公众号分身” | 阿里云服务器玩Openclaw
|
5天前
|
人工智能 数据可视化 安全
王炸组合!阿里云 OpenClaw X 飞书 CLI,开启 Agent 基建狂潮!(附带免费使用6个月服务器)
本文详解如何用阿里云Lighthouse一键部署OpenClaw,结合飞书CLI等工具,让AI真正“动手”——自动群发、生成科研日报、整理知识库。核心理念:未来软件应为AI而生,CLI即AI的“手脚”,实现高效、安全、可控的智能自动化。
3097 8
王炸组合!阿里云 OpenClaw X 飞书 CLI,开启 Agent 基建狂潮!(附带免费使用6个月服务器)
|
7天前
|
人工智能 自然语言处理 数据挖掘
零基础30分钟搞定 Claude Code,这一步90%的人直接跳过了
本文直击Claude Code使用痛点,提供零基础30分钟上手指南:强调必须配置“工作上下文”(about-me.md+anti-ai-style.md)、采用Cowork/Code模式、建立标准文件结构、用提问式提示词驱动AI理解→规划→执行。附可复制模板与真实项目启动法,助你将Claude从聊天工具升级为高效执行系统。
|
6天前
|
人工智能 定位技术
Claude Code源码泄露:8大隐藏功能曝光
2026年3月,Anthropic因配置失误致Claude Code超51万行源码泄露,意外促成“被动开源”。代码中藏有8大未发布功能,揭示其向“超级智能体”演进的完整蓝图,引发AI编程领域震动。(239字)
2448 9