Hace unos días estuvimos viendo como hacer skinny controllers extrayendo toda la lógica de los controladores a servicios que pueden ser reutilizados. Después de leer el artículo original Paul M. Jones autor de «Modernizing Legacy application in PHP» escribió un post al respecto introduciendo el patrón Action-Domain-Responder.
Así que hoy vamos a traducir el artículo http://paul-m-jones.com/archives/6172 en el que Paul describe el trabajo que extra necesario para aplicar el patrón ADR al ejemplo anterior.
Utilizando el código del post anterior vamos a trabajar en los siguientes puntos:
- En la clase de servicio, en lugar de lanzar excepciones y a veces devolver arrays, nosotros siempre vamos a devolver instancias de Payload. Estos establecen explicitamente el resultado de la actividad del dominio («Entrada no valida», «order creada», «order no creada», «error»). Tener de manera explicita la información del estado en «Payload» significa que no necesitamos capturar excepciones en la acción del controlador, y que no es necesario examinar los objetos del dominio para interpretar lo que ocurrió en el dominio. La clase de servicio puede ahora decir explicitamente lo que pasó de una manera estandarizada.
- En la acción del controller, ahora como ya no necesitamos capturar las excepciones, podemos concentrarnos en un conjunto mucho más pequeño de lógica: obtener la entrada del usuario, pasarlo al dominio, recuperar el dominio de Payload y pasar el Payload al responder.
- Por último, introducimos un Responder, cuyo trabajo consiste en construir (y en este caso de envío) la respuesta. La lógica de respuesta termina simplificándose también.
El código modificado es más o menos como este:
- Controller
use Aura\Payload\Payload;
class OrdersController extends Controller {
public function actionCreate() {
$orderData = Yii::app()->request->getParam('order');
$order = new OrdersService();
$payload = $order->create($orderData);
$responder = new OrdersResponder();
$responder->sendCreateResponse($payload);
}
}
- Responder:
class OrdersResponder extends Responder {
public function sendCreateResponse($payload) {
$result = array('messages' => $payload->getMessages());
if ($payload->getStatus() === Payload::CREATED) {
$this->_sendResponse(200, $result);
} else {
$result['status'] = 'error';
$this->_sendResponse(403, $result);
}
}
}
- OrderService:
class OrdersService {
protected function newPayload($status) {
return new Payload($status);
}
public function create($orderData) {
if(empty($orderData['items'])) {
return $this->newPayload(Payload::NOT_VALID)
->setMessages([
"Order items can't be empty."
]);
}
$items = $orderData['items'];
unset($orderData['items']);
try {
$order = new Orders;
$orderTransaction = $order->dbConnection->beginTransaction();
$address = Addresses::createIfDidntExist($orderData);
unset($orderData['address']);
$orderData['address_id'] = $address->id;
$amount = 0;
foreach ($items as $key => $item) {
$amount += $item['total'];
}
$amount += $orderData['extra_charge'];
$orderData['amount'] = $amount;
$order->attributes = $orderData;
if($order->save()) {
if(OrderItems::batchSave($items, $order->id)) {
$orderTransaction->commit();
$this->sendMail($order->id);
return $this->newPayload(Payload::CREATED)
->setMessages([
"Order placed successfully."
]);
}
$orderTransaction->rollback();
return $this->newPayload(Payload::NOT_CREATED)
->setMessages([
"Failed to save the items."
]);
}
else {
// handle validation errors
$orderTransaction->rollback();
return $this->newPayload(Payload::ERROR)
->setMessages($order->getErrors());
}
}
catch(Exception $e) {
$orderTransaction->rollback();
return $this->newPayload(Payload::ERROR)
->setMessages([
"Something wrong happened"
]);
}
}
}
Ahora, todavía hay mucho que mejorar aquí. Por ejemplo, podríamos empezar a separar los métodos de action del controlador en sus propias clases individuales. Pero tendremos que ir dando pequeños pasos para llegar a esa refactorización.
Este pequeño conjunto de cambios nos da una mejor separación de las preocupaciones, especialmente en lo que se refiere a la presentación. Recordemos que la «presentación» en un entorno de «peticionesrespuesta» es toda las respuestas HTTP, no solo el cuerpo de las respuestas. Los cambios anteriores hacen que las cabeceras HTTP y la presentación ya no se mezclen en el controlador, ahora son manejadas por el objeto Responder.