How should a model be structured in MVC?

I am just getting a grasp on the MVC framework and I often wonder how much code should go in the model. I tend to have a data access class that has methods like this:

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

My models tend to be an entity class that is mapped to the database table.

Should the model object have all the database mapped properties as well as the code above or is it OK to separate that code out that actually does the database work?

Will I end up having four layers?


Disclaimer: the following is a description of how I understand MVC-like patterns in the context of PHP-based web applications. All the external links that are used in the content are there to explain terms and concepts, and not to imply my own credibility on the subject.

The first thing that I must clear up is: the model is a layer .

Second: there is a difference between classical MVC and what we use in web development. Here's a bit of an older answer I wrote, which briefly describes how they are different.

What a model is NOT:

The model is not a class or any single object. It is a very common mistake to make (I did too, though the original answer was written when I began to learn otherwise), because most frameworks perpetuate this misconception.

Neither is it an Object-Relational Mapping technique (ORM) nor an abstraction of database tables. Anyone who tells you otherwise is most likely trying to 'sell' another brand-new ORM or a whole framework.

What a model is:

In proper MVC adaptation, the M contains all the domain business logic and the Model Layer is mostly made from three types of structures:

  • 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.

  • The communication between the model layer and other parts of the MVC triad should happen only through Services. The clear separation has a few additional benefits:

  • it helps to enforce the single responsibility principle (SRP)
  • provides additional 'wiggle room' in case the logic changes
  • keeps the controller as simple as possible
  • gives a clear blueprint, if you ever need an external API
  • How to interact with a model?

    Prerequisites: watch lectures "Global State and Singletons" and "Don't Look For Things!" from the Clean Code Talks.

    Gaining access to service instances

    For both the View and Controller instances (what you could call: "UI layer") to have access these services, there are two general approaches:

  • You can inject the required services in the constructors of your views and controllers directly, preferably using a DI container.
  • Using a factory for services as a mandatory dependency for all of your views and controllers.
  • As you might suspect, the DI container is a lot more elegant solution (while not being the easiest for a beginner). The two libraries, that I recommend considering for this functionality would be Syfmony's standalone DependencyInjection component or Auryn.

    Both the solutions using a factory and a DI container would let you also share the instances of various servers to be shared between the selected controller and view for a given request-response cycle.

    Alteration of model's state

    Now that you can access to the model layer in the controllers, you need to start actually using them:

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

    Your controllers have a very clear task: take the user input and, based on this input, change the current state of business logic. In this example the states that are changed between are "anonymous user" and "logged in user".

    Controller is not responsible for validating user's input, because that is part of business rules and controller is definitely not calling SQL queries, like what you would see here or here (please don't hate on them, they are misguided, not evil).

    Showing user the state-change.

    Ok, user has logged in (or failed). Now what? Said user is still unaware of it. So you need to actually produce a response and that is the responsibility of a view.

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

    In this case, the view produced one of two possible responses, based on the current state of model layer. For a different use-case you would have the view picking different templates to render, based on something like "current selected of article" .

    The presentation layer can actually get quite elaborate, as described here: Understanding MVC Views in PHP.

    But I am just making a REST API!

    Of course, there are situations, when this is a overkill.

    MVC is just a concrete solution for Separation of Concerns principle. MVC separates user interface from the business logic, and it in the UI it separated handling of user input and the presentation. This is crucial. While often people describe it as a "triad", it's not actually made up from three independent parts. The structure is more like this:

    MVC分离

    It means, that, when your presentation layer's logic is close to none-existent, the pragmatic approach is to keep them as single layer. It also can substantially simplify some aspects of model layer.

    Using this approach the login example (for an API) can be written as:

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

    While this is not sustainable, when you have complicate logic for rendering a response body, this simplification is very useful for more trivial scenarios. But be warned , this approach will become a nightmare, when attempting to use in large codebases with complex presentation logic.

    How to build the model?

    Since there is not a single "Model" class (as explained above), you really do not "build the model". Instead you start from making Services, which are able to perform certain methods. And then implement Domain Objects and Mappers.

    An example of a service method:

    In the both approaches above there was this login method for the identification service. What would it actually look like. I am using a slightly modified version of the same functionality from a library, that I wrote .. because I am lazy:

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

    As you can see, at this level of abstraction, there is no indication of where the data was fetched from. It might be a database, but it also might be just a mock object for testing purposes. Even the data mappers, that are actually used for it, are hidden away in the private methods of this service.

    private function changeIdentityStatus(EntityIdentity $identity, int $status)
    {
        $identity->setStatus($status);
        $identity->setLastUsed(time());
        $mapper = $this->mapperFactory->create(MapperIdentity::class);
        $mapper->store($identity);
    }
    

    Ways of creating mappers

    To implement an abstraction of persistence, on the most flexible approaches is to create custom data mappers.

    From: PoEAA book

    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.

    Some additional comments:

  • 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 ApplicationControllerDocument and ApplicationViewDocument . 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).


  • Everything that is business logic belongs in a model, whether it is a database query, calculations, a REST call, etc.

    You can have the data access in the model itself, the MVC pattern doesn't restrict you from doing that. You can sugar coat it with services, mappers and what not, but the actual definition of a model is a layer that handles business logic, nothing more, nothing less. It can be a class, a function, or a complete module with a gazillion objects if that's what you want.

    It's always easier to have a separate object that actually executes the database queries instead of having them being executed in the model directly: this will especially come in handy when unit testing (because of the easiness of injecting a mock database dependency in your model):

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

    Also, in PHP, you rarely need to catch/rethrow exceptions because the backtrace is preserved, especially in a case like your example. Just let the exception be thrown and catch it in the controller instead.


    In Web-"MVC" you can do whatever you please.

    The original concept (1) described the model as the business logic. It should represent the application state and enforce some data consistency. That approach is often described as "fat model".

    Most PHP frameworks follow a more shallow approach, where the model is just a database interface. But at the very least these models should still validate the incoming data and relations.

    Either way, you're not very far off if you separate the SQL stuff or database calls into another layer. This way you only need to concern yourself with the real data/behaviour, not with the actual storage API. (It's however unreasonable to overdo it. You'll eg never be able to replace a database backend with a filestorage if that wasn't designed ahead.)

    链接地址: http://www.djcxy.com/p/60444.html

    上一篇: 只更新模型的一部分

    下一篇: MVC中应该如何构建模型?