web-dev-qa-db-ja.com

MVCではモデルはどここのように構成されるべきですか?

私は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層になりますか?

532
Dietpixel

免責事項:以下は、PHPベースのWebアプリケーションのコンテキストでMVCのようなパターンを理解する方法の説明です。コンテンツで使用されるすべての外部リンクは、用語と概念を説明するためのものであり、notは、主題に関する自分自身の信頼性を意味します。

最初に片付けなければならないことは、モデルはレイヤーです

2つ目: classical MVC とWeb開発で使用するものには違いがあります。 Here's 私が書いた少し古い回答で、それらがどのように異なるかを簡単に説明しています。

モデルではないもの:

モデルはクラスでも単一のオブジェクトでもありません。ほとんどのフレームワークはこの誤解を永続させるので、(私もそうしましたが、別の方法で学習し始めたときに元の答えが書かれていましたが)を作成することは非常によくあります。

オブジェクトリレーショナルマッピング手法(ORM)でも、データベーステーブルの抽象化でもありません。別の方法であなたに言う人は誰でも 'sell' 別の真新しいORMまたはフレームワーク全体を試みている可能性が高いです。

モデルとは:

適切なMVC適応では、Mにはすべてのドメインビジネスロジックが含まれ、 Model Layer mostlyから作成されます3種類の構造:

  • Domain Objects

    ドメインオブジェクトは、純粋なドメイン情報の論理コンテナです。通常、問題のドメイン空間内の論理エンティティを表します。一般的にビジネスロジックと呼ばれます。

    これは、請求書を送信する前にデータを検証する方法、または注文の合計コストを計算する方法を定義する場所です。同時に、 Domain Objects はストレージをまったく認識しません- where (SQLデータベース、REST AP​​I 、テキストファイルなど)も if でも保存または取得されます。

  • Data Mappers

    これらのオブジェクトは、ストレージのみを担当します。データベースに情報を保存すると、SQLが存在する場所になります。または、XMLファイルを使用してデータを保存し、 Data Mappers がXMLファイルとの間で解析を行っている可能性があります。

  • Services

    それらは「上位レベルのドメインオブジェクト」と考えることができますが、ビジネスロジックの代わりに、 Services Domain Objects マッパー。これらの構造は、ドメインのビジネスロジックと対話するための「パブリック」インターフェイスを作成することになります。それらを回避できますが、一部のドメインロジックを Controllers にリークするというペナルティがあります。

    ACL implementation の質問には、この主題に関連する答えがあります-役に立つかもしれません。

モデル層とMVCトライアドの他の部分との間の通信は、 Services を介してのみ行われます。明確な分離には、いくつかの追加の利点があります。

  • 単一責任原則 (SRP)を実施するのに役立ちます
  • ロジックが変更された場合に追加の「小刻みの部屋」を提供します
  • コントローラーをできるだけシンプルに保つ
  • 外部APIが必要な場合、明確な青写真を提供します

モデルと対話する方法は?

前提条件:講義を見る "Global State and Singletons" および "Do n't Look for Things! " Clean Code Talksから。

サービスインスタンスへのアクセスの取得

View および Controller インスタンス(いわゆる「UIレイヤー」)の両方にこれらのサービスにアクセスさせるには、2つの一般的なアプローチ:

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

ご想像のとおり、DIコンテナははるかにエレガントなソリューションです(初心者にとっては簡単ではありません)。この機能を検討することをお勧めする2つのライブラリは、Syfmonyのスタンドアロン DependencyInjection component または Auryn です。

ファクトリーとDIコンテナーを使用するソリューションでは、選択されたコントローラーとビュー間で共有されるさまざまなサーバーのインスタンスを共有して、特定の要求/応答サイクルを実現できます。

モデルの状態の変更

コントローラーのモデルレイヤーにアクセスできるようになったので、実際に使用を開始する必要があります。

public function postLogin(Request $request)
{
    $email = $request->get('email');
    $identity = $this->identification->findIdentityByEmailAddress($email);
    $this->identification->loginWithPassword(
        $identity,
        $request->get('password')
    );
}

コントローラーには非常に明確なタスクがあります。ユーザー入力を取得し、この入力に基づいてビジネスロジックの現在の状態を変更します。この例では、間で変更される状態は「匿名ユーザー」と「ログインユーザー」です。

コントローラーはユーザーの入力を検証する責任を負いません。これはビジネスルールの一部であり、コントローラーは間違いなくSQLクエリを呼び出していないためです here または here (ドン彼らを憎むな、彼らは見当違いであり、悪ではない)。

ユーザーに状態変更を表示しています。

OK、ユーザーはログインしました(または失敗しました)。 今何? ユーザーはまだ気づいていません。したがって、実際に応答を生成する必要があり、それはビューの責任です。

public function postLogin()
{
    $path = '/login';
    if ($this->identification->isUserLoggedIn()) {
        $path = '/dashboard';
    }
    return new RedirectResponse($path); 
}

この場合、ビューは、モデルレイヤーの現在の状態に基づいて、2つの可能な応答のいずれかを生成しました。別のユースケースでは、「現在の記事の選択」などに基づいて、レンダリングするさまざまなテンプレートを選択するビューを使用します。

ここで説明されているように、プレゼンテーション層は実際には非常に複雑になります: PHPでのMVCビューについて .

ただし、REST AP​​Iを作成しています。

もちろん、これが過剰な場合の状況があります。

MVCは、 Separation of Concerns 原則の単なる具体的なソリューションです。 MVCはビジネスロジックからユーザーインターフェイスを分離し、UIではユーザー入力とプレゼンテーションの処理を分離しました。これは重要です。多くの場合、人々はそれを「トライアド」と表現しますが、実際には3つの独立した部分で構成されているわけではありません。構造は次のようになります。

MVC separation

つまり、プレゼンテーションレイヤーのロジックがほとんど存在しない場合、実用的なアプローチはプレゼンテーションレイヤーを単一レイヤーとして保持することです。また、モデル層のいくつかの側面を大幅に簡素化できます。

このアプローチを使用すると、ログイン例(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);
}

これは持続可能ではありませんが、応答本文をレンダリングするための複雑なロジックがある場合、この単純化はより単純なシナリオに非常に役立ちます。しかし、警告されます。このアプローチは、複雑なプレゼンテーションロジックを持つ大規模なコードベースで使用しようとすると、悪夢になります。

モデルを構築する方法は?

(上記で説明したように)単一の「モデル」クラスは存在しないため、実際には「モデルを構築」しません。代わりに、特定のメソッドを実行できる Services の作成から始めます。そして、 Domain Objects および Mappers を実装します。

サービスメソッドの例:

上記の両方のアプローチには、識別サービス用のこのログイン方法がありました。実際にはどのように見えるでしょうか。 a library の同じ機能を少し修正したバージョンを使用しています。

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);
}

マッパーを作成する方法

永続性の抽象化を実装するための最も柔軟なアプローチは、カスタム データマッパー を作成することです。

Mapper diagram

From: PoEAA book

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

追加のコメント:

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

    データベーステーブル、 Domain Object 、および Mapper 間に直接1:1:1の関係がある場合がありますが、より大きなプロジェクトではあなたが期待するほど一般的ではありません:

    • 単一の Domain Object で使用される情報は、異なるテーブルからマップされる場合がありますが、オブジェクト自体はデータベースに永続性を持ちません。

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

    • 単一の Mapper は、複数のテーブルに影響を与える可能性があります。

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

    • 単一の Domain Object からのデータは、複数のテーブルに保存されます。

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

    • Domain Object ごとに複数のマッパーが存在する場合があります

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

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

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

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

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

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

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

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

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

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

    MVC設計パターンを使用するWebサイトを扱う場合、最良の方法は、ビューとコントローラーを1対1の関係にすることです。各ビューはWebサイトのページ全体を表し、特定のビューに対するすべての着信要求を処理する専用のコントローラーを備えています。

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

883
tereško

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

あなたはモデル自体の中でデータアクセスを持つことができます、MVCパターンはそれをすることからあなたを制限しません。あなたはそれをサービス、マッパーなどでシュガーコートすることができますが、モデルの実際の定義はビジネスロジックを処理するレイヤです。それがあなたが望むものであるなら、それはクラス、関数、またはgazillionオブジェクトを持つ完全なモジュールでありえます。

モデル内で直接クエリを実行するのではなく、実際にデータベースクエリを実行する別のオブジェクトを使用するほうが常に簡単です。これは単体テストの際に特に便利です(モデルにモックデータベースの依存関係を挿入することが容易なため)

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では、バックトレースが保持されるため、特にあなたの例のような場合には、例外をキャッチ/再スローする必要はめったにありません。ただ例外を投げさせて、代わりにコントローラでそれをキャッチするだけです。

35
netcoder

Web - "MVC"では、あなたが好きなことを何でもすることができます。

元のコンセプト (1) ビジネスロジックとしてモデルを説明しました。これはアプリケーションの状態を表し、データの一貫性を強化する必要があります。そのアプローチはしばしば「ファットモデル」と呼ばれます。

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

どちらにしても、SQLやデータベース呼び出しを別の層に分けても、それほど遠くないでしょう。この方法では、実際のストレージAPIではなく、実際のデータ/動作に注意を払うだけで済みます。 (ただし、やり過ぎるのは無理があります。たとえば、データベースのバックエンドをファイルストレージに置き換えることができない場合は、そのように設計されていないと、決して不可能になります。)

19
mario

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

Model(M - >アプリケーションの状態を保持する属性を持ち、VCについて何も知らない。

View(V - >アプリケーションの表示形式があり、それに関するダイジェストモデルについて知っているだけで、Cについては気にしません。

Controller(C---->アプリケーションの処理部分を持ち、MとVの間の配線として機能し、MVとは異なり、MVの両方に依存します。

完全にそれぞれの間に懸念の分離があります。将来的には、変更や機能拡張を簡単に追加できるようになります。

私の場合は、クエリ、フェッチなど、データベースとの直接のやり取りをすべて処理するデータベースクラスがあります。データベースを 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;
    }
}

テーブルオブジェクトclassL

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);
    }

    ....
}

この例があなたが良い構造を作るのを助けてくれることを願っています。

0
Ibu