我刚刚掌握了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;
    }
}

我的模型往往是映射到数据库表的实体类。

模型对象是否应该像上面的代码一样具有所有的数据库映射属性,或者是否可以将实际执行数据库工作的代码分离出来?

我最终会有四层吗?


当前回答

所有业务逻辑都属于模型,无论是数据库查询、计算、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中,您很少需要捕获/重新抛出异常,因为保留了反向跟踪,特别是在像您的示例这样的情况下。只是让异常抛出并在控制器中捕获它。

其他回答

免责声明:以下是我在基于php的web应用程序上下文中如何理解类似mvc的模式的描述。所有在内容中使用的外部链接都是为了解释术语和概念,而不是暗示我自己在这个主题上的可信度。

我必须弄清楚的第一件事是:模型是一个层。

第二:经典的MVC和我们在web开发中使用的MVC是有区别的。这是我以前写过的一个答案,简要地描述了它们的不同之处。

模型不是什么:

模型不是一个类或任何单个对象。这是一个非常常见的错误(我也犯过,尽管最初的答案是在我开始学习其他知识时写的),因为大多数框架都延续了这种误解。

它既不是对象关系映射技术(ORM),也不是数据库表的抽象。任何不这么说的人都很可能是在试图“销售”另一个全新的ORM或整个框架。

什么是模型:

在适当的MVC改编中,M包含了所有的领域业务逻辑,模型层主要由三种类型的结构组成:

Domain Objects A domain object is a logical container of purely domain information; it usually represents a logical entity in the problem domain space. Commonly referred to as business logic. This would be where you define how to validate data before sending an invoice, or to compute the total cost of an order. At the same time, Domain Objects are completely unaware of storage - neither from where (SQL database, REST API, text file, etc.) nor even if they get saved or retrieved. Data Mappers These objects are only responsible for the storage. If you store information in a database, this would be where the SQL lives. Or maybe you use an XML file to store data, and your Data Mappers are parsing from and to XML files. Services You can think of them as "higher level Domain Objects", but instead of business logic, Services are responsible for interaction between Domain Objects and Mappers. These structures end up creating a "public" interface for interacting with the domain business logic. You can avoid them, but at the penalty of leaking some domain logic into Controllers. There is a related answer to this subject in the ACL implementation question - it might be useful.

模型层和MVC三位一体的其他部分之间的通信应该只通过服务进行。这种明确的分离还有一些额外的好处:

它有助于执行单一责任原则(SRP) 在逻辑发生变化时提供额外的“摆动空间” 保持控制器尽可能简单 如果你需要一个外部API,它会给出一个清晰的蓝图

 

如何与模型交互?

先决条件:观看Clean Code Talks的讲座“全局状态和单例”和“不要寻找东西!”

获得对服务实例的访问权

对于视图和控制器实例(你可以称之为:“UI层”)都可以访问这些服务,有两种一般的方法:

您可以直接在视图和控制器的构造函数中注入所需的服务,最好使用DI容器。 将服务的工厂用作所有视图和控制器的强制依赖项。

正如您可能怀疑的那样,DI容器是一个更优雅的解决方案(但对于初学者来说不是最简单的)。我建议考虑使用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查询,就像你在这里看到的那样(请不要讨厌它们,它们是被误导的,不是邪恶的)。

显示用户状态变化。

Ok,用户已登录(或失败)。现在怎么办呢?该用户仍然不知道它。你需要实际产生一个响应,这是视图的责任。

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

在这种情况下,视图根据模型层的当前状态产生了两种可能的响应之一。对于不同的用例,你会让视图选择不同的模板来渲染,基于类似“当前选择的文章”之类的东西。

表示层实际上可以非常复杂,如本文所述:理解PHP中的MVC视图。

但我只是在做一个REST API!

当然,在某些情况下,这是一种过度的行为。

MVC只是关注点分离原则的一个具体解决方案。MVC将用户界面从业务逻辑中分离出来,在UI中它将用户输入的处理和表示分离出来。这一点至关重要。虽然人们经常把它描述为“三位一体”,但它实际上并不是由三个独立的部分组成的。结构是这样的:

这意味着,当您的表示层逻辑几乎不存在时,实用的方法是将它们保持为单层。它还可以极大地简化模型层的某些方面。

使用这种方法,登录示例(对于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);
}

虽然这是不可持续的,但当您有复杂的逻辑来呈现响应体时,这种简化对于更琐碎的场景非常有用。但是请注意,当尝试在具有复杂表示逻辑的大型代码库中使用时,这种方法将成为一场噩梦。

 

如何构建模型?

因为没有一个单独的“Model”类(如上所述),所以实际上不需要“构建模型”。相反,您可以从创建能够执行某些方法的服务开始。然后实现域对象和映射器。

一个服务方法的例子:

在上述两种方法中,都有这种用于标识服务的登录方法。它实际上会是什么样子。我使用的是从一个库相同的功能略有修改的版本,我写..因为我很懒:

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 function changeIdentityStatus(Entity\Identity $identity, int $status)
{
    $identity->setStatus($status);
    $identity->setLastUsed(time());
    $mapper = $this->mapperFactory->create(Mapper\Identity::class);
    $mapper->store($identity);
}

创建映射器的方法

要实现持久性的抽象,最灵活的方法是创建自定义数据映射器。

选自:poea书

In practice they are implemented for interaction with specific classes or superclasses. Lets say you have Customer and Admin in your code (both inheriting from a User superclass). Both would probably end up having a separate matching mapper, since they contain different fields. But you will also end up with shared and commonly used operations. For example: updating the "last seen online" time. And instead of making the existing mappers more convoluted, the more pragmatic approach is to have a general "User Mapper", which only update that timestamp.

其他一些意见:

Database tables and model While sometimes there is a direct 1:1:1 relationship between a database table, Domain Object, and Mapper, in larger projects it might be less common than you expect: Information used by a single Domain Object might be mapped from different tables, while the object itself has no persistence in the database. Example: if you are generating a monthly report. This would collect information from different of tables, but there is no magical MonthlyReport table in the database. A single Mapper can affect multiple tables. Example: when you are storing data from the User object, this Domain Object could contain collection of other domain objects - Group instances. If you alter them and store the User, the Data Mapper will have to update and/or insert entries in multiple tables. Data from a single Domain Object is stored in more than one table. Example: in large systems (think: a medium-sized social network), it might be pragmatic to store user authentication data and often-accessed data separately from larger chunks of content, which is rarely required. In that case you might still have a single User class, but the information it contains would depend of whether full details were fetched. For every Domain Object there can be more than one mapper Example: you have a news site with a shared codebased for both public-facing and the management software. But, while both interfaces use the same Article class, the management needs a lot more info populated in it. In this case you would have two separate mappers: "internal" and "external". Each performing different queries, or even use different databases (as in master or slave). A view is not a template View instances in MVC (if you are not using the MVP variation of the pattern) are responsible for the presentational logic. This means that each View will usually juggle at least a few templates. It acquires data from the Model Layer and then, based on the received information, chooses a template and sets values. One of the benefits you gain from this is re-usability. If you create a ListView class, then, with well-written code, you can have the same class handing the presentation of user-list and comments below an article. Because they both have the same presentation logic. You just switch templates. You can use either native PHP templates or use some third-party templating engine. There also might be some third-party libraries, which are able to fully replace View instances. What about the old version of the answer? The only major change is that, what is called Model in the old version, is actually a Service. The rest of the "library analogy" keeps up pretty well. The only flaw that I see is that this would be a really strange library, because it would return you information from the book, but not let you touch the book itself, because otherwise the abstraction would start to "leak". I might have to think of a more fitting analogy. What is the relationship between View and Controller instances? The MVC structure is composed of two layers: ui and model. The main structures in the UI layer are views and controller. When you are dealing with websites that use MVC design pattern, the best way is to have 1:1 relation between views and controllers. Each view represents a whole page in your website and it has a dedicated controller to handle all the incoming requests for that particular view. For example, to represent an opened article, you would have \Application\Controller\Document and \Application\View\Document. This would contain all the main functionality for UI layer, when it comes to dealing with articles (of course you might have some XHR components that are not directly related to articles).

在我的例子中,我有一个数据库类,它处理所有直接的数据库交互,如查询、获取等。因此,如果我必须将我的数据库从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;
    }
}

表对象类

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

    ....
}

我希望这个例子能帮助您创建一个好的结构。

更常见的是,大多数应用程序都有数据、显示和处理部分,我们把它们都放在字母M、V和C中。

Model(M)—>拥有应用程序状态的属性,它不知道关于V和C的任何事情。

视图(V)——>具有应用程序的显示格式,并且只知道如何在它上消化模型,而不关心C。

控制器(C)---->有应用程序的处理部分,作为M和V之间的接线,它依赖于M和V,不像M和V。

总之,两者之间的关系是分离的。 将来可以很容易地添加任何更改或增强。

所有业务逻辑都属于模型,无论是数据库查询、计算、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中,您很少需要捕获/重新抛出异常,因为保留了反向跟踪,特别是在像您的示例这样的情况下。只是让异常抛出并在控制器中捕获它。

在Web-“MVC”中,你可以做任何你想做的事情。

最初的概念(1)将模型描述为业务逻辑。它应该表示应用程序状态并强制数据一致性。这种方式通常被称为“肥胖模式”。

大多数PHP框架遵循较浅的方法,其中模型只是一个数据库接口。但至少这些模型仍然应该验证传入的数据和关系。

无论哪种方式,如果您将SQL内容或数据库调用分离到另一层,您都不会离目标太远。这样你只需要关心真实的数据/行为,而不需要关心实际的存储API。(然而,做得太过分是不合理的。例如,如果没有提前设计,你将永远无法用文件存储替换数据库后端。)