MVCでモデルをどのように構成する必要がありますか?[閉まっている]


551

私は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つのレイヤーができますか?


133
なぜそれらを再びスローするためだけに例外をキャッチするのですか?
ベイリーパーカー、

9
@エリアス・ヴァン・ウーテゲム:あなたは要点を逃しました。この場合、それらをキャッチしても意味がありません。
Karoly Horvath 2012

4
@エリアス・ヴァン・オーテゲム:ハァッ?再スローで機能する場合、上位層が例外をキャッチすることを意味します。しかし、もしあるなら、それは無意味な再スローなしでそれをキャッチしたでしょう...(それでもまだそれが得られない場合は、小さなテストコードをモックアップしてください)
Karoly Horvath

3
@エリアス・ヴァン・ウーテゲム:何を言っているのかわかりません。特定のレイヤーで例外を処理しなくても、アプリが停止するわけではありません。その再スローが必要なコード例を構築してください(より正確には、構築に失敗しました)。聞かせてのは、このofftopic会話を中止してください
カロリー・ホーバス

6
@drrcknlsn:これは有効な引数ですが、その場合は少なくとも、スローされると予想される例外をキャッチしてくださいException。ジェネリックにはドキュメント値があまりありません。個人的には、私がその道を進んだ場合@exception、PHPDocのようなメカニズムを選択するので、生成されたドキュメントに表示されます。
Karoly Horvath 2012

回答:


903

免責事項:以下は、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から。

サービスインスタンスにアクセスする

ViewControllerの両方のインスタンス(いわゆる「UIレイヤー」)がこれらのサービスにアクセスするには、2つの一般的なアプローチがあります。

  1. 必要なサービスは、できればDIコンテナーを使用して、ビューとコントローラーのコンストラクターに直接挿入できます。
  2. すべてのビューとコントローラーの必須の依存関係としてのサービスのファクトリーの使用。

ご想像のとおり、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つの独立した部分で構成されているわけではありません。構造は次のようになります。

MVC分離

つまり、プレゼンテーションレイヤーのロジックが存在しないものに近い場合、実用的なアプローチは、それらを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ブック

実際には、特定のクラスまたはスーパークラスとの相互作用のために実装されています。あなたが持っているCustomerAdminあなたのコードに(両方ともUserスーパークラスから継承する)としましょう。どちらも異なるフィールドを含んでいるため、おそらく一致するマッパーは別になります。しかし、共有され、一般的に使用される操作にもなります。たとえば、「最後に見たオンライン」時間を更新します。また、既存のマッパーを複雑にする代わりに、より実用的なアプローチは、そのタイムスタンプのみを更新する一般的な「ユーザーマッパー」を使用することです。

追加のコメント:

  1. データベーステーブルとモデル

    データベーステーブル、ドメインオブジェクトマッパーの間に直接的な1:1:1の関係がある場合がありますが、大規模なプロジェクトでは、予想よりも一般的ではない場合があります。

    • 単一のドメインオブジェクトによって使用される情報は、オブジェクト自体がデータベース内に永続性を持たない一方で、異なるテーブルからマッピングされる場合があります。

      例:月次レポートを生成する場合。これはさまざまなテーブルから情報を収集しMonthlyReportますが、データベースに魔法のテーブルはありません。

    • 1つのマッパーが複数のテーブルに影響を与える可能性があります。

      例:Userオブジェクトのデータを保存する場合、このドメインオブジェクトには、他のドメインオブジェクトのコレクション(Groupインスタンス)を含めることができます。それらを変更してを格納するUserと、データマッパーは複数のテーブルのエントリを更新または挿入する必要があります。

    • 単一のドメインオブジェクトのデータは、複数のテーブルに格納されます。

      例:大規模なシステム(中規模のソーシャルネットワークなど)では、ユーザー認証データと頻繁にアクセスされるデータを、めったに必要とされない大きなコンテンツの塊とは別に保存するのが実用的です。その場合、まだ単一のUserクラスがあるかもしれませんが、そこに含まれる情報は、完全な詳細がフェッチされたかどうかによって異なります。

    • すべてのドメインオブジェクトに対して複数のマッパーが存在する可能性があります

      例:あなたは、一般向けのソフトウェアと管理ソフトウェアの両方を共有するコードベースのニュースサイトを持っています。ただし、両方のインターフェイスが同じArticleクラスを使用する一方で、管理には、より多くの情報を入力する必要があります。この場合、「内部」と「外部」という2つの別個のマッパーがあります。それぞれが異なるクエリを実行するか、異なるデータベースを使用します(マスターまたはスレーブなど)。

  2. ビューはテンプレートではありません

    MVCのビューインスタンス(パターンのMVPバリエーションを使用していない場合)は、表示ロジックを担当します。つまり、各ビューは通常、少なくともいくつかのテンプレートを操作します。モデルレイヤーからデータを取得し、受け取った情報に基づいてテンプレートを選択し、値を設定します。

    これから得られる利点の1つは、再利用性です。ListViewクラスを作成すると、適切に記述されたコードを使用して、同じクラスで記事の下のユーザーリストとコメントのプレゼンテーションを処理できます。どちらも同じプレゼンテーションロジックを持っているからです。テンプレートを切り替えるだけです。

    ネイティブPHPテンプレートを使用することも、サードパーティのテンプレートエンジンを使用することもできます。また、Viewインスタンスを完全に置き換えることができるいくつかのサードパーティライブラリがある場合もあります。

  3. 古いバージョンの回答はどうですか?

    唯一の大きな変更は、旧バージョンではModelと呼ばれていたものが実際にはServiceであることです。「ライブラリの類推」の残りの部分はかなりうまくいきます。

    私が目にする唯一の欠点は、これが本から情報を返すが、本自体には触れられないため、これは本当に奇妙なライブラリになるということです。そうしないと、抽象化が「リーク」し始めます。もっとふさわしいアナロジーを考えなければならないかもしれません。

  4. ViewインスタンスとControllerインスタンスの関係は何ですか?

    MVC構造は、UIとモデルの2つのレイヤーで構成されています。UIレイヤーの主な構造は、ビューとコントローラーです。

    MVCデザインパターンを使用するWebサイトを扱う場合、ビューとコントローラーを1対1の関係にするのが最善の方法です。各ビューはWebサイトのページ全体を表し、特定のビューに対するすべての着信要求を処理するための専用コントローラーがあります。

    たとえば、開かれた記事を表すには\Application\Controller\Document、およびが必要\Application\View\Documentです。記事を処理する場合、これにはUIレイヤーのすべての主要な機能が含まれます(もちろん、記事に直接関連しないいくつかのXHRコンポーネントがある場合があります)


4
@Rinzler、あなたはそのリンクのどこにも、モデルについて何も言わなかったことに気づくでしょう(1つのコメントを除いて)。これは、「データベーステーブルへのオブジェクト指向インターフェイス」にすぎません。これをモデルのようなものに成形しようとすると、SRPLSPに違反することになります。
tereško

8
@hafichukのみの状況で、ActiveRecordパターンを採用することが妥当であるのはプロトタイピング用です。プロダクションにとって意味のあるコードを書き始めると、ストレージとビジネスロジックが混在するため、アンチパターンになります。そして、以来、モデルレイヤーは、他のMVC部品を全く知らないです。これは、元のパターンのバリエーションによっては変わりません。MVVMを使用している場合でも。「複数のモデル」はなく、何にもマップされていません。モデルはレイヤーです。
テレシュコ

3
ショートバージョン-モデルはデータ構造です。
Eddie B、

9
彼がMVCを発明したことをよく見て、この記事にはいくつかのメリットがあるかもしれません。
Eddie B

3
...または単なる関数のセットです。MVCはOOPスタイルで実装する必要はありませんが、ほとんどがそのように実装されています。最も重要なことは、レイヤーを分離し、適切なデータと制御フローを確立することです
hek2mgl 2013

37

データベースクエリ、計算、REST呼び出しなど、ビジネスロジックであるものはすべてモデルに属しています。

モデル自体でデータアクセスを行うことができます。MVCパターンは、それを行うことを制限しません。サービス、マッパーなどで糖衣することができますが、モデルの実際の定義は、ビジネスロジックを処理するレイヤーにすぎません。それはあなたが望むものであれば、クラス、関数、または膨大な数のオブジェクトを備えた完全なモジュールにすることができます。

データベースクエリを直接モデルで実行する代わりに、実際にデータベースクエリを実行する別のオブジェクトを用意する方が常に簡単です。これは特に、ユニットテスト時に便利です(モデルにモックデータベースの依存関係を挿入するのが簡単なため)。

class Database {
   protected $_conn;

   public function __construct($connection) {
       $this->_conn = $connection;
   }

   public function ExecuteObject($sql, $data) {
       // stuff
   }
}

abstract class Model {
   protected $_db;

   public function __construct(Database $db) {
       $this->_db = $db;
   }
}

class User extends Model {
   public function CheckUsername($username) {
       // ...
       $sql = "SELECT Username FROM" . $this->usersTableName . " WHERE ...";
       return $this->_db->ExecuteObject($sql, $data);
   }
}

$db = new Database($conn);
$model = new User($db);
$model->CheckUsername('foo');

また、PHPでは、バックトレースが保持されるため(特に例のような場合)、例外をキャッチ/再スローする必要はほとんどありません。例外をスローさせて、代わりにコントローラーでキャッチしてください。


私の構造は非常によく似ていますが、もう少し分離しているだけだと思います。接続を迂回しているのは、トランザクションでチャンクを実行する必要があるためです。ユーザーを追加してから、そのユーザーをロールに追加したいのですが、ロールが失敗した場合はロールバックします。それを整理する唯一の方法は、接続を渡すことでした。
Dietpixel

10
-1:完全に間違っている。モデルはテーブルの抽象化ではありません。
テレシュコ2014年

1
Userクラスは基本的にモデルを拡張するが、オブジェクトをitsn't。ユーザーはオブジェクトである必要があり、id、nameなどのプロパティがあります...デプロイするUserクラスはヘルパーです。
TomSawyer 16

1
MVCは理解していると思いますが、OOPについては理解していません。このシナリオでは、私が言ったようUserに、オブジェクトの略で、のようなメソッドではなく、ユーザーのプロパティを持つCheckUsername必要がありUserます。新しいオブジェクトを作成する場合はどうすればよいですか?new User($db)
TomSawyer 16

@TomSawyer OOPは、オブジェクトにプロパティが必要であることを意味しません。あなたが説明しているのはデザインパターンであり、それは質問やその質問への回答とは無関係です。OOPは言語モデルであり、デザインパターンではありません。
ネットコーダー2016

20

Web-「MVC」では、好きなことができます。

元のコンセプト(1)では、モデルをビジネスロジックとして説明していました。アプリケーションの状態を表し、データの整合性を確保する必要があります。そのアプローチは、しばしば「脂肪モデル」と呼ばれます。

ほとんどのPHPフレームワークは、モデルが単なるデータベースインターフェイスである、より浅いアプローチに従います。ただし、少なくともこれらのモデルは、入力データと関係を検証する必要があります。

どちらの方法でも、SQLの呼び出しやデータベースの呼び出しを別のレイヤーに分離しても、それほど遠くはありません。この方法では、実際のストレージAPIではなく、実際のデータ/動作にのみ関心を持つ必要があります。(ただし、やり過ぎることは不合理です。たとえば、事前に設計されていなければ、データベースバックエンドをファイルストレージに置き換えることはできません。)


8
リンクが無効です(404)
Kyslik 2013


6

多くの場合、ほとんどのアプリケーションにはデータ、表示、および処理の部分があり、それらすべてを文字MVおよびに入れCます。

モデル(M - >アプリケーションの状態を保持している属性があり、それがどの程度のことを知らないVC

View(V ->アプリケーションの表示形式があり、その上でダイジェスト方法のモデルについてのみ知っており、気にしませんC

コントローラ(C ---->アプリケーションの処理部を有し、MとVとの間の配線として機能し、それは両方に依存してMV異なりMそしてV

全体として、それぞれの間で懸念の分離があります。将来、変更や機能強化は非常に簡単に追加できます。


0

私の場合、クエリ、フェッチなどのすべての直接データベース対話を処理するデータベースクラスがあります。したがって、データベースをMySQLからPostgreSQLに変更する必要がある場合、問題は発生しません。そのため、追加のレイヤーを追加すると便利です。

各テーブルは独自のクラスと特定のメソッドを持つことができますが、実際にデータを取得するために、データベースクラスにそれを処理させます。

ファイル Database.php

class Database {
    private static $connection;
    private static $current_query;
    ...

    public static function query($sql) {
        if (!self::$connection){
            self::open_connection();
        }
        self::$current_query = $sql;
        $result = mysql_query($sql,self::$connection);

        if (!$result){
            self::close_connection();
            // throw custom error
            // The query failed for some reason. here is query :: self::$current_query
            $error = new Error(2,"There is an Error in the query.\n<b>Query:</b>\n{$sql}\n");
            $error->handleError();
        }
        return $result;
    }
 ....

    public static function find_by_sql($sql){
        if (!is_string($sql))
            return false;

        $result_set = self::query($sql);
        $obj_arr = array();
        while ($row = self::fetch_array($result_set))
        {
            $obj_arr[] = self::instantiate($row);
        }
        return $obj_arr;
    }
}

テーブルオブジェクトクラスL

class DomainPeer extends Database {

    public static function getDomainInfoList() {
        $sql = 'SELECT ';
        $sql .='d.`id`,';
        $sql .='d.`name`,';
        $sql .='d.`shortName`,';
        $sql .='d.`created_at`,';
        $sql .='d.`updated_at`,';
        $sql .='count(q.id) as queries ';
        $sql .='FROM `domains` d ';
        $sql .='LEFT JOIN queries q on q.domainId = d.id ';
        $sql .='GROUP BY d.id';
        return self::find_by_sql($sql);
    }

    ....
}

この例が、良い構造を作成するのに役立つことを願っています。


12
「ですから、私のデータベースをMySQLからPostgreSQLに変更しなければならない場合、何の問題もありません。」上記のコードを使用すると、いろんなものを変更するのに大きな問題が発生します。
PeeHaa

編集後、時間が経つにつれ、私の答えが意味をなさなくなります。しかし、ここにとどまる必要があります
イブ

2
Databaseこの例ではクラスではありません。これは関数のラッパーにすぎません。また、オブジェクトのない「テーブルオブジェクトクラス」をどのように作成できますか?
tereško

2
@tereško私はあなたの投稿の多くを読みました、そして彼らは素晴らしいです。しかし、どこにも完全なフレームワークはありません。「ちゃんとやってる」って知ってる?それとも、あなたや他の何人かと同じようにそれをする人がいますか?ありがとう。
ジョニー2014

遅くなるかもしれませんが、PDOは将来の変更を容易にするためにDBの「レイヤー」を作成する必要があるという問題をほぼ解決することを指摘したいと思います。
Matthew Goulart 2016
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.