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

相关文章
|
22天前
|
缓存 安全 PHP
PHP实战:从零到一构建企业级应用(一)
教程来源 本文以实战为导向,通过构建企业级用户管理系统,系统讲解PHP开发全流程:从Docker环境搭建、PSR-4自动加载、RBAC权限设计,到MySQL数据库建模、Phinx迁移、JWT认证、Redis缓存、日志审计与安全防护,助你跨越理论与实践鸿沟。
|
3月前
|
关系型数据库 MySQL Linux
phpMyAdmin-4.0.10.10安装教程 手把手教你配置与部署
本指南详解phpMyAdmin 4.0.10.10在Linux服务器的快速部署:下载安装包→上传至服务器→解压→移至网站根目录并重命名→设置权限→配置blowfish_secret及数据库连接参数→即可通过IP/phpmyadmin访问登录。
|
存储 Java 数据库
若依框架----源码分析(@Log)
若依框架----源码分析(@Log)
3595 1
|
2月前
|
前端开发 数据可视化 定位技术
什么是CMS,网站内容管理系统(CMS)详解与建站指南
本文系统介绍了网站内容管理系统(CMS)的核心概念、主要功能与应用场景,详细对比了主流开源与商业CMS的特点,并提供了从规划到上线的完整建站流程指南,帮助读者根据自身需求选择合适工具并高效构建数字平台。
1008 4
|
5月前
|
自然语言处理 JavaScript 前端开发
全面解析 i18n:从概念到实践,再到底层原理
本文系统讲解国际化(i18n)的核心概念与实现原理,涵盖多语言文本、日期、数字、复数等处理方式,结合 i18next 与 Vue I18n 实战案例,深入剖析资源分离、环境识别与动态替换三大机制,并分享插值、格式化、CI/CD 集成等最佳实践,助力构建可扩展的全球化应用。
1968 15
npm WARN deprecated core-js@2.6.12 core-js@<3.3 is no longer maintained and not recommended for usa
npm WARN deprecated core-js@2.6.12 core-js@<3.3 is no longer maintained and not recommended for usa
1323 0
|
存储 Windows
移动硬盘数据恢复 详细操作指南 (6种方法)
很多情况下,移动硬盘丢失的数据是可以恢复的。本文将给大家详细介绍移动硬盘数据恢复的完整过程,帮助大家有效应对数据丢失问题。
|
存储 缓存 编译器
DSP存储器与寄存器管理
DSP存储器与寄存器管理
705 1
|
关系型数据库 MySQL 数据库
XAMPP报错:mysqli::real_connect(): (HY000/1045): Access denied for user ‘pma‘@‘localhost‘
XAMPP报错:mysqli::real_connect(): (HY000/1045): Access denied for user ‘pma‘@‘localhost‘
Access denied for user ‘xxx’@‘localhost‘(using password:YES)
Access denied for user ‘xxx’@‘localhost‘(using password:YES)