Skip to main content

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:

  • page y itemsPerPage para paginación
  • filters para filtrado
  • sortBy y sortDesc para 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 :id en 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

  1. Mantén los controladores ligeros: Delega la lógica de negocio al servicio.
  2. Utiliza tipos de respuesta adecuados: 200 para éxito, 400 para errores de cliente, etc.
  3. Valida siempre las entradas: Define reglas de validación para cada método.
  4. Estructura las respuestas de manera consistente: Mantén el mismo formato en todos tus endpoints.
  5. Documenta tus endpoints personalizados: Añade comentarios que expliquen el propósito.
  6. Sigue las convenciones REST: Usa los verbos HTTP apropiados para cada operación.
  7. Maneja errores explícitamente: Devuelve mensajes de error claros y útiles.
  8. 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.