Laravel

Cómo Crear un Carrito de Compras Profesional en Laravel 13 desde Cero (Sin Paquetes)

1 septiembre, 2026
34 lecturas
Cómo Crear un Carrito de Compras en Laravel 13 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

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) o Cart::total().

  • Session Storage: Mecanismo donde se persisten temporalmente los ítems del usuario.


Paso 1: Implementación del CartService

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

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

Gratis
11.77 KB 2 descargas

Tutoriales Recomendados

Sigue explorando publicaciones de la misma categoría

Ver todos