免責事項:以下は、PHPベースのWebアプリケーションのコンテキストでMVCのようなパターンを理解する方法の説明です。コンテンツで使用されているすべての外部リンクは、用語と概念を説明するためのものであり、主題に関する私自身の信頼性を意味するものではありません。
まず最初に明らかにしなければならないのは、モデルがレイヤーであることです。
第二に、従来のMVCとWeb開発で使用するものとの間には違いがあります。ここだ簡単には、彼らがどのように異なるかを説明し、私が書いた古い答えのビットは、。
どのモデルではないか:
モデルはクラスまたは単一のオブジェクトではありません。ほとんどのフレームワークがこの誤解を永続させるため、これはよくある間違いです(私もそうしましたが、元の答えは私が他の方法を学び始めたときに書かれました)。
オブジェクトリレーショナルマッピングテクニック(ORM)でも、データベーステーブルの抽象化でもありません。それ以外の場合は、別の真新しいORMまたはフレームワーク全体を「販売」しようとしている可能性があります。
モデルとは:
適切なMVC適応では、Mにはすべてのドメインビジネスロジックが含まれ、モデルレイヤーは主に次の 3種類の構造から構成されます。
ドメインオブジェクト
ドメインオブジェクトは、純粋なドメイン情報の論理コンテナです。これは通常、問題ドメイン空間の論理エンティティを表します。一般にビジネスロジックと呼ばれます。
ここで、請求書を送信する前にデータを検証する方法、または注文の総コストを計算する方法を定義します。同時に、ドメインオブジェクトは、ストレージの完全に気づいていない-どちらから場所(SQLデータベース、RESTのAPI、テキストファイルなど)もさえあれば、彼らが保存または取得されます。
データマッパー
これらのオブジェクトはストレージのみを担当します。データベースに情報を格納する場合、これはSQLが存在する場所です。または、XMLファイルを使用してデータを格納し、データマッパーがXMLファイルとの間で解析を行っている場合もあります。
サービス
それらは「上位レベルのドメインオブジェクト」と考えることができますが、ビジネスロジックの代わりに、サービスはドメインオブジェクトとマッパー間の相互作用を担当します。これらの構造は、ドメインビジネスロジックと対話するための「パブリック」インターフェースを作成することになります。それらを回避することはできますが、一部のドメインロジックがコントローラーにリークするというペナルティがあります。
ACLの実装に関する質問には、この主題に関連する回答があります。これは役立つ場合があります。
モデルレイヤーとMVCトライアドの他の部分との間の通信は、サービスを介してのみ行われる必要があります。明確な分離には、さらにいくつかの利点があります。
- 単一責任原則(SRP)の実施に役立ちます
- ロジックが変更された場合に備えて、追加の「ウィグルルーム」を提供します
- コントローラを可能な限りシンプルに保つ
- 外部APIが必要な場合に、明確な青写真を提供します
モデルと相互作用するには?
前提条件:講義「Global State and Singletons」および「Do n't Look For Things!」をご覧ください。Clean Code Talksから。
サービスインスタンスにアクセスする
ViewとControllerの両方のインスタンス(いわゆる「UIレイヤー」)がこれらのサービスにアクセスするには、2つの一般的なアプローチがあります。
- 必要なサービスは、できればDIコンテナーを使用して、ビューとコントローラーのコンストラクターに直接挿入できます。
- すべてのビューとコントローラーの必須の依存関係としてのサービスのファクトリーの使用。
ご想像のとおり、DIコンテナーははるかに洗練されたソリューションです(初心者には簡単ではありません)。この機能について検討することをお勧めする2つのライブラリは、SyfmonyのスタンドアロンのDependencyInjectionコンポーネントまたはAurynです。
ファクトリーと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つの可能な応答のいずれかを生成しました。別のユースケースでは、「現在選択されている記事」のようなものに基づいて、レンダリングするさまざまなテンプレートを選択するビューがあります。
ここで説明するように、プレゼンテーション層は実際にはかなり複雑になる可能性があります:PHPでのMVCビューの理解。
しかし、私はREST APIを作成しているだけです!
もちろん、これがやり過ぎである状況もあります。
MVCは、関心の分離の原則に対する具体的なソリューションにすぎません。MVCはユーザーインターフェイスをビジネスロジックから分離し、UIではユーザー入力の処理とプレゼンテーションを分離しました。これは非常に重要です。多くの場合、それは「トライアド」として説明されますが、実際には3つの独立した部分で構成されているわけではありません。構造は次のようになります。
つまり、プレゼンテーションレイヤーのロジックが存在しないものに近い場合、実用的なアプローチは、それらを1つのレイヤーとして保持することです。また、モデルレイヤーの一部の側面を大幅に簡略化できます。
このアプローチを使用すると、ログインの例(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);
}
これは持続可能ではありませんが、応答本文をレンダリングするための複雑なロジックがある場合、この単純化はより簡単なシナリオで非常に役立ちます。しかし、警告され、複雑なプレゼンテーション・ロジックを持つ大規模なコードベースで使用しようとしたときに、このアプローチは、悪夢になるだろう。
モデルの作成方法は?
(上記で説明したように)「モデル」クラスは1つではないため、実際には「モデルを構築」しません。代わりに、特定のメソッドを実行できるServicesの作成から始めます。そして、ドメインオブジェクトとマッパーを実装します。
サービスメソッドの例:
上記の両方のアプローチには、識別サービスのためのこのログイン方法がありました。実際にはどのように見えるでしょうか。私が書いたライブラリの同じ機能の少し変更されたバージョンを使用しています。私は怠惰だからです:
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);
}
マッパーを作成する方法
永続性の抽象化を実装するには、最も柔軟なアプローチは、カスタムデータマッパーを作成することです。
From:PoEAAブック
実際には、特定のクラスまたはスーパークラスとの相互作用のために実装されています。あなたが持っているCustomer
とAdmin
あなたのコードに(両方ともUser
スーパークラスから継承する)としましょう。どちらも異なるフィールドを含んでいるため、おそらく一致するマッパーは別になります。しかし、共有され、一般的に使用される操作にもなります。たとえば、「最後に見たオンライン」時間を更新します。また、既存のマッパーを複雑にする代わりに、より実用的なアプローチは、そのタイムスタンプのみを更新する一般的な「ユーザーマッパー」を使用することです。
追加のコメント:
データベーステーブルとモデル
データベーステーブル、ドメインオブジェクト、マッパーの間に直接的な1:1:1の関係がある場合がありますが、大規模なプロジェクトでは、予想よりも一般的ではない場合があります。
単一のドメインオブジェクトによって使用される情報は、オブジェクト自体がデータベース内に永続性を持たない一方で、異なるテーブルからマッピングされる場合があります。
例:月次レポートを生成する場合。これはさまざまなテーブルから情報を収集しMonthlyReport
ますが、データベースに魔法のテーブルはありません。
1つのマッパーが複数のテーブルに影響を与える可能性があります。
例:User
オブジェクトのデータを保存する場合、このドメインオブジェクトには、他のドメインオブジェクトのコレクション(Group
インスタンス)を含めることができます。それらを変更してを格納するUser
と、データマッパーは複数のテーブルのエントリを更新または挿入する必要があります。
単一のドメインオブジェクトのデータは、複数のテーブルに格納されます。
例:大規模なシステム(中規模のソーシャルネットワークなど)では、ユーザー認証データと頻繁にアクセスされるデータを、めったに必要とされない大きなコンテンツの塊とは別に保存するのが実用的です。その場合、まだ単一のUser
クラスがあるかもしれませんが、そこに含まれる情報は、完全な詳細がフェッチされたかどうかによって異なります。
すべてのドメインオブジェクトに対して複数のマッパーが存在する可能性があります
例:あなたは、一般向けのソフトウェアと管理ソフトウェアの両方を共有するコードベースのニュースサイトを持っています。ただし、両方のインターフェイスが同じ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レイヤーのすべての主要な機能が含まれます(もちろん、記事に直接関連しないいくつかのXHRコンポーネントがある場合があります)。