В Zend Framework 3, нет одной единственной директории 'Model' для хранения классов моделей, как вы могли бы предположить. Вместо этого модели условно делятся на следующие основные типы, и каждый хранится в своей собственной поддиректории.
Имя модели | Директория |
---|---|
Сущности | APP_DIR/module/Application/src/Entity |
Репозитории | APP_DIR/module/Application/src/Repository |
Объекты-значения | APP_DIR/module/Application/src/ValueObject |
Сервисы | APP_DIR/module/Application/src/Service |
Фабрики | В поддиректории Factory для каждой директории типа модели. Например, фабрики контроллеров будут храниться в APP_DIR/module/Application/src/Controller/Factory |
Разделение моделей на разные типы упрощает создание бизнес-логики. Это также называется "проблемно-ориентированное проектирование" (Domain Driven Design или DDD). Впервые DDD предложил использовать Эрик Эванс в его книге Domain-Driven Design — Tackling Complexity in the Heart of Software.
Ниже мы опишем основные типы моделей более детально.
Сущности предназначены для хранения данных и всегда имеют свойство идентификатор, чтобы вы могли точно идентифицировать объект.
Например, сущность User
всегда будет иметь уникальное свойство login
,
и вы можете идентифицировать пользователя по этому свойству. Вы можете менять некоторые другие
свойства сущности, как например firstName
или address
, но ее идентификатор не будет изменяться.
Сущности обычно хранятся в базе данных, в файловой системе или в любом другом хранилище.
Ниже вы найдете образец сущности User
, которая представляет посетителя сайта:
// Сущность User представляет посетителя сайта
class User
{
// Свойства
private $login; // например, "admin"
private $title; // например, "Mr."
private $firstName; // например, "Ivan"
private $lastName; // например, "Ivanov"
private $country; // например, "Russia"
private $city; // например, "Moscow"
private $postCode; // например, "174354"
private $address; // например, "Stroiteley st."
// Поведения
public function getLogin()
{
return $this->login;
}
public function setLogin($login)
{
$this->login = $login;
}
//...
}
В строках 5-12 мы определяем свойства модели User
. Лучше всего определять свойства,
используя приватный тип доступа и делать их доступными для вызова через публичные методы -
геттеры и сеттеры (такие как getLogin()
и setLogin()
и т.д.)
Методы поведения модели не ограничены геттерами и сеттерами. Вы можете создавать другие методы, которые будут управлять данными модели. Например, для удобства вы можете определить метод
getFullName()
, который будет возвращать полное имя пользователя, например, "Mr. Ivan Ivanov".
Репозитории - это специальные модели, ответственные за хранение и извлечение сущностей.
Например, UserRepository
может представлять таблицу базы данных и предоставлять методы для
извлещения сущностей User
. Обычно вы используете репозитории для хранения сущностей в базе данных.
С помощью репозиториев вы можете инкапсулировать SQL-запросы в одно место и с легкостью
поддерживать их и тестировать.
Мы узнаем о репозиториях более детально в Управление БД с Doctrine, когда будем говорить о библиотеке Doctrine.
Объекты-значения - это тип модели, для которого идентификация не так важна, как для сущностей. Объект-значение - это обычно маленький класс, идентифицируемый всеми его свойствами. У него нет свойства идентификатора. Объекты-значения, как правило, имеют геттеры, но не имеют сеттеров (объекты-значения неизменны).
Например, модель, обрабатывающая сумму денег может быть объектом-значением:
class MoneyAmount
{
// Свойства
private $currency;
private $amount;
// Конструктор
public function __construct($amount, $currency='USD')
{
$this->amount = $amount;
$this->currency = $currency;
}
// Получаем код валюты
public function getCurrency()
{
return $this->currency;
}
// Получаем сумму денег
public function getAmount()
{
return $this->amount;
}
}
В строках 4-5 мы определяем два свойства: currency
и amount
. У модели нет
свойства идентификатора, вместо этого она идентифицируется всеми свойствами, как одним целым:
если вы поменяете либо currency
либо amount
, у вас будет другой объект суммы денег.
В строках 8-12 мы определяем метод конструктора, который инициализирует свойства.
В строках 15-24 мы определяем геттеры для свойств модели. Заметьте, что мы не определяем сеттеры (так как модель неизменна).
Модели-сервисы обычно инкапсулируют некоторую функциональность бизнес-логики.
Сервисы, как правило, имеют легко узнаваемые имена, заканчивающиеся на суффикс "er", например FileUploader
или UserManager
.
Ниже представлен образец сервиса Mailer
. У него есть метод sendMail()
, который
берет объект-значение EmailMessage
и отправляет электронное письмо, используя
стандартную PHP-функцию mail()
.
<?php
// Объект-значение и-мейла
class EmailMessage
{
private $recipient;
private $subject;
private $text;
// Конструктор
public function __construct($recipient, $subject, $text)
{
$this->recipient = $recipient;
$this->subject = $subject;
$this->text = $text;
}
// Геттеры
public function getRecipient()
{
return $this->recipient;
}
public function getSubject()
{
return $this->subject;
}
public function getText()
{
return $this->text;
}
}
// Сервис Mailer, который может отправлять сообщения по эл. почте
class Mailer
{
public function sendMail($message)
{
// Используем PHP-функцию mail() для отправки письма
if(!mail($message->getRecipient(), $message->getSubject(),
$message()->getText()))
{
// Ошибка отправки письма
return false;
}
return true;
}
}
В Zend Framework вы, как правило, регистрируете ваши сервисы в менеджере сервисов (Service Manager).
Фабрики обычно создаются для инстанцирования других моделей (а именно, моделей сервисов). В простейших случаях вы
можете создать экземпляр класса модели без фабрики, просто используя оператор new
, но иногда
алгоритм создания класса может быть довольно запутанным, и вы инкапсулируете его внутри класса фабрики.
Например, сервисы часто зависят друг от друга, так что вам может понадобиться внедрить зависимости в сервис. Также, иногда
сразу после инстанциации сервиса вам может понадобиться инициализировать его путем вызова определенного(ых) метода(ов).
Классы фабрик обычно имеют имена заканчивающиеся на 'Factory', такие как
СurrencyConverterFactory
, MailerFactory
, и т.д.
В качестве реального примера давайте представим себе сервис PurchaseManager
, который отвечает за логику покупки каких либо товаров,
и что сервис PurchaseManager
использует в своей работе другой сервис CurrencyConverter
, который может соединяться с внешней
системой, которая предоставляет курсы обмена валют. Давайте напишем фабрику для сервиса PurchaseManager
, которая бы инстанцировала сервис
и передавала бы ему зависимости:
<?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);
}
}
В коде, приведенном выше, у нас есть класс PurchaseManagerFactory
, который реализует интерфейс
Zend\ServiceManager\Factory\FactoryInterface
. У класса фабрики есть метод __invoke()
,
целью которого является инстанцирование объекта. У этого метода есть аргумент $container
,
который является менеджером сервисов. Вы можете использовать $container
, чтобы извлекать сервисы
из менеджера сервисов и передавать их создаваемому объекту.