En Zend Framework 3, como ya pudimos adivinar, no hay una sola carpeta Model
para guardar las clases de modelos. En cambio y por convención, los modelos
son subdivididos en los siguientes tipos principales que son guardados cada uno
en sus propias subcarpetas (ver tabla 4.9):
Tipo de Modelo | Carpeta |
---|---|
Entidades | APP_DIR/module/Application/src/Entity |
Repositorios | APP_DIR/module/Application/src/Repository |
Objetos con Valor (Value Objects) | APP_DIR/module/Application/src/ValueObject |
Servicios | APP_DIR/module/Application/src/Service |
Fábricas | Una subcarpeta Factory dentro de cada carpeta de tipo de modelo. Por ejemplo, las fábricas para los controladores se almecenarán en APP_DIR/module/Application/src/Controller/Factory |
Separar los modelos en diferentes tipos hace más fácil el diseño de nuestro dominio de reglas de negocio. Esto es también llamado "Diseño guiado por dominio" (o brevemente DDD). La persona que propuso DDD es Eric Evans en su famoso libro llamado Domain-Driven Design — Tackling Complexity in the Heart of Software.
Abajo describiremos los principales tipos de modelos.
Las Entidades se usan para guardar datos que siempre tienen una propiedad que
funciona como identificador de manera que podemos identificar de manera única los
datos. Por ejemplo, una entidad User
siempre tiene la propiedad única login
,
y mediante este atributo podemos identificar al usuario. Podemos cambiar los otros
atributos de la entidad, como primerNombre
o dirección
pero su identificador
nunca cambia. Las entidades son almacenadas usualmente en una base de datos,
en un archivo del sistema o en cualquier otro almacenamiento.
Abajo podemos encontrar un ejemplo de la entidad User
que representa a los
usuarios del sitio web:
// 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 function setLogin($login)
{
$this->login = $login;
}
//...
}
En las líneas 5-12 definimos las propiedades del modelo User
. La mejor práctica
es definir las propiedades usando el tipo de acceso privado y hacerlas disponibles
a través de los métodos públicos getter y setter (como getLogin()
y
setLogin(), etc).
El comportamiento de los métodos del modelo no se limitan a los getters y setters. Podemos crear otros métodos que manipulan los datos del modelo. Por ejemplo, podemos definir a conveniencia el método
getFullName()
que regresaría el nombre completo del usuario: "Mr. John Doe".
Los repositorios son modelos específicos responsables de guardar y recuperar
entidades. Por ejemplo, el UserRepository
puede representar una tabla de la
base de datos y proveer los métodos para recuperar las entidades User
. Generalmente
usamos los repositorios cuando guardamos entidades en la base de datos. Con los
repositorios podemos encapsular la lógica de la consulta SQL en un solo lugar,
y de fácil mantenimiento y, además, probarlos.
Aprenderemos sobre los repositorios con más detalles en Administración de la Base de Datos con Doctrine en donde hablaremos sobre la biblioteca Doctrine.
Los objetos con valor son un tipo de modelo en que no es importante la identidad como si lo es en las entidades. Un objeto con valor es usualmente una pequeña clase identificada por medio de todos su atributos. Él no tiene un atributo identificador. Los objetos con valor típicamente tienen métodos getter pero no tienen métodos setters (los objetos con valor son inmutables).
Por ejemplo, un modelo que maneja una cantidad de dinero puede ser tratado como un objeto con valor:
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;
}
}
En las líneas 4-5 definimos dos propiedades: currency
y amount
. El modelo
no tiene una propiedad como identificador único, en cambio su identidad se define
por medio de todas sus propiedades: si cambiamos la propiedad currency
o
amount
tendríamos diferentes objetos con diferentes cantidades de dinero.
En las líneas 8-12 definimos el método constructor que inicializa las propiedades.
En las líneas 15-24 definimos los métodos getter para las propiedades del modelo. Veamos que no tenemos métodos setter (el modelo es inmutable).
Los modelos de tipo servicio usualmente encapsulan alguna parte de las funcionalidades
de la lógica de negocio. Los servicios tienen nombres reconocibles fácilmente
por su terminación en "er", como FileUploader
o UserManager
.
Abajo un ejemplo del servicio Mailer
se presenta. Este tiene el método sendMail()
que toma el objeto EmailMessage
como valor y enviá un mensaje de correo electrónico
usando la función estándar mail()
de PHP:
<?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;
}
}
En Zend Framework registramos nuestros modelos de tipo servicio en el Administrador de Servicios.
Las fábricas se diseñan usualmente para instanciar otros modelos (particularmente
los modelos de tipo servicio). En los casos más simples podemos crear una instancia
sin una fábrica mediante el uso del operador new
pero a veces la lógica de
creación de clases debe ser más compleja. Por ejemplo, es común que los servicios
dependan de otros servicios, así necesitaremos inyectar dependencias a un servicio.
Además, puede ser necesario inicializar el servicio justo después de la instanciación
mediante el llamado de uno o varios de sus métodos.
Los nombres de las clases tienen típicamente nombres que terminan con el sufijo
Factory
tal como CurrencyConverterFactory
, MailerFactory
, etc.
Como un ejemplo de la vida real vamos a imaginar que tenemos el servicio PurchaseManager
,
que puede procesar las compras de determinados bienes y que el servicio PurchaseManager
usa otro servicio llamado CurrencyConverter
que se puede conectar a un servicio
externo que provee las tasas de cambio. Vamos a escribir una clase de tipo fábrica
para el PurchaseManager
que instanciará el servicio y lo pasará como dependencia:
<?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);
}
}
En el código de arriba tenemos la clase PurchaseManagerFactory
que implementa
la interface Zend\ServiceManager\Factory\FactoryInterface
. La clase de tipo
fábrica tiene el método __invoke()
cuyo objetivo es instanciar el objeto.
Este método tiene el argumento $container
que es el administrador de servicios.
Podemos usar la variable $container
para recuperar servicios del administrador
de servicios y pasarlos al método constructor del servicio que se está instanciando.