
本文介绍一种符合单一职责原则的优雅方案:通过为每张数据表创建独立的 repository 类,并在上层 service 中组合使用,避免重复代码,同时保持可测试性与可维护性。
本文介绍一种符合单一职责原则的优雅方案:通过为每张数据表创建独立的 repository 类,并在上层 service 中组合使用,避免重复代码,同时保持可测试性与可维护性。
在现代 PHP 应用(尤其是遵循分层架构的项目)中,Repository 模式常用于抽象数据访问逻辑。但初学者容易陷入一个误区:试图让一个 Repository 同时管理多张表(如 tbl_users 和 tbl_locations),结果导致 getTablename() 等方法逻辑混乱、条件分支增多,违背了“一个类只做一件事”的设计原则。
✅ 正确做法是:一表一 Repository。
每个 Repository 专注封装一张表的 CRUD 操作,复用抽象基类提供的通用能力(如 readAll()、findById() 等),而具体表名与模型映射则由子类自行定义。
以下是一个清晰、可扩展的实现示例:
// 抽象基类 —— 提供通用查询能力
abstract class AbstractRepository
{
protected PDO $pdo;
public function __construct(PDO $pdo)
{
$this->pdo = $pdo;
}
abstract public function getTablename(): string;
abstract public function getModel(): string;
public function readAll(): array
{
$table = $this->getTablename();
$model = $this->getModel();
$stmt = $this->pdo->query("SELECT * FROM `$table`");
return $stmt->fetchAll(PDO::FETCH_CLASS, $model);
}
public function findById(int $id): ?object
{
$table = $this->getTablename();
$model = $this->getModel();
$stmt = $this->pdo->prepare("SELECT * FROM `$table` WHERE id = ?");
$stmt->execute([$id]);
return $stmt->fetchObject($model);
}
}
// 用户专属 Repository
class UserRepository extends AbstractRepository implements UserRepositoryInterface
{
public function getTablename(): string
{
return 'tbl_users';
}
public function getModel(): string
{
return 'administration\CMR\UserModel';
}
}
// 地点专属 Repository
class LocationRepository extends AbstractRepository implements LocationRepositoryInterface
{
public function getTablename(): string
{
return 'tbl_locations';
}
public function getModel(): string
{
return 'administration\CMR\LocationModel';
}
}
当业务逻辑需要跨表协作(例如“获取某用户所在的所有地点”),绝不应在 Repository 内部硬编码 JOIN 或跨表查询,而应交由更高层级的 Service 协调:
class UserLocationService implements UserLocationServiceInterface
{
public function __construct(
private UserRepositoryInterface $userRepository,
private LocationRepositoryInterface $locationRepository
) {}
// 示例:获取用户及其关联地点(可按需组合查询)
public function getUserWithLocations(int $userId): array
{
$user = $this->userRepository->findById($userId);
if (!$user) {
throw new InvalidArgumentException("User not found: $userId");
}
// 假设存在外键关系:locations.user_id = users.id
$stmt = $this->locationRepository->getPdo()->prepare(
"SELECT * FROM tbl_locations WHERE user_id = ?"
);
$stmt->execute([$userId]);
$locations = $stmt->fetchAll(PDO::FETCH_CLASS, $this->locationRepository->getModel());
return [
'user' => $user,
'locations' => $locations,
];
}
}
? 关键设计要点总结:
- ✅ 职责分离:Repository 只负责单表数据映射;Service 负责业务编排;Controller 仅协调请求与响应。
- ✅ 类型安全与可测性:每个 Repository 接口(如 UserRepositoryInterface)明确契约,便于 Mock 测试。
- ⚠️ 避免反模式:不要在 AbstractRepository 中添加 setTable() 方法或运行时切换表名——这会破坏对象不变性,增加调试难度。
- ? 安全性提醒:示例中使用反引号包裹表名(`$table`)仅适用于静态、可信的表名;若表名来自配置,请确保严格校验,切勿拼接用户输入。
这种结构天然支持未来扩展:新增 tbl_orders?只需添加 OrderRepository 并注入到对应 Service 即可,零侵入、高内聚、低耦合。










