第一周 周五:Hyperf 依赖注入、注解路由与数据库连接


在这里插入图片描述

今日目标
  1. 理解 Hyperf 的依赖注入容器,学会通过构造函数、#[Inject] 注解注入依赖。
  2. 掌握 注解路由,使用 #[Controller]#[RequestMapping] 定义 RESTful 接口,告别手写路由配置。
  3. 安装并配置 hyperf/database,实现模型与数据表的映射,完成第一个 CRUD 接口。
  4. 结合验证器与异常处理(衔接下周一内容),编写一个完整的文章创建接口,体验框架的便捷。
  5. 测试所有接口,验证数据库读写、参数校验和依赖注入是否正常工作。

一、环境准备(约 20 分钟)

继续使用周四创建的 hyperf-app 项目,确保 Docker 容器已启动并进入:

cd swoole-course
docker-compose exec swoole bash
cd /var/www/hyperf-app
1. 确认 MySQL 服务可用

昨天我们已经在 Docker Compose 中加入了 MySQL(如果还没有,请参考周四内容添加)。测试连接:

php -r "new PDO('mysql:host=mysql;dbname=hyperf_blog', 'blog_user', 'blog_pass'); echo 'OK';"

若输出 OK,说明数据库连接正常。

2. 安装数据库组件(若尚未安装)

周四我们已经安装了 hyperf/database,如果没有安装,执行:

composer require hyperf/database hyperf/db-connection

发布配置文件(如果还没有):

php bin/hyperf.php vendor:publish hyperf/database
3. 创建数据表

我们需要一张 articles 表用于今天的 CRUD。在 MySQL 中执行以下 SQL(可以通过 mysql 客户端或 PHP 脚本):

CREATE TABLE `articles` (
    `id` INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    `title` VARCHAR(255) NOT NULL,
    `content` TEXT NOT NULL,
    `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    `updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

在容器内使用 mysql 客户端:

mysql -h mysql -u blog_user -pblog_pass hyperf_blog

粘贴 SQL 执行。或者直接用 PHP 脚本创建。


二、知识核心:DI、注解路由与 Eloquent ORM(约 1.5 小时)

1. 依赖注入(DI)容器

Hyperf 内置了强大的 PSR-11 容器,它管理对象的创建和依赖解析。我们不需要手动 new 对象,而是通过容器获取,或者由框架自动注入。

三种注入方式

  • 构造函数注入:在类构造函数中声明依赖,容器会自动解析并传入。
  • #[Inject] 注解注入:在属性上使用注解,适合控制器、服务等由容器管理的类。
  • ApplicationContext 手动获取:在非容器管理的场景(如闭包)中,通过 ApplicationContext::getContainer()->get(SomeClass::class) 获取。

例如,在控制器中注入 ArticleModelValidatorFactoryInterface 都是通过 #[Inject] 实现。

2. 注解路由

与周四的闭包路由不同,注解路由直接写在控制器类和方法上,更贴近代码,便于维护。

  • #[Controller(prefix: '/articles')]:标记控制器,prefix 为所有方法路由的前缀。
  • #[RequestMapping(path: '', methods: 'get')]:标记方法,path 相对于控制器前缀,methods 为 HTTP 方法(GET、POST、PUT、DELETE 等)。

框架启动时扫描所有注解,自动注册路由。这种方式的优点是路由与控制器紧密绑定,减少配置文件修改。

3. Eloquent ORM

Hyperf 的 hyperf/database 基于 Illuminate Database,提供 Eloquent ORM。我们可以创建模型类,继承 Hyperf\DbConnection\Model\Model,定义表名、可填充字段等,然后像操作对象一样操作数据库。

关键点

  • 模型默认表名为类名的蛇形复数(如 Articlearticles),可通过 $table 属性指定。
  • 使用 $fillable 定义允许批量赋值的字段。
  • 通过 Article::create()Article::find()Article::where() 等方法进行数据库操作。

三、实战:创建文章 CRUD 接口(约 2.5 小时)

步骤 1:配置数据库连接

编辑 config/autoload/databases.php(如果已存在,检查配置):

<?php
return [
    'default' => [
        'driver' => env('DB_DRIVER', 'mysql'),
        'host' => env('DB_HOST', 'mysql'),
        'port' => env('DB_PORT', 3306),
        'database' => env('DB_DATABASE', 'hyperf_blog'),
        'username' => env('DB_USERNAME', 'blog_user'),
        'password' => env('DB_PASSWORD', 'blog_pass'),
        'charset' => 'utf8mb4',
        'collation' => 'utf8mb4_unicode_ci',
        'prefix' => '',
        'pool' => [
            'min_connections' => 1,
            'max_connections' => 10,
            'connect_timeout' => 10.0,
            'wait_timeout' => 3.0,
            'heartbeat' => -1,
            'max_idle_time' => 60,
        ],
    ],
];

确认 .env 中有对应配置。

步骤 2:创建模型

新建 app/Model/Article.php

<?php
namespace App\Model;

use Hyperf\DbConnection\Model\Model;

class Article extends Model
{
    protected $table = 'articles';
    protected $fillable = ['title', 'content'];
    public $timestamps = true; // 使用 created_at 和 updated_at
}
步骤 3:创建控制器

新建 app/Controller/ArticleController.php

<?php
namespace App\Controller;

use App\Model\Article;
use Hyperf\HttpServer\Annotation\Controller;
use Hyperf\HttpServer\Annotation\RequestMapping;
use Hyperf\Di\Annotation\Inject;
use Hyperf\Validation\Contract\ValidatorFactoryInterface;
use Hyperf\Validation\ValidationException;

#[Controller(prefix: '/articles')]
class ArticleController extends AbstractController
{
    #[Inject]
    protected ValidatorFactoryInterface $validatorFactory;

    // 列表
    #[RequestMapping(path: '', methods: 'get')]
    public function index()
    {
        $articles = Article::orderBy('id', 'desc')->paginate(10);
        return $articles->toArray();
    }

    // 详情
    #[RequestMapping(path: '{id:\d+}', methods: 'get')]
    public function show(int $id)
    {
        $article = Article::find($id);
        if (!$article) {
            return $this->response->json(['code' => 404, 'message' => '文章不存在'])->withStatus(404);
        }
        return $article->toArray();
    }

    // 创建
    #[RequestMapping(path: '', methods: 'post')]
    public function store()
    {
        $data = $this->request->all();
        $validator = $this->validatorFactory->make($data, [
            'title' => 'required|string|max:255',
            'content' => 'required|string',
        ], [
            'title.required' => '文章标题必填',
            'content.required' => '文章内容必填',
        ]);
        if ($validator->fails()) {
            throw new ValidationException($validator);
        }

        $article = Article::create($validator->validated());
        return $this->response->json(['code' => 201, 'message' => '创建成功', 'data' => $article])->withStatus(201);
    }

    // 更新
    #[RequestMapping(path: '{id:\d+}', methods: 'put')]
    public function update(int $id)
    {
        $article = Article::find($id);
        if (!$article) {
            return $this->response->json(['code' => 404, 'message' => '文章不存在'])->withStatus(404);
        }
        $data = $this->request->all();
        $validator = $this->validatorFactory->make($data, [
            'title' => 'sometimes|required|string|max:255',
            'content' => 'sometimes|required|string',
        ]);
        if ($validator->fails()) {
            throw new ValidationException($validator);
        }
        $article->update($validator->validated());
        return ['code' => 200, 'message' => '更新成功', 'data' => $article];
    }

    // 删除
    #[RequestMapping(path: '{id:\d+}', methods: 'delete')]
    public function destroy(int $id)
    {
        $article = Article::find($id);
        if (!$article) {
            return $this->response->json(['code' => 404, 'message' => '文章不存在'])->withStatus(404);
        }
        $article->delete();
        return $this->response->json(['code' => 204, 'message' => '删除成功'])->withStatus(204);
    }
}

注意

  • 控制器继承 AbstractController,可以直接使用 $this->request$this->response
  • 验证器工厂通过 #[Inject] 注入,无需手动实例化。
  • paginate() 需要 hyperf/paginator 组件,如果未安装请执行 composer require hyperf/paginator
  • 如果验证失败,主动抛出 ValidationException。该异常目前还没有专门的处理器,会走全局异常处理,但会返回不太友好的调试信息。我们将在下周一学习异常处理器,今天可以先略过或简单捕获。
步骤 4:测试接口

重启 Hyperf 服务(确保注解路由被扫描):

php bin/hyperf.php start

使用 curl 测试:

# 创建文章
curl -X POST http://localhost:9501/articles \
  -H "Content-Type: application/json" \
  -d '{"title":"Hello Hyperf","content":"这是第一篇文章"}'

# 获取列表
curl http://localhost:9501/articles

# 获取详情
curl http://localhost:9501/articles/1

# 更新
curl -X PUT http://localhost:9501/articles/1 \
  -H "Content-Type: application/json" \
  -d '{"title":"Updated Title"}'

# 删除
curl -X DELETE http://localhost:9501/articles/1

验证数据库中的数据变化。


四、成果测试与检验(约 1 小时)

测试清单
检验项方法通过标准
依赖注入正常接口能正常创建文章,验证器工厂被成功注入验证失败时返回 422(如缺少字段)
注解路由生效访问 /articles 各 HTTP 方法返回预期 JSON,无 404
模型映射正确查询数据库 articles 表,数据与请求一致字段正确填充
分页功能列表接口 ?page=2返回分页结构,包含 data, current_page, total
参数校验发送缺少 title 的 POST返回验证错误信息(目前可能是 500,需等下周一优化)
数据库连接池高并发请求列表接口无连接错误,延迟稳定
常见问题
  • 路由未生效:检查控制器类是否在 app/Controller 目录下(默认扫描),注解是否正确,以及服务重启。
  • 验证失败导致 500:暂时未配置验证异常处理器,可在控制器内 catch 或直接返回,或者接受这个临时问题,因为下周一我们会专门解决异常处理。
  • 数据库连接错误:检查 .envdatabases.php 配置,确保容器内可访问 MySQL 服务(主机名 mysql)。
调试技巧

在控制器方法中临时添加 var_dump($article); exit; 查看对象属性,验证模型工作正常。


五、今日作业与学习产出

  1. 提交代码:将 Article 模型、ArticleController、数据库配置等提交到 Git。
  2. 完善 CRUD
    • 为文章增加 author_id 字段(可先不关联用户,仅存储 ID),并在创建时设置。
    • 实现软删除(使用 SoftDeletes trait),并测试删除后数据仍可查询。
  3. 学习笔记
    • 绘制 Hyperf 请求生命周期图,标注 DI 容器、注解路由、控制器、模型的位置。
    • 对比注解路由和闭包路由的优劣。
  4. 挑战任务
    • 使用模型事件created, updated)在文章创建后自动写入日志。
    • 尝试使用查询构造器Db::table)替代 Eloquent 完成相同功能,体会两者差异。

通过今天的学习,你已经掌握了 Hyperf 中最核心的开发三件套:依赖注入、注解路由和数据库 ORM。下周我们将继续深入,学习验证器和异常处理,让你的 API 更加健壮和专业。

更多推荐