Cuando empecé a utilizar PHP, yo incluía bastante cantidad de lógica en el controller, entonces empecé a leer sobre refactoring y me topé con skinny controller fat models
, el código de mis controlllers se reducía bastante y la lógica estaba en los modelos, pero lo único que hacía era mover el problema de sitio. Ahora tenía modelos muy grandes. Cuando una action interactúa con más de un modelo, me encontraba en que no sabía en que model escribir esa lógica…
El código en los controller no puede reutilizarse, por lo que en algunos casos como en ‘pedidos/Order’ en los que hay que lidiar con los descuentos, los artículos del la orden, la dirección,… escribir toda lógica de este proceso en un controlador no es la solución ideal, ya que modificar toda esa lógica hará que tengamos código duplicado.
Así que lo mejor es abstraer toda la lógica del proceso en unas clases de servicio. Así estaba el controller antes de crear el servicio:
<?php
class OrdersController extends Controller {
public function actionCreate() {
try {
$orderData = Yii::app()->request->getParam('order');
if(empty($orderData['items'])) {
$this->_sendResponse(403, array(
'status' => 'error', 'message' => 'Can\'t save order without items'
));
}
$items = $orderData['items'];
unset($orderData['items']);
try {
$order = new Orders;
$orderTransaction = $order->dbConnection->beginTransaction();
if($order) {
$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);
$this->_sendResponse(200, array(
'status' => 'success', 'message' => 'Order placed successfully.'
));
}
$orderTransaction->rollback();
$this->_sendResponse(403, array(
'status' => 'error', 'message' => 'Order creation failed'
));
}
else {
$orderTransaction->rollback();
$this->_sendResponse(403, array(
'status' => 'error', 'errors' => $order->getErrors()
));
}
}
}
catch(Exception $e) {
$orderTransaction->rollback();
$this->_sendResponse(403, array(
'status' => 'error', 'message' => $e->getMessage()
));
}
}
catch(Exception $e) {
$this->_sendResponse(403, array(
'status' => 'error', 'message' => $e->getMessage()
));
}
}
public function sendMail($order_id) {
// logic to send email after placing an order successfully
}
?>
Toda la lógica y las excepciones están en el controlador y no es posible volver a utilizarla en otra acción. También tenemos un código muy difícil de probar, así que lo mejor es mudar a una clase OrderService
:
<?php
class OrdersService {
public function create($orderData) {
if(empty($orderData['items'])) {
throw new OrdersServiceException('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 array('status' => 'success');
}
$orderTransaction->rollback();
throw new OrdersServiceException("Failed to save the items.", 1);
}
else {
// handle validation errors
$orderTransaction->rollback();
return array('status' => 'error', 'errors' => $order->getErrors());
}
}
catch(Exception $e) {
$orderTransaction->rollback();
throw new Exception('Something wrong happened');
}
}
public function sendMail($order_id) {
// logic to send email after placing an order successfully
}
}
class OrdersException extends Exception {
}
?>
Ahora se está lanzando una excepción para todos los errores. Así que se puede capturar la excepción en cualquier lugar en el que utilicemos el servicio y mostrar los errores dependiendo de acción (un mensaje de error, un JSON,…)
Una vez que hemos mudado todo a la clase de servicio, tenemos un controlador como este:
<?php
class OrdersController extends Controller {
public function actionCreate() {
$orderData = Yii::app()->request->getParam('order');
try {
$order = new OrdersService();
$res = $order->create($orderData);
if(isset($res['status']) && $res['status'] == 'success') {
$res['message'] = 'Order placed successfully.';
$this->_sendResponse(200, $res);
}
$this->_sendResponse(403, $res);
}
catch(OrdersServiceException $e) {
$this->_sendResponse(403, array(
'status' => 'error', 'message' => $e->getMessage()
));
}
catch(Exception $e) {
$this->_sendResponse(403, array(
'status' => 'error', 'message' => $e->getMessage()
));
}
}
}
?>
Ahora el controller solo tiene el código necesario que necesita esta acción. No tenemos que reutilizar nada de esto, ya que la forma de mostrar el error depende de la action particular.
Referencias
Esta entrada esta basada en el artículo http://blog.revathskumar.com/2015/08/php-service-classes.html
Un comentario en “Skinny Controller: moviendo la lógica del controller”