自分の質問に答えるのに一苦労すると思いました。以下は、私の元の質問の問題1〜3を解決する1つの方法にすぎません。
免責事項:パターンや手法を説明するときに、常に正しい用語を使用しているとは限りません。そのために残念。
目標:
- 表示および編集するための基本的なコントローラーの完全な例を作成します
Users
。
- すべてのコードは完全にテスト可能でモック可能でなければなりません。
- コントローラーは、データがどこに保存されているかを認識してはなりません(つまり、データを変更できることを意味します)。
- SQL実装を示す例(最も一般的)。
- 最大のパフォーマンスを得るには、コントローラーは必要なデータのみを受信し、追加のフィールドは受信しないでください。
- 実装では、開発を容易にするために、あるタイプのデータマッパーを活用する必要があります。
- 実装には、複雑なデータ検索を実行する機能が必要です。
ソリューション
永続ストレージ(データベース)の相互作用をR(読み取り)とCUD(作成、更新、削除)の2つのカテゴリーに分割しています。私の経験では、読み取りが実際にアプリケーションの速度を低下させる原因になっています。また、データ操作(CUD)は実際には低速ですが、発生する頻度がはるかに少ないため、心配する必要はほとんどありません。
CUD(作成、更新、削除)は簡単です。これには、実際のモデルでの作業が含まれ、Repositories
永続化のためにmy に渡されます。私のリポジトリはまだReadメソッドを提供しますが、オブジェクトの作成のためだけで、表示はしません。詳細は後ほど。
R(読み取り)はそれほど簡単ではありません。ここにはモデルはなく、値オブジェクトのみです。必要に応じて配列を使用してください。これらのオブジェクトは、実際には、単一のモデルまたは多くのモデルのブレンドを表す場合があります。これら自体はそれほど興味深いものではありませんが、どのように生成されるかです。私は私が呼んでいるものを使用していQuery Objects
ます。
コード:
ユーザーモデル
基本的なユーザーモデルから簡単に始めましょう。ORM拡張やデータベースに関することはまったくないことに注意してください。純粋なモデルの栄光。ゲッター、セッター、検証などを追加します。
class User
{
public $id;
public $first_name;
public $last_name;
public $gender;
public $email;
public $password;
}
リポジトリインターフェース
ユーザーリポジトリを作成する前に、リポジトリインターフェイスを作成します。これは、私のコントローラーがリポジトリを使用するために必要な「契約」を定義します。覚えておいてください、私のコントローラーはデータが実際にどこに保存されているかを知りません。
私のリポジトリには、これら3つのメソッドのみが含まれることに注意してください。この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
またはAllActiveUsersQuery
、またはがありMostCommonUserFirstNames
ます。
「リポジトリにメソッドを作成して、クエリを実行できないのではないか」とお考えかもしれません。はい、しかしここに私がこれをしない理由があります:
- 私のリポジトリは、モデルオブジェクトを操作するためのものです。実世界のアプリで、
password
すべてのユーザーをリストする場合、なぜフィールドを取得する必要があるのですか?
- 多くの場合、リポジトリはモデル固有ですが、クエリには複数のモデルが含まれることがよくあります。では、どのリポジトリにメソッドを配置しますか?
- これにより、リポジトリが非常にシンプルになります。メソッドの肥大化したクラスではありません。
- すべてのクエリが独自のクラスに編成されました。
- 本当に、この時点で、リポジトリは単に私のデータベース層を抽象化するために存在しています。
私の例では、 "AllUsers"を検索するクエリオブジェクトを作成します。これがインターフェースです:
interface AllUsersQueryInterface
{
public function fetch($fields);
}
クエリオブジェクトの実装
ここで、データマッパーを再び使用して、開発のスピードを上げることができます。返されたデータセット(フィールド)を1回微調整できることに注意してください。これは、実行したクエリを操作することについてです。私のクエリオブジェクトはクエリビルダーではありません。特定のクエリを実行するだけです。ただし、これを頻繁に使用することがわかっているため、さまざまな状況で、フィールドを指定する機能を自分に与えています。不要なフィールドを返したくない!
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...";
}
}
これにより、このレポートのすべてのロジックが1つのクラスにうまく収められ、テストも簡単です。私はそれを心のこもった内容に模倣することができます、または完全に別の実装を使用することさえできます。
コントローラー
楽しい部分です。すべての要素を1つにまとめます。依存関係注入を使用していることに注意してください。通常、依存関係はコンストラクターに注入されますが、実際には依存関係をコントローラーメソッド(ルート)に直接注入することを好みます。これにより、コントローラーのオブジェクトグラフが最小化され、実際に見やすくなりました。このアプローチが気に入らない場合は、従来のコンストラクターメソッドを使用してください。
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を使用するため、ほとんどの場合、アプリケーション全体でこれらのモデルを直接参照します。ただし、より複雑なクエリがある場合は、クエリオブジェクトを使用して、クエリをより再利用できるようにします。また、モデルを常にメソッドに挿入しているため、テストでモックしやすくなっています。