4. Controladores y endpoints
4.1. Estructura básica de un controlador
Los controladores son responsables de gestionar las peticiones HTTP, aplicar validaciones y delegar la lógica de negocio a los servicios. En nuestro framework, los controladores siguen una estructura estándar:
namespace ApiLayer\Domains\Products;
use App\Base\DomainController;
use ApiLayer\Domains\Products\ProductsService;
class ProductsController extends DomainController
{
protected $productsService;
public function __construct(ProductsService $service)
{
parent::__construct($service);
// Definición de reglas de validación
$this->validations = [
'headers' => ['x-tienda' => 'required|string'],
'body' => [
'name' => 'required|string',
'code' => 'required|string'
],
];
}
// Métodos adicionales personalizados...
}
Aspectos importantes:
- Todos los controladores de dominio deben extender
DomainController - El servicio se inyecta a través del constructor
- Las validaciones se definen en el array
$validations - Los métodos estándar (index, show, store, update, delete) son heredados de la clase base
4.2. Métodos estándar (index, show, store, update, delete)
La clase base DomainController ya implementa los 5 métodos estándar para operaciones CRUD:
index
Lista todos los recursos con filtrado y paginación:
public function index(Request $request)
{
$collection = $this->service->index($request);
return response()->json($collection, 200);
}
Este método acepta parámetros como:
pageyitemsPerPagepara paginaciónfilterspara filtradosortByysortDescpara ordenación
show
Muestra un recurso específico por su ID:
public function show($id)
{
$resource = $this->service->selectById($id);
if ($resource === null) {
return response()->json(['error' => 'Resource not found'], 404);
}
return response()->json($resource, 200);
}
store
Crea un nuevo recurso:
public function store(Request $request)
{
// Validación de datos según reglas definidas en $this->validations
ValidaRequest::make($request)
->validateHeaders($this->validations['headers'])
->validateBody($this->validations['body'])
->action('store')
->handle();
$response = $this->service->store($request->all());
return response()->json($response, 200);
}
update
Actualiza un recurso existente:
public function update($id, Request $request)
{
// Validación con context 'update'
ValidaRequest::make($request)
->validateHeaders($this->validations['headers'])
->validateBody($this->validations['body'])
->action('update')
->handle();
$response = $this->service->update($id, $request->all());
return response()->json($response, 200);
}
delete
Elimina un recurso:
public function delete($id)
{
$item = $this->service->delete($id);
return response()->json($item, 200);
}
4.3. Añadiendo nuevos métodos personalizados
Para añadir métodos personalizados, simplemente define nuevos métodos en tu controlador:
class ProductsController extends DomainController
{
// Constructor y configuración estándar...
// Método personalizado para habilitar/deshabilitar un producto
public function toggleStatus($id, Request $request)
{
// Validación específica para este método
ValidaRequest::make($request)
->validateHeaders(['x-tienda' => 'required|string'])
->validateBody(['active' => 'required|boolean'])
->action('update')
->handle();
// Delegar al servicio
$result = $this->productsService->toggleStatus($id, $request->active);
return response()->json($result, 200);
}
// Método para obtener productos destacados
public function featured(Request $request)
{
$result = $this->productsService->getFeaturedProducts($request->all());
return response()->json($result, 200);
}
}
4.4. Cómo configurar rutas para nuevos métodos
Una vez que has añadido métodos personalizados a tu controlador, necesitas configurar las rutas correspondientes en el archivo de rutas:
// src/ApiLayer/Domains/Products/ProductsRoutes.php
namespace ApiLayer\Domains\Products;
use App\Core\Router\Route;
use ApiLayer\Domains\Products\ProductsController;
use App\Middlewares\AuthMiddleware;
$middlewares = ['before' => [AuthMiddleware::class]];
Route::group(ProductsController::class, function () use ($middlewares) {
// Rutas estándar
Route::get('products/', 'index', $middlewares);
Route::get('products/:id', 'show', $middlewares);
Route::post('products/', 'store', $middlewares);
Route::put('products/:id', 'update', $middlewares);
Route::delete('products/:id', 'delete', $middlewares);
// Rutas personalizadas
Route::put('products/:id/toggle-status', 'toggleStatus', $middlewares);
Route::get('products/featured', 'featured', $middlewares);
});
Notas importantes:
- Mantén las rutas RESTful y descriptivas
- Para acciones sobre un recurso específico, incluye el
:iden la ruta - Las rutas se definen con el verbo HTTP adecuado (GET, POST, PUT, DELETE)
- Puedes aplicar middlewares específicos a rutas individuales
4.5. Devolviendo diferentes tipos de respuestas
El framework proporciona un helper response() que facilita el envío de respuestas HTTP con diferentes formatos:
Respuesta JSON estándar
return response()->json($data, 200);
Respuesta de error
return response()->json([
'error' => 'Producto no encontrado',
'code' => 'PRODUCT_NOT_FOUND'
], 404);
Respuesta sin contenido
return response()->json(null, 204);
Respuestas con cabeceras personalizadas
return response()
->json($data, 200)
->headers->set('X-Custom-Header', 'Value');
Respuesta de redirección
header('Location: /new-url');
exit;
Ejemplo completo: Controlador extendido
Veamos un ejemplo completo de un controlador de productos con métodos personalizados:
namespace ApiLayer\Domains\Products;
use App\Base\DomainController;
use App\Core\RequestFactory as Request;
use function App\Helpers\response;
use App\Validations\ValidaRequest;
class ProductsController extends DomainController
{
protected $productsService;
public function __construct(ProductsService $service)
{
parent::__construct($service);
$this->validations = [
'headers' => ['x-tienda' => 'required|string'],
'body' => [
'name' => 'required|string|max:100',
'code' => 'required|string|max:20',
'price' => 'required|numeric',
'categoryId' => 'required|numeric',
'description' => 'string'
],
];
}
// Método personalizado para buscar productos por término
public function search(Request $request)
{
ValidaRequest::make($request)
->validateHeaders(['x-tienda' => 'required|string'])
->validateBody(['term' => 'required|string|min:3'])
->action('store')
->handle();
$results = $this->productsService->searchProducts($request->term);
return response()->json([
'results' => $results,
'count' => count($results),
'term' => $request->term
], 200);
}
// Método para obtener productos por categoría
public function byCategory($categoryId, Request $request)
{
$products = $this->productsService->getProductsByCategory($categoryId);
if (empty($products)) {
return response()->json([
'message' => 'No products found in this category',
'categoryId' => $categoryId
], 200);
}
return response()->json([
'categoryId' => $categoryId,
'products' => $products,
'total' => count($products)
], 200);
}
// Método para cambiar el precio de un producto
public function updatePrice($id, Request $request)
{
ValidaRequest::make($request)
->validateHeaders(['x-tienda' => 'required|string'])
->validateBody([
'price' => 'required|numeric|min:0',
'reason' => 'required|string'
])
->action('update')
->handle();
try {
$result = $this->productsService->updateProductPrice(
$id,
$request->price,
$request->reason
);
return response()->json([
'message' => 'Price updated successfully',
'product' => $result
], 200);
} catch (\Exception $e) {
return response()->json([
'error' => 'Failed to update price',
'message' => $e->getMessage()
], 400);
}
}
// Sobrescribir método estándar para añadir lógica personalizada
public function delete($id)
{
// Verificar si el producto puede ser eliminado
if (!$this->productsService->canBeDeleted($id)) {
return response()->json([
'error' => 'Cannot delete product',
'message' => 'This product has associated orders and cannot be deleted'
], 409); // Conflict
}
// Proceder con la eliminación estándar
return parent::delete($id);
}
}
Y las rutas correspondientes:
// src/ApiLayer/Domains/Products/ProductsRoutes.php
$middlewares = ['before' => [AuthMiddleware::class]];
Route::group(ProductsController::class, function () use ($middlewares) {
// Rutas estándar
Route::get('products/', 'index', $middlewares);
Route::get('products/:id', 'show', $middlewares);
Route::post('products/', 'store', $middlewares);
Route::put('products/:id', 'update', $middlewares);
Route::delete('products/:id', 'delete', $middlewares);
// Rutas personalizadas
Route::post('products/search', 'search', $middlewares);
Route::get('products/category/:categoryId', 'byCategory', $middlewares);
Route::patch('products/:id/price', 'updatePrice', $middlewares);
});
Buenas prácticas en controladores
- Mantén los controladores ligeros: Delega la lógica de negocio al servicio.
- Utiliza tipos de respuesta adecuados: 200 para éxito, 400 para errores de cliente, etc.
- Valida siempre las entradas: Define reglas de validación para cada método.
- Estructura las respuestas de manera consistente: Mantén el mismo formato en todos tus endpoints.
- Documenta tus endpoints personalizados: Añade comentarios que expliquen el propósito.
- Sigue las convenciones REST: Usa los verbos HTTP apropiados para cada operación.
- Maneja errores explícitamente: Devuelve mensajes de error claros y útiles.
- Agrupa rutas relacionadas: Coloca las rutas relacionadas juntas en el archivo de rutas.
Siguiendo estas pautas, podrás crear controladores limpios, mantenibles y eficaces que gestionan correctamente las interacciones entre los clientes y tu aplicación.