私は MVC フレームワークを理解し始めたばかりで、モデルにどの程度のコードを入れるべきかよく考えます。私は次のようなメソッドを持つデータ アクセス クラスを作成する傾向があります。
public function CheckUsername($connection, $username)
{
try
{
$data = array();
$data['Username'] = $username;
//// SQL
$sql = "SELECT Username FROM" . $this->usersTableName . " WHERE Username = :Username";
//// Execute statement
return $this->ExecuteObject($connection, $sql, $data);
}
catch(Exception $e)
{
throw $e;
}
}
私のモデルは、データベース テーブルにマップされるエンティティ クラスになる傾向があります。
モデル オブジェクトには、上記のコードだけでなく、データベースにマップされたすべてのプロパティが必要ですか。それとも、実際にデータベースを操作するコードを分離しても問題ありませんか。
最終的には4層になるのでしょうか?
ベストアンサー1
免責事項:以下は、PHP ベースの Web アプリケーションのコンテキストで MVC のようなパターンを私がどのように理解しているかを説明したものです。コンテンツで使用されているすべての外部リンクは、用語と概念を説明するために存在しており、この主題に関する私の信頼性を暗示するものではありません。
最初に明確にしておきたいのは、モデルはレイヤーであるということです。
2 つ目:従来の MVCと Web 開発で使用するものには違いがあります。こちらは私が書いた少し古い回答で、それらの違いを簡単に説明しています。
モデルが何ではないか:
モデルはクラスでも単一のオブジェクトでもありません。これはよくある間違いです(私もやりました。ただし、元の回答は別の方法を学び始めたときに書かれました)。ほとんどのフレームワークがこの誤解を永続させているからです。
これは、オブジェクト リレーショナル マッピング技術 (ORM) でも、データベース テーブルの抽象化でもありません。そうでないと主張する人は、おそらく、別の新しい ORM またはフレームワーク全体を「販売」しようとしているのでしょう。
モデルとは何か:
適切な MVC 適応では、M にはすべてのドメイン ビジネス ロジックが含まれ、モデル レイヤーは主に次の 3 種類の構造で構成されます。
-
ドメイン オブジェクトは、純粋にドメイン情報の論理コンテナーです。通常は、問題ドメイン空間内の論理エンティティを表します。一般に、ビジネス ロジックと呼ばれます。
ここでは、請求書を送信する前にデータを検証する方法や、注文の合計金額を計算する方法を定義します。同時に、ドメイン オブジェクトはストレージについてまったく認識しません。つまり、ストレージがどこから(SQL データベース、REST API、テキスト ファイルなど)保存されたか、取得されたかさえも認識しません。
-
これらのオブジェクトはストレージのみを担当します。データベースに情報を保存する場合、SQL はここに存在します。または、XML ファイルを使用してデータを保存し、データ マッパーがXML ファイルとの間で解析を行っている場合もあります。
-
これらは「高レベルのドメイン オブジェクト」と考えることができますが、ビジネス ロジックの代わりに、サービスはドメイン オブジェクトとマッパーの間のやり取りを担当します。これらの構造により、ドメイン ビジネス ロジックとやり取りするための「パブリック」インターフェイスが作成されます。これらを回避することはできますが、一部のドメイン ロジックがコントローラーに漏れるというペナルティがあります。
このテーマに関連する回答はACL 実装質問 - 役に立つかもしれません。
モデル層と MVC トライアドの他の部分との間の通信は、サービスを通じてのみ行われる必要があります。明確に分離することで、次のようないくつかの利点が得られます。
- それは、単一責任原則(希望小売価格)
- ロジックが変更になった場合に備えて、追加の「余裕」を提供する
- コントローラーを可能な限りシンプルに保つ
- 外部APIが必要な場合に明確な設計図を提供します
モデルと対話するにはどうすればいいですか?
前提条件:講義を視聴する「グローバル状態とシングルトン」そして「物を探さないで!」Clean Code Talks より。
サービスインスタンスへのアクセス
ViewインスタンスとControllerインスタンス (いわゆる「UI レイヤー」)の両方がこれらのサービスにアクセスできるようにするには、次の 2 つの一般的な方法があります。
- DI コンテナを使用すると、ビューとコントローラのコンストラクターに必要なサービスを直接挿入できます。
- すべてのビューとコントローラーの必須依存関係として、サービスのファクトリを使用します。
ご想像のとおり、DIコンテナははるかに洗練されたソリューションです(初心者にとって最も簡単ではありませんが)。この機能のために検討することをお勧めする2つのライブラリは、Syfmonyのスタンドアロンです。DependencyInjection コンポーネントまたはオーリン。
ファクトリと DI コンテナを使用する両方のソリューションでは、特定の要求応答サイクルで、選択したコントローラとビュー間で共有されるさまざまなサーバーのインスタンスを共有することもできます。
モデルの状態の変更
コントローラーのモデル レイヤーにアクセスできるようになったので、実際に使用を開始する必要があります。
public function postLogin(Request $request)
{
$email = $request->get('email');
$identity = $this->identification->findIdentityByEmailAddress($email);
$this->identification->loginWithPassword(
$identity,
$request->get('password')
);
}
コントローラーのタスクは非常に明確です。ユーザー入力を取得し、この入力に基づいてビジネス ロジックの現在の状態を変更します。この例では、変更される状態は「匿名ユーザー」と「ログインしたユーザー」です。
コントローラは、ユーザーの入力を検証する責任はありません。これはビジネスルールの一部であり、コントローラはSQLクエリを呼び出すことはありません。ここまたはここ(彼らを憎まないでください。彼らは間違った方向に進んでいるだけで、悪人ではありません)。
ユーザーに状態の変化を表示します。
ユーザーはログインしました(または失敗しました)。それで?当該ユーザーはまだそれに気づいていません。したがって、実際に応答を生成する必要があり、それがビューの役割です。
public function postLogin()
{
$path = '/login';
if ($this->identification->isUserLoggedIn()) {
$path = '/dashboard';
}
return new RedirectResponse($path);
}
この場合、ビューはモデル レイヤーの現在の状態に基づいて、2 つの可能な応答のうちの 1 つを生成しました。別のユース ケースでは、ビューは「現在選択されている記事」などに基づいて、レンダリングする異なるテンプレートを選択します。
プレゼンテーション層は、次のように説明されているように、実際にはかなり複雑になることがあります。PHP での MVC ビューの理解。
しかし、私は REST API を作成しているだけです。
もちろん、これがやり過ぎである状況もあります。
MVCは単なる具体的な解決策であり、関心事の分離原則。MVCはユーザー インターフェイスをビジネス ロジックから分離し、UI ではユーザー入力の処理とプレゼンテーションを分離します。これは非常に重要です。よく「トライアド」と表現されますが、実際には 3 つの独立した部分から構成されているわけではありません。構造は次のようになります。
つまり、プレゼンテーション層のロジックがほとんど存在しない場合、それらを単一の層として保持するのが実用的なアプローチです。これにより、モデル層のいくつかの側面を大幅に簡素化することもできます。
このアプローチを使用すると、ログイン例 (API 用) は次のように記述できます。
public function postLogin(Request $request)
{
$email = $request->get('email');
$data = [
'status' => 'ok',
];
try {
$identity = $this->identification->findIdentityByEmailAddress($email);
$token = $this->identification->loginWithPassword(
$identity,
$request->get('password')
);
} catch (FailedIdentification $exception) {
$data = [
'status' => 'error',
'message' => 'Login failed!',
]
}
return new JsonResponse($data);
}
これは持続可能ではありませんが、レスポンス本文をレンダリングするための複雑なロジックがある場合、この単純化はより単純なシナリオでは非常に役立ちます。ただし、このアプローチは、複雑なプレゼンテーション ロジックを持つ大規模なコードベースで使用しようとすると悪夢になることに注意してください。
モデルを構築するにはどうすればいいですか?
上で説明したように、単一の「モデル」クラスは存在しないため、実際には「モデルを構築する」ことはありません。代わりに、特定のメソッドを実行できるサービスを作成することから始めます。次に、ドメイン オブジェクトとマッパーを実装します。
サービスメソッドの例:
上記の両方のアプローチでは、識別サービス用のログイン方法がありました。これは実際にはどのように見えるでしょうか。私は、同じ機能のわずかに修正されたバージョンを使用しています。図書館、私が怠け者なので書いたものです:
public function loginWithPassword(Identity $identity, string $password): string
{
if ($identity->matchPassword($password) === false) {
$this->logWrongPasswordNotice($identity, [
'email' => $identity->getEmailAddress(),
'key' => $password, // this is the wrong password
]);
throw new PasswordMismatch;
}
$identity->setPassword($password);
$this->updateIdentityOnUse($identity);
$cookie = $this->createCookieIdentity($identity);
$this->logger->info('login successful', [
'input' => [
'email' => $identity->getEmailAddress(),
],
'user' => [
'account' => $identity->getAccountId(),
'identity' => $identity->getId(),
],
]);
return $cookie->getToken();
}
ご覧のとおり、この抽象化レベルでは、データがどこから取得されたかはわかりません。データベースである可能性もありますが、テスト目的の単なる模擬オブジェクトである可能性もあります。実際に使用されるデータ マッパーでさえ、private
このサービスのメソッド内に隠されています。
private function changeIdentityStatus(Entity\Identity $identity, int $status)
{
$identity->setStatus($status);
$identity->setLastUsed(time());
$mapper = $this->mapperFactory->create(Mapper\Identity::class);
$mapper->store($identity);
}
マッパーの作成方法
永続性の抽象化を実装するには、最も柔軟なアプローチはカスタムを作成することですデータマッパー。
から:インド太平洋地域本
実際には、これらは特定のクラスまたはスーパークラスとのやり取りのために実装されます。コードにCustomer
とがあるとします (両方ともスーパークラスから継承)。これらには異なるフィールドが含まれているため、おそらく両方とも別々の対応するマッパーを持つことになります。ただし、共有され、共通に使用される操作も発生します。たとえば、「最後にオンラインで見た」時間の更新などです。既存のマッパーをさらに複雑にする代わりに、より実用的な方法は、そのタイムスタンプのみを更新する一般的な「ユーザー マッパー」を使用することです。Admin
User
追加コメント:
データベーステーブルとモデル
データベース テーブル、ドメイン オブジェクト、およびMapperの間に直接的な 1:1:1 関係が存在する場合もありますが、大規模なプロジェクトでは予想よりも一般的ではない場合があります。
単一のドメイン オブジェクトによって使用される情報は、異なるテーブルからマップされる可能性がありますが、オブジェクト自体はデータベース内に永続的ではありません。
例:
MonthlyReport
月次レポートを生成する場合。これはさまざまなテーブルから情報を収集しますが、データベースには魔法のテーブルはありません。単一のMapper が複数のテーブルに影響を与える可能性があります。
例:オブジェクトからデータを保存する場合
User
、このドメイン オブジェクトには他のドメイン オブジェクト (Group
インスタンス) のコレクションが含まれる可能性があります。これらを変更して を保存する場合User
、データ マッパーは複数のテーブルでエントリを更新または挿入する必要があります。単一のドメイン オブジェクトのデータは複数のテーブルに保存されます。
例:大規模なシステム (中規模のソーシャル ネットワークなど) では、ユーザー認証データと頻繁にアクセスされるデータを、めったに必要とされない大きなコンテンツ チャンクとは別に保存するのが実用的かもしれません。その場合、
User
クラスは 1 つのままですが、そのクラスに含まれる情報は、完全な詳細が取得されたかどうかによって異なります。ドメインオブジェクトごとに複数のマッパーが存在する可能性がある
例:公開用と管理用ソフトウェアの両方でコードベースを共有するニュース サイトがあるとします。ただし、両方のインターフェイスで同じ
Article
クラスが使用される一方で、管理用にはさらに多くの情報を入力する必要があります。この場合、「内部」と「外部」の 2 つの別々のマッパーが必要になります。それぞれが異なるクエリを実行するか、異なるデータベース (マスターまたはスレーブなど) を使用します。
ビューはテンプレートではありません
MVC のビューインスタンス (パターンの MVP バリエーションを使用していない場合) は、プレゼンテーション ロジックを担当します。つまり、各ビューは通常、少なくともいくつかのテンプレートを扱います。モデル レイヤーからデータを取得し、受信した情報に基づいてテンプレートを選択し、値を設定します。
これによって得られる利点の 1 つは、再利用性です。
ListView
クラスを作成すれば、適切に記述されたコードを使用して、同じクラスで記事の下のユーザー リストとコメントの表示を処理できます。どちらも同じ表示ロジックを持っているためです。テンプレートを切り替えるだけです。どちらかを使用することができますネイティブ PHP テンプレートまたは、サードパーティのテンプレート エンジンを使用します。また、 Viewインスタンスを完全に置き換えることができるサードパーティのライブラリもいくつかある可能性があります。
古いバージョンの回答はどうですか?
唯一の大きな変更点は、古いバージョンでModelと呼ばれていたものが、実際にはServiceであることです。その他の「ライブラリのアナロジー」は、ほぼそのまま維持されています。
私が見つけた唯一の欠点は、これが本当に奇妙なライブラリになるということです。なぜなら、本から情報を返すものの、本自体に触れることはできないからです。そうしないと、抽象化が「漏れ」始めます。もっと適切な例えを考えなければならないかもしれません。
View インスタンスとControllerインスタンスの関係は何ですか?
MVC 構造は、UI とモデルの 2 つのレイヤーで構成されます。UIレイヤーの主な構造は、ビューとコントローラーです。
MVC デザイン パターンを使用する Web サイトを扱う場合、ビューとコントローラーを 1:1 の関係にするのが最善の方法です。各ビューは Web サイトのページ全体を表し、その特定のビューに対するすべての受信リクエストを処理する専用のコントローラーがあります。
たとえば、開かれた記事を表すには、
\Application\Controller\Document
とを使用します\Application\View\Document
。これには、記事を扱う際のUIレイヤーの主な機能がすべて含まれます(もちろん、HR の記事に直接関係のないコンポーネント。