A free and open-source book on ZF3 for beginners


4.20. Model Types

In Zend Framework 3, there is no single Model directory for storing the model classes, as you could assume. Instead, by convention, models are further subdivided into the following principal types, and each type is stored in its own subdirectory (see table 4.9):

Table 4.9. Model Types and their Location
Model Type Directory
Entities APP_DIR/module/Application/src/Entity
Repositories APP_DIR/module/Application/src/Repository
Value Objects APP_DIR/module/Application/src/ValueObject
Services APP_DIR/module/Application/src/Service
Factories In Factory subdirectory under each model type directory. For example, controller factories would be stored in APP_DIR/module/Application/src/Controller/Factory

Separation of models into different types make it easier to design your business logic domain. This is also called the "Domain Driven Design" (or shortly, DDD). The person who proposed DDD was Eric Evans in his famous book called Domain-Driven Design — Tackling Complexity in the Heart of Software.

Below, we will describe the principal model types further.

4.20.1. Entities

Entities are intended for storing some data and always have some identifier property, so you can uniquely identify the data. For example, a User entity always has a unique login property, and you can identify the user by that attribute. You can change some other attributes of the entity, like firstName, or address, but its identifier never changes. Entities are usually stored in a database, in a file system or in any other storage.

Below, you can find an example a User entity, which represents a site visitor:

// The User entity represents a site visitor
class User 
{
    // Properties
    private $login;     // e.g. "admin"
    private $title;     // e.g. "Mr."
    private $firstName; // e.g. "John"
    private $lastName;  // e.g. "Doe"
    private $country;   // e.g. "USA"
    private $city;      // e.g. "Paris"
    private $postCode;  // e.g. "10543"
    private $address;   // e.g. "Jackson rd."
    
    // Behaviors    
    public function getLogin() 
    {
        return $this->login;
    }
        
    public setLogin($login) 
    {
        $this->login = $login;
    }
    
    //...
}

In lines 5-12, we define User model's properties. The best practice is to define the properties using the private access type, and make them available to the caller through getter and setter public methods (like getLogin() and setLogin(), etc).

Model's behavior methods are not limited by getters and setters. You can create other methods which manipulate model's data. For example, you can define the getFullName() convenience method, which would return the user's full name, like "Mr. John Doe".

4.20.2. Repositories

Repositories are specific models responsible for storing and retrieving entities. For example, a UserRepository may represent a database table and provide methods for retrieving User entities. You typically use repositories when storing entities in a database. With repositories, you can encapsulate SQL query logic in the single place and easily maintain and test it.

We will learn about repositories in more details in Database Management with Doctrine, when talking about Doctrine library.

4.20.3. Value Objects

Value objects are a kind of model for which the identity is not as important as for entities. A value object is usually a small class identified by all of its attributes. It does not have an identifier attribute. Value objects typically have getter methods, but do not have setters (value objects are immutable).

For example, a model wrapping a money amount can be treated as a value object:

class MoneyAmount 
{
    // Properties
    private $currency;
    private $amount;
  
    // Constructor
    public function __construct($amount, $currency='USD') 
    {
        $this->amount = $amount;
        $this->currency = $currency;
    }
  
    // Gets the currency code  
    public function getCurrency() 
    {
        return $this->currency;  
    }
  
    // Gets the money amount
    public function getAmount() 
    {
        return $this->amount;
    }  
}

In lines 4-5 we define two properties: currency and amount. The model has no identifier property, instead it is identified by all properties as a whole: if you change either the currency or amount, you would have a different money amount object.

In lines 8-12 we define the constructor method, which initializes the properties.

In lines 15-24, we define getter methods for model's properties. Note that we do not have setter methods (the model is immutable).

4.20.4. Services

Service models usually encapsulate some business logic functionality. Services usually have easily recognizable names ending with "er" suffix, like FileUploader or UserManager.

Below, an example of Mailer service is presented. It has the sendMail() method which takes an EmailMessage value object and sends an E-mail message using standard PHP mail() function:

<?php

// The Email message value object
class EmailMessage 
{  
    private $recipient;
    private $subject;
    private $text;  
  
    // Constructor
    public function __construct($recipient, $subject, $text) 
    {
        $this->recipient = $recipient;
        $this->subject = $subject;
        $this->text = $text;
    }
  
    // Getters
    public function getRecipient() 
    {
        return $this->recipient;
    }
  
    public function getSubject() 
    {
        return $this->subject;
    }
  
    public function getText() 
    {
        return $this->text;
    }
}

// The Mailer service, which can send messages by E-mail
class Mailer 
{
  
    public function sendMail($message) 
    {
        // Use PHP mail() function to send an E-mail
        if(!mail($message->getRecipient(), $message->getSubject(), 
             $message()->getText())) 
        {
            // Error sending message
            return false;
        }
    
        return true;
    }
}

In Zend Framework, you typically register your service models in Service Manager.

4.20.5. Factories

Factories are usually being designed to instantiate other models (particularly, service models). In the simplest cases you can create an instance of a service without any factory, just by using the new operator, but sometimes class creation logic might be rather complex. For example, services often depend on each other, so you might need to inject dependencies to a service. Also, sometimes it may be required to initialize the service right after instantiation by calling one (or several) of its methods.

Factory classes typically have names ending with Factory suffix, like CurrencyConverterFactory, MailerFactory, etc.

For a real-life example, let's imagine that we have a PurchaseManager service, which can process purchases of some goods, and that the PurchaseManager service uses another service named CurrencyConverter, which can connect to an external system providing money exchange rates. Let's write a factory class for the PurchaseManager, which would instantiate the service and pass it the dependency:

<?php
namespace Application\Service\Factory;

use Interop\Container\ContainerInterface;
use Zend\ServiceManager\Factory\FactoryInterface;
use Application\Service\CurrencyConverter;
use Application\Service\PurchaseManager;

/**
 * This is the factory for PurchaseManager service. Its purpose is to instantiate the 
 * service and inject its dependencies.
 */
class PurchaseManagerFactory implements FactoryInterface
{
    public function __invoke(ContainerInterface $container, 
                $requestedName, array $options = null)
    {
        // Get CurrencyConverter service from the service manager.
        $currencyConverter = $container->get(CurrencyConverter::class);
        
        // Instantiate the service and inject dependencies.
        return new PurchaseManager($currencyConverter);
    }
}

In the code above we have the PurchaseManagerFactory class which implements the Zend\ServiceManager\Factory\FactoryInterface interface. The factory class has the __invoke() method whose goal is to instantiate the object. This method has the $container argument which is the service manager. You can use $container to retrieve services from service manager and pass them to the constructor method of the service being instantiated.


Top