我想我会尽力回答我自己的问题。以下只是解决我最初问题中问题 1-3 的一种方法。
免责声明:在描述模式或技术时,我可能并不总是使用正确的术语。对此感到抱歉。
目标:
- 创建一个用于查看和编辑的基本控制器的完整示例
Users
。
- 所有代码都必须是完全可测试和可模拟的。
- 控制器应该不知道数据存储在哪里(意味着可以更改)。
- 显示 SQL 实现的示例(最常见)。
- 为了获得最佳性能,控制器应该只接收他们需要的数据——没有额外的字段。
- 实现应该利用某种类型的数据映射器来简化开发。
- 实现应该具有执行复杂数据查找的能力。
解决方案
我将我的持久存储(数据库)交互分为两类:R(读取)和CUD(创建、更新、删除)。我的经验是读取确实是导致应用程序变慢的原因。尽管数据操作 (CUD) 实际上速度较慢,但它发生的频率要低得多,因此也不那么令人担忧。
CUD(创建、更新、删除)很容易。这将涉及使用实际模型,然后将其传递给我Repositories
的持久性。请注意,我的存储库仍将提供 Read 方法,但仅用于创建对象,而不是显示。稍后再谈。
R(读)不是那么容易。这里没有模型,只有值对象。如果您愿意,请使用数组。这些对象可能代表单个模型或多个模型的混合,实际上是任何东西。这些本身并不是很有趣,但是它们是如何生成的。我正在使用我正在调用的内容Query Objects
。
编码:
用户模型
让我们从我们的基本用户模型开始。请注意,根本没有 ORM 扩展或数据库的东西。只是纯粹的模特荣耀。添加你的 getter、setter、validation 等等。
class User
{
public $id;
public $first_name;
public $last_name;
public $gender;
public $email;
public $password;
}
存储库接口
在我创建我的用户存储库之前,我想创建我的存储库界面。这将定义存储库必须遵循的“合同”才能被我的控制器使用。请记住,我的控制器不会知道数据实际存储在哪里。
请注意,我的存储库将只包含这三种方法。该save()
方法负责创建和更新用户,仅取决于用户对象是否设置了 id。
interface UserRepositoryInterface
{
public function find($id);
public function save(User $user);
public function remove(User $user);
}
SQL 存储库实现
现在创建我的接口实现。如前所述,我的示例将使用 SQL 数据库。请注意使用数据映射器来避免编写重复的 SQL 查询。
class SQLUserRepository implements UserRepositoryInterface
{
protected $db;
public function __construct(Database $db)
{
$this->db = $db;
}
public function find($id)
{
// Find a record with the id = $id
// from the 'users' table
// and return it as a User object
return $this->db->find($id, 'users', 'User');
}
public function save(User $user)
{
// Insert or update the $user
// in the 'users' table
$this->db->save($user, 'users');
}
public function remove(User $user)
{
// Remove the $user
// from the 'users' table
$this->db->remove($user, 'users');
}
}
查询对象接口
现在有了我们的存储库处理的CUD(创建、更新、删除),我们可以专注于R(读取)。查询对象只是某种类型的数据查找逻辑的封装。他们不是查询构建器。通过像我们的存储库一样抽象它,我们可以更改它的实现并更容易地对其进行测试。查询对象的一个示例可能是AllUsersQuery
or AllActiveUsersQuery
,甚至是MostCommonUserFirstNames
。
您可能在想“我不能在我的存储库中为这些查询创建方法吗?” 是的,但这就是我不这样做的原因:
- 我的存储库用于处理模型对象。
password
在现实世界的应用程序中,如果我想列出所有用户,为什么还需要获取该字段?
- 存储库通常是特定于模型的,但查询通常涉及多个模型。那么你将你的方法放在哪个存储库中?
- 这使我的存储库非常简单——而不是臃肿的方法类。
- 所有查询现在都组织到它们自己的类中。
- 实际上,在这一点上,存储库的存在只是为了抽象我的数据库层。
对于我的示例,我将创建一个查询对象来查找“AllUsers”。这是界面:
interface AllUsersQueryInterface
{
public function fetch($fields);
}
查询对象实现
这是我们可以再次使用数据映射器来帮助加快开发的地方。请注意,我允许对返回的数据集(字段)进行一次调整。这大约是我想要操纵执行的查询的程度。请记住,我的查询对象不是查询生成器。他们只是执行特定的查询。但是,由于我知道我可能会在许多不同的情况下经常使用这个,所以我让自己能够指定字段。我从不想返回我不需要的字段!
class AllUsersQuery implements AllUsersQueryInterface
{
protected $db;
public function __construct(Database $db)
{
$this->db = $db;
}
public function fetch($fields)
{
return $this->db->select($fields)->from('users')->orderBy('last_name, first_name')->rows();
}
}
在继续讨论控制器之前,我想展示另一个示例来说明它的强大功能。也许我有一个报告引擎,需要为AllOverdueAccounts
. 这对我的数据映射器来说可能很棘手,我可能想SQL
在这种情况下写一些实际的东西。没问题,这是这个查询对象的样子:
class AllOverdueAccountsQuery implements AllOverdueAccountsQueryInterface
{
protected $db;
public function __construct(Database $db)
{
$this->db = $db;
}
public function fetch()
{
return $this->db->query($this->sql())->rows();
}
public function sql()
{
return "SELECT...";
}
}
这很好地将我对这个报告的所有逻辑都集中在一个类中,并且很容易测试。我可以尽情地模拟它,甚至完全使用不同的实现。
控制器
现在是有趣的部分——把所有的部分放在一起。请注意,我正在使用依赖注入。通常依赖项被注入到构造函数中,但我实际上更喜欢将它们直接注入到我的控制器方法(路由)中。这最小化了控制器的对象图,实际上我发现它更清晰。请注意,如果您不喜欢这种方法,只需使用传统的构造方法即可。
class UsersController
{
public function index(AllUsersQueryInterface $query)
{
// Fetch user data
$users = $query->fetch(['first_name', 'last_name', 'email']);
// Return view
return Response::view('all_users.php', ['users' => $users]);
}
public function add()
{
return Response::view('add_user.php');
}
public function insert(UserRepositoryInterface $repository)
{
// Create new user model
$user = new User;
$user->first_name = $_POST['first_name'];
$user->last_name = $_POST['last_name'];
$user->gender = $_POST['gender'];
$user->email = $_POST['email'];
// Save the new user
$repository->save($user);
// Return the id
return Response::json(['id' => $user->id]);
}
public function view(SpecificUserQueryInterface $query, $id)
{
// Load user data
if (!$user = $query->fetch($id, ['first_name', 'last_name', 'gender', 'email'])) {
return Response::notFound();
}
// Return view
return Response::view('view_user.php', ['user' => $user]);
}
public function edit(SpecificUserQueryInterface $query, $id)
{
// Load user data
if (!$user = $query->fetch($id, ['first_name', 'last_name', 'gender', 'email'])) {
return Response::notFound();
}
// Return view
return Response::view('edit_user.php', ['user' => $user]);
}
public function update(UserRepositoryInterface $repository)
{
// Load user model
if (!$user = $repository->find($id)) {
return Response::notFound();
}
// Update the user
$user->first_name = $_POST['first_name'];
$user->last_name = $_POST['last_name'];
$user->gender = $_POST['gender'];
$user->email = $_POST['email'];
// Save the user
$repository->save($user);
// Return success
return true;
}
public function delete(UserRepositoryInterface $repository)
{
// Load user model
if (!$user = $repository->find($id)) {
return Response::notFound();
}
// Delete the user
$repository->delete($user);
// Return success
return true;
}
}
最后的想法:
这里要注意的重要事项是,当我修改(创建、更新或删除)实体时,我正在使用真实的模型对象,并通过我的存储库执行持久性。
但是,当我显示(选择数据并将其发送到视图)时,我使用的不是模型对象,而是普通的旧值对象。我只选择我需要的字段,它的设计是为了最大限度地提高我的数据查找性能。
我的存储库保持非常干净,相反,这个“混乱”被组织到我的模型查询中。
我使用数据映射器来帮助开发,因为为常见任务编写重复的 SQL 是很荒谬的。但是,您绝对可以在需要的地方(复杂的查询、报告等)编写 SQL。当你这样做时,它很好地隐藏在一个正确命名的类中。
我很想听听你对我的方法的看法!
2015 年 7 月更新:
我在评论中被问到我在哪里结束了这一切。嗯,其实也没有那么远。老实说,我仍然不太喜欢存储库。我发现它们对于基本查找来说太过分了(特别是如果你已经在使用 ORM),并且在处理更复杂的查询时会很混乱。
我通常使用 ActiveRecord 样式的 ORM,所以通常我会在我的应用程序中直接引用这些模型。但是,在我有更复杂的查询的情况下,我将使用查询对象来使这些更可重用。我还应该注意,我总是将我的模型注入我的方法中,使它们更容易在我的测试中模拟。