Cómo Crear un Carrito de Compras Profesional en Laravel 13 desde Cero (Sin Paquetes)
En el desarrollo de aplicaciones e-commerce y plataformas de cursos online con Laravel 13, la tendencia de depender de paquetes externos para funciones esenciales ha quedado en el pasado. Aunque bibliotecas populares como hardevine/shoppingcart han servido durante años, crear tu propio sistema de carrito usando Services y Facades nativas ofrece un control total del código, mayor rendimiento y cero deuda técnica.
En este artículo aprenderás la arquitectura, diseño y lógica detrás de un sistema de carrito robusto, escalable y listo para producción.
¿Por qué crear tu propio Carrito en lugar de instalar un paquete?
- Cero Dependencias Obsoletas: No dependerás de mantenimientos de terceros cuando actualices la versión de Laravel o PHP.
- Lógica de Negocio Adaptada: Permite aplicar reglas específicas sin hackear paquetes (por ejemplo, restringir la compra a máximo 1 unidad por curso digital).
- Rendimiento Máximo: Sin clases adicionales ni sobrecarga en la sesión; solo la información que realmente necesitas.
- Arquitectura Limpia: Mantiene el código alineado al patrón de diseño Service-Repository.
Arquitectura del Sistema: Services, Facades y Sesión
Arquitectura del Sistema: Services, Facades y Sesión
Para que el carrito sea accesible desde cualquier punto de tu aplicación (Controladores, Middleware o Vistas Blade) sin acoplar código, la arquitectura se divide en tres capas:
[ Vista / Controlador ] ---> [ Cart Facade ] ---> [ CartService ] ---> [ Laravel Session / Cache ]
-
CartService: Clase PHP que contiene toda la lógica de negocio (agregar, eliminar, calcular subtotal/total).
-
Cart Facade: Acceso estático global que simplifica llamadas como
Cart::add($course)oCart::total(). -
Session Storage: Mecanismo donde se persisten temporalmente los ítems del usuario.
Paso 1: Implementación del CartService
Paso 1: Implementación del CartService
El servicio encapsula las reglas de negocio. Para productos digitales o cursos, una regla fundamental es impedir que un usuario agregue más de una vez el mismo ítem, evitando cantidades duplicadas e ilógicas.
<?php
namespace App\Services\Cart;
use Illuminate\Session\SessionManager;
use Illuminate\Support\Collection;
class Cart
{
protected SessionManager $session;
protected string $instance = 'default';
public function __construct(SessionManager $session)
{
$this->session = $session;
}
/**
* Cambiar instancia del carrito (default, wishlist, etc.)
*/
public function instance(?string $instance = null): self
{
$this->instance = $instance ?: 'default';
return $this;
}
/**
* Obtener la instancia actual
*/
public function currentInstance(): string
{
return $this->instance;
}
/**
* Agregar curso al carrito (Máximo 1 por curso)
*/
public function add(array $item): CartItem
{
// En cursos, 'qty' deja de ser obligatorio en el array de entrada
if (!isset($item['id'], $item['name'], $item['price'])) {
throw new \InvalidArgumentException('El item debe contener id, name y price.');
}
$content = $this->getContent();
// 1. Verificar si el curso ya existe en el carrito por su ID original
$existingItem = $content->firstWhere('id', $item['id']);
if ($existingItem) {
// Si ya existe, aseguramos que qty sea 1 y actualizamos opciones si aplica
$existingItem->qty = 1;
if (isset($item['options'])) {
$existingItem->options = $item['options'];
}
$this->session->put($this->getSessionKey(), $content);
return $existingItem;
}
// 2. Si es un curso nuevo en el carrito, creamos el CartItem con qty = 1
$cartItem = new CartItem(
$item['id'],
$item['name'],
(float) $item['price'],
1, // Fuerza siempre cantidad 1
$item['options'] ?? []
);
$content->put($cartItem->rowId, $cartItem);
$this->session->put($this->getSessionKey(), $content);
return $cartItem;
}
/**
* Actualizar item por rowId
*/
public function update(string $rowId, mixed $value): CartItem
{
$content = $this->getContent();
if (!$content->has($rowId)) {
throw new \RuntimeException("No se encontró el item con rowId: {$rowId}");
}
$cartItem = $content->get($rowId);
if (is_int($value) || is_float($value)) {
// Un curso no puede tener cantidad mayor a 1 ni menor a 1 (para borrar se usa remove)
$cartItem->qty = 1;
} elseif (is_array($value)) {
$cartItem->update($value);
$cartItem->qty = 1; // Asegurar que siga siendo 1 tras la actualización
if ($rowId !== $cartItem->rowId) {
$content->forget($rowId);
}
}
$content->put($cartItem->rowId, $cartItem);
$this->session->put($this->getSessionKey(), $content);
return $cartItem;
}
/**
* Eliminar item por rowId
*/
public function remove(string $rowId): void
{
$content = $this->getContent();
if (!$content->has($rowId)) {
throw new \RuntimeException("No se encontró el item con rowId: {$rowId}");
}
$content->forget($rowId);
$this->session->put($this->getSessionKey(), $content);
}
/**
* Obtener item por rowId
*/
public function get(string $rowId): ?CartItem
{
return $this->getContent()->get($rowId);
}
/**
* Obtener todo el contenido del carrito
*/
public function content(): Collection
{
return $this->getContent();
}
/**
* Destruir el carrito
*/
public function destroy(): void
{
$this->session->forget($this->getSessionKey());
}
/**
* Contar cursos únicos en el carrito
*/
public function count(): int
{
return $this->getContent()->count();
}
/**
* Cantidad total de productos (Equivalente a count() en cursos)
*/
public function countItems(): int
{
return $this->count();
}
/**
* Subtotal (sin impuestos)
*/
public function subtotal(): float
{
return $this->getContent()->sum(function ($item) {
if (is_array($item)) {
return $item['price'] ?? 0;
}
return method_exists($item, 'subtotal') ? $item->subtotal() : $item->price;
});
}
/**
* Total del carrito
*/
public function total(): float
{
return $this->getContent()->sum(function ($item) {
if (is_array($item)) {
return $item['price'] ?? 0;
}
return method_exists($item, 'total') ? $item->total() : $item->price;
});
}
/**
* Asociar modelo a un item
*/
public function associate(string $rowId, string $modelClass): CartItem
{
$cartItem = $this->get($rowId);
if (!$cartItem) {
throw new \RuntimeException("Item no encontrado: {$rowId}");
}
$cartItem->associate($modelClass);
$content = $this->getContent();
$content->put($rowId, $cartItem);
$this->session->put($this->getSessionKey(), $content);
return $cartItem;
}
/**
* Obtener contenido desde sesión e hidratar los items a objetos CartItem
*/
protected function getContent(): Collection
{
$content = $this->session->get($this->getSessionKey(), collect());
if (is_array($content)) {
$content = collect($content);
}
return $content->map(function ($item) {
if (is_array($item)) {
return new CartItem(
$item['id'] ?? null,
$item['name'] ?? '',
(float) ($item['price'] ?? 0),
1, // Se asegura que al hidratar la cantidad siempre sea 1
$item['options'] ?? []
);
}
$item->qty = 1;
return $item;
});
}
/**
* Clave de sesión
*/
protected function getSessionKey(): string
{
return 'cart.' . $this->instance;
}
}
Paso 2: Creación del Facade Personalizado
Paso 2: Creación del Facade Personalizado
Para utilizar sintaxis limpia en las vistas Blade y controladores (Cart::getContent()), crea la Facade:
<?php
namespace App\Facades;
use Illuminate\Support\Facades\Facade;
class Cart extends Facade
{
protected static function getFacadeAccessor(): string
{
return 'cart';
}
}
Luego, registra el servicio dentro del método register() en tu AppServiceProvider.php:
<?php
namespace App\Services\Cart;
use Illuminate\Support\ServiceProvider;
class CartServiceProvider extends ServiceProvider
{
public function register(): void
{
$this->app->singleton('cart', function ($app) {
return new Cart($app['session']);
});
}
public function boot(): void
{
//
}
}
Finalmente
Construir tu propio carrito de compras en Laravel 13 utilizando la combinación de Services y Facades no solo te da el control absoluto sobre la arquitectura de tu software, sino que te brinda una base limpia para escalar con pasarelas como PayPal, Stripe o flujos personalizados mediante WhatsApp.
Recurso del Tutorial
GratisTutoriales Recomendados
Sigue explorando publicaciones de la misma categoría