How should a model be structured in MVC?

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.

How to interact with a model?

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

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

The easiest way to make sure that both View and Controller instances (for that incoming request) have access to the same version of the Model Layer would be to provide them both with the same ServiceFactory instance. I would do it like this:

 

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.

How to interact with a model?

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

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

The easiest way to make sure that both View and Controller instances (for that incoming request) have access to the same version of the Model Layer would be to provide them both with the same ServiceFactory instance. I would do it like this:

/*
 * Closure for providing lazy initialization of DB connection
 */
$dbhProvider = function() {
    $instance = new \PDO(‘mysql:host=localhost;dbname=******;charset=UTF-8‘, 
    ‘**username**‘, ‘**password**‘);
    $instance->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
    $instance->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
    return $instance;
};

/* 
 * Creates basic structures, which will be used for 
 * interaction with model layer
 */
$serviceFactory = new ServiceFactory(
    new DataMapperFactory($dbhProvider),
    new DomainObjectFactory
    );
$serviceFactory->setDefaultNamespace(‘Application\\Service‘);

/*
 * Initializes the routing mechanism
 */
$configuration = json_decode(
    file_get_contents(__DIR__ . ‘/config/routes.json‘), true);
$router = new Router(new RouteBuilder);
$router->import($configuration);

/*
 * Gets the part of URI after the "?" symbol
 */
$uri = isset($_SERVER[‘REQUEST_URI‘]) 
           ? $_SERVER[‘REQUEST_URI‘] 
           : ‘/‘;

/*
 * Initializes the request abstraction and 
 * apply routing pattens to that instance
 */
$request = new Request($uri);
$router->route($request);

/* 
 * Initialization of View 
 */
$class = ‘\\Application\\View\\‘ . $request->getResourceName();
$view = new $class($serviceFactory);
$view->setDefaultTemplateLocation(__DIR__ . ‘/templates‘);

/*
 * Initialization of Controller
 */
$class = ‘\\Application\\Controller\\‘ . $request->getResourceName();
$controller = new $class($serviceFactory, $view);

/*
 * Execute the necessary command on the controller
 */
$command = $request->getCommand();
$controller->{$command}($request);

/*
 * Produces the response
 */
echo $view->render();

This would let you initialize a not-too-complicated MVC application (notice that there is no caching nor authentication/authorization included). As you can see, the $serviceFactory object is shared between both the View object and Controller object, and keeps track of initialized services.

Also, you might notice that the anonymous $dbhProvider function is passed only to the DataMapperFactory instance, which would be creating all the Data Mappers within any given service.

With this given code, the Controller instance would change the state of the Model Layer, and the View instance (as per Model2 MVC) would request data from the Model Layer.

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:

This might be a simplified authentication method in a recognition service (something that ascertains a user‘s identity).

But you should not think of this example as directly related to the one above, because as part of the authentication process, it should happen right after the $serviceFactory was created (the check-if-logged-in part), while the authenticate() method would be called from within the controller. And the authentication would closely interact with (but be separate from) the authorization service.

namespace Service;

class Recognitions
{
    // -- snip --

    /* This is an EXAMPLE, not a production-level code.
       Do not copy-paste! */
    public function authenticate( $username, $password )
    {
        $account = $this->domainObjectFactory->build(‘User‘);
        $mapper  = $this->dataMapperFactory->build(‘User‘);

        $account->setUsername( $username );
        $mapper->fetch( $account );

        if ( $account->matchPassword($password) )
        {
            $state = $this->dataMapperFactory->build(‘Cookie‘);
        }
        else
        {
            $state = $this->dataMapperFactory->build(‘Session‘);
        }

        $state->store($account);

    }
    // -- snip --
}

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.

P.S. This would also be the part where caching is introduced. For example, as an additional Mapper.

 

Some additional comments:

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

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

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

  4. What is the relationship between View and Controller instances?

    The MVC structure is composed of two layers: presentation and model. The main structures in the Presentation 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 presentation layer, when it comes to dealing with articles (of course you might have some XHR components that are not directly related to articles).

郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。