我想我应该试着回答我自己的问题.以下只是解决我原问题中1-3个问题的一种方法.
Disclaimer: I may not always use the right terms when describing patterns or techniques. Sorry for that.
目标是:
- 创建一个用于查看和编辑
Users
的基本控制器的完整示例.
- 所有代码必须是完全可测试和可模拟的.
- 控制器应该不知道数据存储在哪里(意味着数据可以更改).
- 显示SQL实现的示例(最常见).
- 为了获得最高性能,控制器应该只接收它们需要的数据,而不是额外的字段.
- 实现应该利用某种类型的数据映射器以便于开发.
- 实现应该能够执行复杂的数据查找.
解决方案
我将持久性存储(数据库)交互分为两类:R(读取)和CUD(创建、更新、删除).我的经验是,阅读确实是导致应用程序速度变慢的原因.虽然数据操作(CUD)实际上较慢,但它发生的频率要低得多,因此也就不是什么大问题了.
CUD(创建、更新、删除)很简单.这将涉及使用实际的models,然后将其传递给我的Repositories
进行持久性.注意,我的存储库仍将提供读取方法,但只是用于对象创建,而不是显示.稍后再谈.
R(读)不是那么容易的.这里没有模特,只有value objects个.使用数组if you prefer.这些对象可以表示单个模型,也可以表示多个模型的混合,实际上是任何东西.这些本身并不是很有趣,但是它们是如何生成的.我用的是我所谓的Query Objects
.
代码:
用户模型
让我们从简单的基本用户模型开始.请注意,根本没有ORM扩展或数据库内容.只是纯粹的模特荣耀.添加您的getter、setter、验证等等.
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数据库.请注意,使用data mapper可以避免编写重复的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(读取).查询对象只是某种类型的数据查找逻辑的封装.他们是not个查询生成器.通过将其抽象为我们的存储库,我们可以改变它的实现并更容易地进行测试.查询对象的示例可能是AllUsersQuery
或AllActiveUsersQuery
,甚至MostCommonUserFirstNames
.
你可能会想,"我不能在我的存储库中为这些查询创建方法吗?"是的,但我不这么做的原因如下:
- 我的存储库是用来处理模型对象的.在现实世界的应用程序中,如果我想列出所有用户,为什么我需要获得
password
字段?
- 存储库通常是特定于模型的,但查询通常涉及多个模型.那么,你把你的方法放在什么存储库中呢?
- 这使我的存储库非常简单-而不是臃肿的方法类.
- 所有查询现在都被组织到自己的类中.
- 实际上,在这一点上,存储库的存在只是为了抽象我的数据库层.
对于我的示例,我将创建一个Query对象来查找"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;
}
}
最后的 idea :
这里需要注意的重要事项是,当我修改(创建、更新或删除)实体时,我使用的是真实的模型对象,并通过我的存储库执行持久化.
但是,当我显示( Select 数据并将其发送到视图)时,我使用的不是模型对象,而是普通的旧值对象.我只 Select 我需要的字段,它的设计使我可以最大限度地提高数据查找性能.
我的存储库保持非常干净,相反,这种"混乱"被组织到我的模型查询中.
我使用数据映射器来帮助开发,因为为常见任务编写重复的SQL是非常荒谬的.但是,您绝对可以在需要的地方编写SQL(复杂的查询、报告等).当您这样做时,它会很好地隐藏在一个命名正确的类中.
我很想听听你对我的方法的看法!
July 2015 Update:
我在 comments 中被问到,我在哪里结束了这一切.其实没那么远.说实话,我还是不太喜欢仓库.我发现它们对于基本的查找(尤其是如果你已经在使用ORM的话)来说过于繁琐,而在处理更复杂的查询时,它们又很混乱.
我通常使用ActiveRecord样式的ORM,所以大多数情况下,我只会在整个应用程序中直接引用这些模型.但是,在有更复杂查询的情况下,我将使用查询对象使这些查询更具可重用性.我还应该注意到,我总是将模型注入到方法中,使它们更容易在测试中模拟.