<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>Laravel / PHP &#8211; Blog Maria Tech</title>
	<atom:link href="https://www.mariatech.com.mx/blog/categoria/laravel-php/feed/" rel="self" type="application/rss+xml" />
	<link>https://www.mariatech.com.mx/blog</link>
	<description>Desarrollo web, tecnologías de la información y marketing digital</description>
	<lastBuildDate>Tue, 14 Jul 2026 23:15:07 +0000</lastBuildDate>
	<language>es</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=7.0.2</generator>

<image>
	<url>https://www.mariatech.com.mx/blog/wp-content/uploads/2024/05/cropped-icon_wordpress-32x32.png</url>
	<title>Laravel / PHP &#8211; Blog Maria Tech</title>
	<link>https://www.mariatech.com.mx/blog</link>
	<width>32</width>
	<height>32</height>
</image> 
	<item>
		<title>Arquitectura de un proyecto de Laravel</title>
		<link>https://www.mariatech.com.mx/blog/laravel-php/arquitectura-de-un-proyecto-de-laravel/</link>
					<comments>https://www.mariatech.com.mx/blog/laravel-php/arquitectura-de-un-proyecto-de-laravel/#respond</comments>
		
		<dc:creator><![CDATA[Victor Santillan]]></dc:creator>
		<pubDate>Tue, 14 Jul 2026 23:15:07 +0000</pubDate>
				<category><![CDATA[Laravel / PHP]]></category>
		<guid isPermaLink="false">https://www.mariatech.com.mx/blog/?p=311</guid>

					<description><![CDATA[Conoce una buena práctica para separar tu código en proyectos de Laravel...]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">Cuando un proyecto Laravel empieza a crecer, una de las primeras señales de desorden aparece en los controladores: métodos cada vez más largos, validaciones mezcladas con consultas, reglas de negocio dentro del mismo archivo y operaciones repetidas en varias partes del sistema.</p>



<p class="wp-block-paragraph">Al inicio puede parecer práctico resolver todo dentro del controller, pero con el tiempo esa decisión vuelve más difícil mantener, probar y escalar el proyecto. Por eso conviene separar responsabilidades entre <strong>Controllers</strong>, <strong>Form Requests</strong>, <strong>Services</strong> y <strong>Models</strong>.</p>



<p class="wp-block-paragraph">La idea no es complicar la arquitectura, sino poner cada pieza en el lugar correcto.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">Resumen rápido</h2>



<p class="wp-block-paragraph">Una forma sencilla de entenderlo es esta:</p>



<figure class="wp-block-table"><table><thead><tr><th>Elemento</th><th>Responsabilidad principal</th></tr></thead><tbody><tr><td>Controller</td><td>Controla el flujo HTTP: recibe la petición, llama lo necesario y responde.</td></tr><tr><td>Form Request</td><td>Valida y prepara los datos antes de llegar al controller.</td></tr><tr><td>Service</td><td>Ejecuta la lógica de negocio o procesos importantes del sistema.</td></tr><tr><td>Model</td><td>Representa una tabla, relaciones, atributos, scopes y comportamiento simple de la entidad.</td></tr></tbody></table></figure>



<p class="wp-block-paragraph">En otras palabras:</p>



<pre class="wp-block-code"><code>Request
  ↓
Form Request
  ↓
Controller
  ↓
Service
  ↓
Model / Base de datos
  ↓
Response</code></pre>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">Qué es un Controller en Laravel</h2>



<p class="wp-block-paragraph">Un controller agrupa la lógica que responde a peticiones HTTP relacionadas con un recurso. Laravel recomienda usarlos para organizar el manejo de solicitudes que, de otra forma, terminarían como closures dentro de las rutas.</p>



<p class="wp-block-paragraph">El controller debe actuar como coordinador. No debería cargar toda la lógica de negocio del sistema.</p>



<h3 class="wp-block-heading">Responsabilidades recomendadas de un controller</h3>



<ul class="wp-block-list">
<li>Recibir la petición.</li>



<li>Usar un Form Request para validar.</li>



<li>Buscar un modelo cuando sea algo simple.</li>



<li>Llamar un Service cuando haya lógica de negocio.</li>



<li>Regresar una vista, redirect o respuesta JSON.</li>
</ul>



<h3 class="wp-block-heading">Ejemplo de controller limpio</h3>



<pre class="wp-block-code"><code>public function update(UpdateProductRequest $request, string $product)
{
    $product = Product::find($product);

    $product = $this-&gt;productService-&gt;update(
        $product,
        $request-&gt;validated()
    );

    return response()-&gt;json(&#91;
        'success' =&gt; true,
        'message' =&gt; 'Producto actualizado correctamente.',
        'product' =&gt; &#91;
            'id' =&gt; $product-&gt;id,
            'name' =&gt; $product-&gt;name,
        ],
    ]);
}</code></pre>



<p class="wp-block-paragraph">Este controller no sabe todos los detalles de cómo se actualiza el producto. Solo controla el flujo.</p>



<p class="wp-block-paragraph">Eso lo hace más fácil de leer, mantener y modificar.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">Qué es un Form Request</h2>



<p class="wp-block-paragraph">Un Form Request es una clase especializada para validar y autorizar datos de entrada. Laravel permite crear estos archivos para encapsular validación y autorización en un lugar separado del controller.</p>



<p class="wp-block-paragraph">Esto evita que el controller se llene de reglas como <code>required</code>, <code>exists</code>, <code>max</code>, <code>email</code>, validaciones condicionales o preparación de datos.</p>



<h3 class="wp-block-heading">Responsabilidades recomendadas de un Form Request</h3>



<ul class="wp-block-list">
<li>Validar campos requeridos.</li>



<li>Validar formatos.</li>



<li>Validar existencia de registros en base de datos.</li>



<li>Preparar datos antes de validar.</li>



<li>Autorizar si el usuario puede ejecutar la acción.</li>
</ul>



<h3 class="wp-block-heading">Ejemplo de Form Request</h3>



<pre class="wp-block-code"><code>class UpdateProductRequest extends FormRequest
{
    public function rules(): array
    {
        return &#91;
            'product_id' =&gt; &#91;'required', 'integer', 'exists:products,id'],
            'name' =&gt; &#91;'required', 'string', 'max:255'],
            'price' =&gt; &#91;'required', 'numeric', 'min:0'],
        ];
    }

    public function authorize(): bool
    {
        return true;
    }
}</code></pre>



<p class="wp-block-paragraph">El controller ya no necesita preocuparse por esa preparación.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">Qué es un Service</h2>



<p class="wp-block-paragraph">Un Service es una clase donde se concentra la lógica de negocio o los procesos importantes del sistema. Aunque Laravel no obliga a crear una carpeta <code>Services</code>, el framework sí facilita este patrón gracias a su Service Container y a la inyección de dependencias.</p>



<p class="wp-block-paragraph">La pregunta clave para decidir si algo va a un Service es:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">¿Esto solo controla una petición HTTP o representa una operación importante del negocio?</p>
</blockquote>



<p class="wp-block-paragraph">Si representa una operación del negocio, normalmente conviene moverlo a un Service.</p>



<h3 class="wp-block-heading">Responsabilidades recomendadas de un Service</h3>



<ul class="wp-block-list">
<li>Crear registros.</li>



<li>Actualizar registros.</li>



<li>Eliminar registros.</li>



<li>Ejecutar transacciones.</li>



<li>Aplicar reglas de negocio.</li>



<li>Coordinar varias tablas.</li>



<li>Calcular importes, descuentos, comisiones o estados.</li>



<li>Consumir APIs externas.</li>



<li>Disparar notificaciones, eventos o procesos relacionados.</li>
</ul>



<h3 class="wp-block-heading">Ejemplo de Service</h3>



<pre class="wp-block-code"><code>class ProductService
{
    public function create(array $data): Product
    {
        return DB::transaction(function () use ($data) {
            return Product::create(&#91;
                'name' =&gt; $data&#91;'name'],
                'description' =&gt; $data&#91;'description'] ?? null,
                'price' =&gt; $data&#91;'price'],
                'enabled' =&gt; $data&#91;'enabled'] ?? true,
            ]);
        });
    }

    public function update(Product $product, array $data): Product
    {
        return DB::transaction(function () use ($product, $data) {
            $product-&gt;name = $data&#91;'name'];
            $product-&gt;description = $data&#91;'description'] ?? null;
            $product-&gt;price = $data&#91;'price'];
            $product-&gt;enabled = $data&#91;'enabled'] ?? $product-&gt;enabled;
            $product-&gt;save();

            return $product-&gt;fresh();
        });
    }

    public function delete(Product $product): void
    {
        DB::transaction(function () use ($product) {
            $product-&gt;delete();
        });
    }
}</code></pre>



<p class="wp-block-paragraph">Aunque algunos métodos parezcan simples al inicio, tenerlos en un Service permite que crezcan ordenadamente. Hoy un <code>delete()</code> puede borrar un registro; mañana puede validar permisos internos, eliminar relaciones, registrar historial y disparar una notificación.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">Qué es un Model en Laravel</h2>



<p class="wp-block-paragraph">Un Model representa una tabla de base de datos dentro de Eloquent. Laravel indica que cada tabla suele tener un modelo correspondiente, y que estos modelos permiten consultar, insertar, actualizar y eliminar registros.</p>



<p class="wp-block-paragraph">Pero eso no significa que toda la lógica de negocio deba vivir dentro del Model.</p>



<h3 class="wp-block-heading">Responsabilidades recomendadas de un Model</h3>



<ul class="wp-block-list">
<li>Definir la tabla si no sigue la convención.</li>



<li>Definir campos <code>fillable</code> o <code>guarded</code>.</li>



<li>Definir relaciones.</li>



<li>Definir casts.</li>



<li>Crear accessors y mutators.</li>



<li>Crear scopes reutilizables.</li>



<li>Agregar métodos simples propios de la entidad.</li>
</ul>



<h3 class="wp-block-heading">Ejemplo de Model</h3>



<pre class="wp-block-code"><code>class Product extends Model
{
    protected $fillable = &#91;
        'name',
        'description',
        'price',
        'enabled',
    ];

    protected function casts(): array
    {
        return &#91;
            'price' =&gt; 'decimal:2',
            'enabled' =&gt; 'boolean',
        ];
    }

    public function scopeEnabled($query)
    {
        return $query-&gt;where('enabled', true);
    }

    public function isAvailable(): bool
    {
        return $this-&gt;enabled === true;
    }
}</code></pre>



<p class="wp-block-paragraph">Este tipo de lógica sí pertenece al modelo porque describe el comportamiento directo de la entidad <code>Product</code>.</p>



<p class="wp-block-paragraph">En cambio, si el proceso implica cobrar, registrar historial, enviar notificaciones y actualizar varias tablas, eso ya debería ir a un Service.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">Cuándo usar cada uno</h2>



<h3 class="wp-block-heading">Usa Controller cuando&#8230;</h3>



<ul class="wp-block-list">
<li>Necesitas recibir una petición HTTP.</li>



<li>Necesitas devolver una vista.</li>



<li>Necesitas devolver JSON.</li>



<li>Necesitas redireccionar.</li>



<li>La acción solo coordina otras piezas.</li>
</ul>



<pre class="wp-block-code"><code>public function index()
{
    $products = Product::enabled()
        -&gt;orderBy('name')
        -&gt;paginate(20);

    return view('products.index', compact('products'));
}</code></pre>



<p class="wp-block-paragraph">Una consulta sencilla para mostrar datos puede quedarse en el controller.</p>



<h3 class="wp-block-heading">Usa Form Request cuando&#8230;</h3>



<ul class="wp-block-list">
<li>Hay reglas de validación.</li>



<li>Hay autorización de la acción.</li>



<li>Necesitas preparar datos antes de validarlos.</li>



<li>Quieres evitar que el controller se llene de reglas.</li>
</ul>



<pre class="wp-block-code"><code>public function rules(): array
{
    return &#91;
        'name' =&gt; &#91;'required', 'string', 'max:255'],
        'email' =&gt; &#91;'required', 'email'],
    ];
}</code></pre>



<h3 class="wp-block-heading">Usa Service cuando&#8230;</h3>



<ul class="wp-block-list">
<li>La acción crea, actualiza o elimina información importante.</li>



<li>La acción toca varias tablas.</li>



<li>La lógica se reutilizará desde varios lugares.</li>



<li>Hay transacciones.</li>



<li>Hay integración con APIs externas.</li>



<li>Hay reglas de negocio.</li>
</ul>



<pre class="wp-block-code"><code>$order = $this-&gt;orderService-&gt;create($request-&gt;validated());</code></pre>



<h3 class="wp-block-heading">Usa Model cuando&#8230;</h3>



<ul class="wp-block-list">
<li>Necesitas definir relaciones.</li>



<li>Necesitas casts.</li>



<li>Necesitas scopes.</li>



<li>Necesitas accessors o mutators.</li>



<li>Necesitas métodos simples de la entidad.</li>
</ul>



<pre class="wp-block-code"><code>public function scopeEnabled($query)
{
    return $query-&gt;where('enabled', true);
}</code></pre>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">Ejemplo completo: creación de una orden</h2>



<p class="wp-block-paragraph">Supongamos que tenemos un sistema donde un cliente puede crear una orden. La orden debe guardar productos, calcular total, registrar pagos pendientes y devolver una respuesta.</p>



<p class="wp-block-paragraph">Si todo se mete en el controller, el método puede volverse enorme. Una mejor separación sería:</p>



<h3 class="wp-block-heading">Ruta</h3>



<pre class="wp-block-code"><code>Route::post('/orders', &#91;OrderController::class, 'store'])
    -&gt;name('orders.store');</code></pre>



<h3 class="wp-block-heading">Controller</h3>



<pre class="wp-block-code"><code>class OrderController extends Controller
{
    public function __construct(
        private readonly OrderService $orderService
    ) {}

    public function store(StoreOrderRequest $request)
    {
        $order = $this-&gt;orderService-&gt;create($request-&gt;validated());

        return response()-&gt;json(&#91;
            'success' =&gt; true,
            'message' =&gt; 'Orden creada correctamente.',
            'order' =&gt; &#91;
                'id' =&gt; $order-&gt;id,
                'total' =&gt; $order-&gt;total,
            ],
        ]);
    }
}</code></pre>



<h3 class="wp-block-heading">Form Request</h3>



<pre class="wp-block-code"><code>class StoreOrderRequest extends FormRequest
{
    public function rules(): array
    {
        return &#91;
            'customer_name' =&gt; &#91;'required', 'string', 'max:255'],
            'items' =&gt; &#91;'required', 'array', 'min:1'],
            'items.*.product_id' =&gt; &#91;'required', 'integer', 'exists:products,id'],
            'items.*.quantity' =&gt; &#91;'required', 'integer', 'min:1'],
        ];
    }
}</code></pre>



<h3 class="wp-block-heading">Service</h3>



<pre class="wp-block-code"><code>class OrderService
{
    public function create(array $data): Order
    {
        return DB::transaction(function () use ($data) {
            $order = Order::create(&#91;
                'customer_name' =&gt; $data&#91;'customer_name'],
                'total' =&gt; 0,
                'status' =&gt; 'pending',
            ]);

            $total = 0;

            foreach ($data&#91;'items'] as $item) {
                $product = Product::findOrFail($item&#91;'product_id']);
                $subtotal = $product-&gt;price * $item&#91;'quantity'];

                $order-&gt;items()-&gt;create(&#91;
                    'product_id' =&gt; $product-&gt;id,
                    'quantity' =&gt; $item&#91;'quantity'],
                    'unit_price' =&gt; $product-&gt;price,
                    'subtotal' =&gt; $subtotal,
                ]);

                $total += $subtotal;
            }

            $order-&gt;update(&#91;
                'total' =&gt; $total,
            ]);

            return $order-&gt;fresh();
        });
    }
}</code></pre>



<h3 class="wp-block-heading">Model</h3>



<pre class="wp-block-code"><code>class Order extends Model
{
    protected $fillable = &#91;
        'customer_name',
        'total',
        'status',
    ];

    protected function casts(): array
    {
        return &#91;
            'total' =&gt; 'decimal:2',
        ];
    }

    public function items()
    {
        return $this-&gt;hasMany(OrderItem::class);
    }

    public function isPending(): bool
    {
        return $this-&gt;status === 'pending';
    }
}</code></pre>



<p class="wp-block-paragraph">Con esta estructura, cada clase tiene una responsabilidad clara.</p>



<p class="wp-block-paragraph">El controller no calcula totales. El Form Request no guarda datos. El Service no genera respuestas HTTP. El Model no se encarga de coordinar todo el proceso.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">Errores comunes al separar responsabilidades</h2>



<h3 class="wp-block-heading">1. Crear Services para todo</h3>



<p class="wp-block-paragraph">No todas las acciones necesitan un Service. Si solo vas a mostrar una vista con una consulta sencilla, el controller puede hacerlo.</p>



<h3 class="wp-block-heading">2. Meter validaciones en el Service</h3>



<p class="wp-block-paragraph">La validación de entrada normalmente debe vivir en un Form Request. El Service debe recibir datos ya validados y ejecutar el proceso.</p>



<h3 class="wp-block-heading">3. Regresar respuestas HTTP desde el Service</h3>



<p class="wp-block-paragraph">El Service no debería regresar <code>response()-&gt;json()</code> ni redirects. Eso le corresponde al controller.</p>



<h3 class="wp-block-heading">4. Convertir el Model en una clase gigante</h3>



<p class="wp-block-paragraph">El Model debe representar la entidad, pero no coordinar procesos completos del sistema. Si una operación toca varias entidades o APIs externas, probablemente debe vivir en un Service.</p>



<h3 class="wp-block-heading">5. No usar transacciones en procesos críticos</h3>



<p class="wp-block-paragraph">Cuando una operación crea o actualiza varios registros relacionados, conviene usar transacciones para evitar datos incompletos si algo falla a mitad del proceso.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">Regla práctica para proyectos Laravel</h2>



<p class="wp-block-paragraph">Una convención sencilla para mantener el orden es esta:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">Si una acción crea, actualiza o elimina información importante de la base de datos, o representa un proceso del negocio, la lógica principal debe ir en un Service.</p>
</blockquote>



<p class="wp-block-paragraph">Esto no significa que el controller nunca pueda consultar modelos. Para listados simples, vistas de edición o pantallas informativas, puede ser totalmente válido.</p>



<p class="wp-block-paragraph">Pero cuando el método empieza a tener transacciones, ciclos, cálculos, integraciones, actualizaciones en varias tablas o condiciones del negocio, es momento de mover esa lógica a un Service.</p>



<h3 class="wp-block-heading">Guía rápida</h3>



<figure class="wp-block-table"><table><thead><tr><th>Situación</th><th>Dónde colocarlo</th></tr></thead><tbody><tr><td>Mostrar una lista simple</td><td>Controller</td></tr><tr><td>Validar campos de formulario</td><td>Form Request</td></tr><tr><td>Crear un registro con reglas internas</td><td>Service</td></tr><tr><td>Actualizar varias tablas</td><td>Service</td></tr><tr><td>Consumir una API externa</td><td>Service</td></tr><tr><td>Definir relación belongsTo</td><td>Model</td></tr><tr><td>Crear accessor o cast</td><td>Model</td></tr><tr><td>Regresar JSON</td><td>Controller</td></tr><tr><td>Redireccionar a otra ruta</td><td>Controller</td></tr></tbody></table></figure>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">Conclusión</h2>



<p class="wp-block-paragraph">Separar Controllers, Form Requests, Services y Models no es solo una cuestión de estilo. Es una forma de mantener proyectos Laravel más claros, escalables y fáciles de modificar.</p>



<p class="wp-block-paragraph">Un controller debe controlar el flujo de la petición. Un Form Request debe validar y preparar datos. Un Service debe ejecutar la lógica de negocio. Un Model debe representar la entidad y su relación con la base de datos.</p>



<p class="wp-block-paragraph">La meta no es crear más archivos sin razón, sino evitar que una sola clase tenga demasiadas responsabilidades.</p>



<p class="wp-block-paragraph">Cuando cada pieza cumple su función, el código se vuelve más fácil de leer, probar y mantener.</p>



<h2 class="wp-block-heading">Fuentes consultadas</h2>



<ul class="wp-block-list">
<li><a href="https://laravel.com/docs/13.x/controllers" target="_blank" rel="noreferrer noopener">Laravel Documentation: Controllers</a></li>



<li><a href="https://laravel.com/docs/13.x/validation#form-request-validation" target="_blank" rel="noreferrer noopener">Laravel Documentation: Form Request Validation</a></li>



<li><a href="https://laravel.com/docs/13.x/validation#preparing-input-for-validation" target="_blank" rel="noreferrer noopener">Laravel Documentation: Preparing Input for Validation</a></li>



<li><a href="https://laravel.com/docs/13.x/container" target="_blank" rel="noreferrer noopener">Laravel Documentation: Service Container</a></li>



<li><a href="https://laravel.com/docs/13.x/eloquent" target="_blank" rel="noreferrer noopener">Laravel Documentation: Eloquent ORM</a></li>
</ul>



<p class="wp-block-paragraph"></p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.mariatech.com.mx/blog/laravel-php/arquitectura-de-un-proyecto-de-laravel/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>GET vs POST: cuándo usar cada método en desarrollo web</title>
		<link>https://www.mariatech.com.mx/blog/laravel-php/get-vs-post-cuando-usar-cada-metodo-en-desarrollo-web/</link>
					<comments>https://www.mariatech.com.mx/blog/laravel-php/get-vs-post-cuando-usar-cada-metodo-en-desarrollo-web/#respond</comments>
		
		<dc:creator><![CDATA[Victor Santillan]]></dc:creator>
		<pubDate>Sun, 12 Jul 2026 00:34:29 +0000</pubDate>
				<category><![CDATA[Laravel / PHP]]></category>
		<guid isPermaLink="false">https://www.mariatech.com.mx/blog/?p=307</guid>

					<description><![CDATA[En este artículo veremos cuándo usar el método GET o POST y porque en el desarrollo web...]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">Cuando desarrollamos una aplicación web, una de las decisiones más comunes es elegir qué método HTTP utilizar para una ruta: <code>GET</code> o <code>POST</code>. A simple vista puede parecer una decisión menor, sobre todo cuando no estamos enviando parámetros, pero elegir correctamente el método ayuda a mejorar la seguridad, mantener una arquitectura más clara y evitar comportamientos inesperados.</p>



<p class="wp-block-paragraph">En este artículo explicaremos de forma práctica cuándo usar <code>GET</code>, cuándo usar <code>POST</code> y por qué no todas las acciones deberían ejecutarse desde un simple enlace.</p>



<h2 class="wp-block-heading">La regla básica</h2>



<p class="wp-block-paragraph">Una forma sencilla de verlo es la siguiente:</p>



<pre class="wp-block-preformatted">GET  → consultar, mostrar o leer información.
POST → enviar información, ejecutar acciones o modificar el estado del sistema.</pre>



<p class="wp-block-paragraph">Esta diferencia es importante porque no todos los accesos a una URL deberían tener consecuencias dentro del sistema. Una ruta que solamente muestra información puede trabajar con <code>GET</code>. En cambio, una ruta que guarda, actualiza, elimina o dispara un proceso debería utilizar <code>POST</code> u otro método apropiado.</p>



<h2 class="wp-block-heading">Cuándo usar GET</h2>



<p class="wp-block-paragraph">El método <code>GET</code> se utiliza principalmente para consultar información. Es decir, cuando el usuario entra a una URL y espera ver datos, una página, un listado o un formulario, pero sin modificar nada en el sistema.</p>



<p class="wp-block-paragraph">Ejemplos comunes de rutas que sí pueden usar <code>GET</code>:</p>



<ul class="wp-block-list">
<li>Mostrar el listado de productos.</li>



<li>Ver el detalle de un pedido.</li>



<li>Abrir un formulario de registro.</li>



<li>Consultar reportes.</li>



<li>Filtrar información en una tabla.</li>



<li>Mostrar una pantalla de configuración.</li>
</ul>



<p class="wp-block-paragraph">Ejemplo genérico en Laravel:</p>



<pre class="wp-block-code"><code>Route::get('/admin/productos', &#91;ProductController::class, 'index'])
    -&gt;name('admin.products.index');</code></pre>



<p class="wp-block-paragraph">En este caso, la ruta solamente muestra información. No guarda datos, no elimina registros y no dispara procesos internos. Por eso <code>GET</code> es adecuado.</p>



<h2 class="wp-block-heading">Cuándo usar POST</h2>



<p class="wp-block-paragraph">El método <code>POST</code> se utiliza cuando el usuario va a enviar información o cuando una acción puede modificar el estado de la aplicación.</p>



<p class="wp-block-paragraph">Ejemplos comunes de acciones que deberían usar <code>POST</code>:</p>



<ul class="wp-block-list">
<li>Guardar un producto.</li>



<li>Crear un pedido.</li>



<li>Registrar un cliente.</li>



<li>Enviar un formulario.</li>



<li>Ejecutar una sincronización.</li>



<li>Procesar información externa.</li>



<li>Generar una acción administrativa.</li>
</ul>



<p class="wp-block-paragraph">Ejemplo genérico:</p>



<pre class="wp-block-code"><code>Route::post('/admin/productos', &#91;ProductController::class, 'store'])
    -&gt;name('admin.products.store');</code></pre>



<p class="wp-block-paragraph">Aquí la ruta recibe información y crea un nuevo registro. Por lo tanto, usar <code>POST</code> es lo correcto.</p>



<h2 class="wp-block-heading">¿Y si no estoy enviando parámetros?</h2>



<p class="wp-block-paragraph">Una duda común aparece cuando tenemos un botón que no envía datos visibles, pero sí ejecuta una acción. Por ejemplo:</p>



<ul class="wp-block-list">
<li>Sincronizar productos.</li>



<li>Procesar pedidos pendientes.</li>



<li>Actualizar inventario.</li>



<li>Ejecutar una tarea interna.</li>
</ul>



<p class="wp-block-paragraph">En estos casos podría parecer práctico usar un enlace con <code>href</code> y una ruta <code>GET</code>, porque no estamos enviando campos de formulario. Sin embargo, la decisión no depende solamente de si enviamos parámetros, sino de lo que ocurre cuando se visita la URL.</p>



<p class="wp-block-paragraph">Si al abrir la URL se ejecuta un proceso, se modifica información o se cambia el estado del sistema, entonces no debería ser <code>GET</code>. Debería ser <code>POST</code>.</p>



<h2 class="wp-block-heading">Ejemplo: ejecutar una sincronización</h2>



<p class="wp-block-paragraph">Supongamos que tenemos una aplicación administrativa y queremos agregar un botón para sincronizar información con un servicio externo.</p>



<p class="wp-block-paragraph">Una opción incorrecta sería hacerlo así:</p>



<pre class="wp-block-code"><code>&lt;a href="/admin/sincronizar-productos" class="btn btn-primary"&gt;
    Sincronizar productos
&lt;/a&gt;</code></pre>



<p class="wp-block-paragraph">Y definir la ruta como <code>GET</code>:</p>



<pre class="wp-block-code"><code>Route::get('/admin/sincronizar-productos', &#91;SyncController::class, 'syncProducts']);</code></pre>



<p class="wp-block-paragraph">El problema es que esa URL ejecuta un proceso. No solo muestra información. Por lo tanto, puede activarse accidentalmente si el usuario recarga la página, abre el enlace en otra pestaña, el navegador intenta previsualizarlo o incluso si algún rastreador accede a esa URL.</p>



<p class="wp-block-paragraph">La forma más adecuada sería usar una ruta <code>POST</code>:</p>



<pre class="wp-block-code"><code>Route::post('/admin/sincronizar-productos', &#91;SyncController::class, 'syncProducts'])
    -&gt;name('admin.products.sync');</code></pre>



<p class="wp-block-paragraph">Y ejecutarla desde un formulario:</p>



<pre class="wp-block-code"><code>&lt;form action="{{ route('admin.products.sync') }}" method="POST"&gt;
    @csrf

    &lt;button type="submit" class="btn btn-primary"&gt;
        Sincronizar productos
    &lt;/button&gt;
&lt;/form&gt;</code></pre>



<p class="wp-block-paragraph">Aunque el formulario no envíe campos adicionales, el uso de <code>POST</code> comunica correctamente que se trata de una acción, no de una simple consulta.</p>



<h2 class="wp-block-heading">Por qué evitar GET para acciones importantes</h2>



<p class="wp-block-paragraph">Usar <code>GET</code> para ejecutar acciones puede provocar varios problemas:</p>



<ul class="wp-block-list">
<li>La acción puede ejecutarse accidentalmente al recargar la página.</li>



<li>La URL puede compartirse o abrirse sin intención.</li>



<li>Un navegador puede intentar previsualizar o precargar el enlace.</li>



<li>Un bot o rastreador podría visitar la ruta.</li>



<li>No se aprovecha la protección CSRF de la misma manera que con formularios <code>POST</code>.</li>



<li>Se rompe la semántica de la aplicación, porque una consulta termina modificando el sistema.</li>
</ul>



<p class="wp-block-paragraph">Por eso, aunque usar <code>GET</code> pueda parecer más cómodo visualmente, no siempre es la opción correcta.</p>



<h2 class="wp-block-heading">La importancia del CSRF</h2>



<p class="wp-block-paragraph">En frameworks como Laravel, los formularios <code>POST</code> suelen incluir un token CSRF para validar que la petición realmente fue generada desde la aplicación y no desde un sitio externo.</p>



<p class="wp-block-paragraph">Ejemplo:</p>



<pre class="wp-block-code"><code>&lt;form action="{{ route('admin.products.sync') }}" method="POST"&gt;
    @csrf

    &lt;button type="submit"&gt;
        Ejecutar sincronización
    &lt;/button&gt;
&lt;/form&gt;</code></pre>



<p class="wp-block-paragraph">La directiva <code>@csrf</code> genera un token oculto que Laravel verifica al recibir la solicitud. Esto ayuda a proteger acciones sensibles como guardar información, eliminar registros o ejecutar procesos administrativos.</p>



<h2 class="wp-block-heading">¿Qué pasa si el formulario afecta el diseño?</h2>



<p class="wp-block-paragraph">En ocasiones se evita usar un formulario porque visualmente no encaja bien dentro de un grupo de botones o dentro de una interfaz administrativa. Sin embargo, ese es un problema de presentación, no de arquitectura.</p>



<p class="wp-block-paragraph">Podemos mantener el método correcto y ajustar el diseño con CSS:</p>



<pre class="wp-block-code"><code>&lt;div class="btn-group" role="group">
    &lt;a href="{{ route('admin.products.index') }}" class="btn btn-secondary">
        Regresar
    &lt;/a>

    &lt;form action="{{ route('admin.products.sync') }}" method="POST" class="m-0 d-inline">
        @csrf
        &lt;button type="submit" class="btn btn-primary">
            Sincronizar
        &lt;/button>
    &lt;/form>
&lt;/div></code></pre>



<p class="wp-block-paragraph">También puede resolverse con una petición AJAX usando <code>fetch</code>, manteniendo el método <code>POST</code> y enviando el token CSRF en los encabezados.</p>



<pre class="wp-block-code"><code>&lt;button type="button" id="btn-sync" class="btn btn-primary"&gt;
    Sincronizar
&lt;/button&gt;

&lt;script&gt;
document.getElementById('btn-sync').addEventListener('click', function () {
    fetch('/admin/sincronizar-productos', {
        method: 'POST',
        headers: {
            'X-CSRF-TOKEN': document.querySelector('meta&#91;name="csrf-token"]').content,
            'Accept': 'application/json'
        }
    });
});
&lt;/script&gt;</code></pre>



<p class="wp-block-paragraph">Lo importante es no cambiar a <code>GET</code> solo porque estéticamente es más cómodo usar un enlace.</p>



<h2 class="wp-block-heading">GET no debe tener efectos secundarios</h2>



<p class="wp-block-paragraph">Una buena práctica es que las rutas <code>GET</code> no tengan efectos secundarios importantes. Es decir, visitar una URL con <code>GET</code> no debería crear registros, eliminar información, actualizar inventario ni ejecutar procesos internos.</p>



<p class="wp-block-paragraph">Por ejemplo, estas rutas deberían evitarse:</p>



<pre class="wp-block-preformatted">GET /admin/eliminar-producto/10
GET /admin/procesar-pedidos
GET /admin/sincronizar-inventario
GET /admin/ejecutar-comando</pre>



<p class="wp-block-paragraph">Aunque técnicamente puedan funcionar, no son una buena práctica porque una simple visita a la URL puede cambiar el estado del sistema.</p>



<h2 class="wp-block-heading">Entonces, ¿cuándo usar cada uno?</h2>



<figure class="wp-block-table"><table><thead><tr><th>Situación</th><th>Método recomendado</th><th>Motivo</th></tr></thead><tbody><tr><td>Mostrar un listado</td><td>GET</td><td>Solo consulta información.</td></tr><tr><td>Ver detalle de un registro</td><td>GET</td><td>No modifica datos.</td></tr><tr><td>Mostrar un formulario</td><td>GET</td><td>Solo carga la interfaz.</td></tr><tr><td>Guardar un formulario</td><td>POST</td><td>Crea o modifica información.</td></tr><tr><td>Ejecutar una sincronización</td><td>POST</td><td>Dispara un proceso interno.</td></tr><tr><td>Eliminar un registro</td><td>DELETE o POST</td><td>Modifica el estado del sistema.</td></tr><tr><td>Actualizar información</td><td>PUT, PATCH o POST</td><td>Modifica datos existentes.</td></tr></tbody></table></figure>



<h2 class="wp-block-heading">Más allá de GET y POST</h2>



<p class="wp-block-paragraph">En aplicaciones modernas también es común utilizar otros métodos HTTP:</p>



<ul class="wp-block-list">
<li><code>PUT</code>: para actualizar completamente un recurso.</li>



<li><code>PATCH</code>: para actualizar parcialmente un recurso.</li>



<li><code>DELETE</code>: para eliminar un recurso.</li>
</ul>



<p class="wp-block-paragraph">Sin embargo, en formularios HTML tradicionales normalmente se trabaja con <code>GET</code> y <code>POST</code>. Frameworks como Laravel permiten simular métodos como <code>PUT</code>, <code>PATCH</code> o <code>DELETE</code> mediante campos ocultos.</p>



<pre class="wp-block-code"><code>&lt;form action="{{ route('admin.products.destroy', $product) }}" method="POST">
    @csrf
    @method('DELETE')
    &lt;button type="submit">
        Eliminar producto
    &lt;/button>
&lt;/form></code></pre>



<p class="wp-block-paragraph">Esto permite mantener una estructura más clara y alineada con las buenas prácticas de desarrollo web.</p>



<h2 class="wp-block-heading">Ejemplo práctico de flujo correcto</h2>



<p class="wp-block-paragraph">Supongamos que tenemos un módulo de pedidos y necesitamos estas acciones:</p>



<pre class="wp-block-preformatted">GET    /admin/pedidos              → Mostrar pedidos
GET    /admin/pedidos/15           → Ver detalle del pedido
GET    /admin/pedidos/create       → Mostrar formulario
POST   /admin/pedidos              → Guardar pedido
POST   /admin/pedidos/sincronizar  → Ejecutar sincronización
DELETE /admin/pedidos/15           → Eliminar pedido</pre>



<p class="wp-block-paragraph">Este tipo de estructura permite entender rápidamente qué hace cada ruta y qué impacto tiene dentro de la aplicación.</p>



<h2 class="wp-block-heading">Conclusión</h2>



<p class="wp-block-paragraph">Elegir entre <code>GET</code> y <code>POST</code> no debería depender únicamente de si enviamos parámetros o de qué opción se ve mejor en la interfaz. La decisión debe basarse en la intención de la ruta.</p>



<p class="wp-block-paragraph">Si la ruta solo consulta o muestra información, <code>GET</code> es correcto. Si la ruta guarda, modifica, elimina o ejecuta un proceso dentro del sistema, entonces debe usarse <code>POST</code> u otro método adecuado.</p>



<p class="wp-block-paragraph">En proyectos reales, esta diferencia ayuda a construir aplicaciones más seguras, ordenadas y fáciles de mantener. Además, evita que acciones importantes se ejecuten accidentalmente desde una URL que debería ser únicamente de consulta.</p>



<p class="wp-block-paragraph">La interfaz siempre se puede ajustar con CSS o JavaScript, pero la intención técnica de una ruta debe mantenerse clara desde el diseño de la aplicación.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.mariatech.com.mx/blog/laravel-php/get-vs-post-cuando-usar-cada-metodo-en-desarrollo-web/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Cómo sincronizar stock entre un sistema interno y una plataforma externa usando Laravel</title>
		<link>https://www.mariatech.com.mx/blog/laravel-php/como-sincronizar-stock-entre-un-sistema-interno-y-una-plataforma-externa-usando-laravel/</link>
					<comments>https://www.mariatech.com.mx/blog/laravel-php/como-sincronizar-stock-entre-un-sistema-interno-y-una-plataforma-externa-usando-laravel/#respond</comments>
		
		<dc:creator><![CDATA[Luis Duran]]></dc:creator>
		<pubDate>Fri, 26 Jun 2026 23:39:23 +0000</pubDate>
				<category><![CDATA[Laravel / PHP]]></category>
		<category><![CDATA[MySQL]]></category>
		<guid isPermaLink="false">https://www.mariatech.com.mx/blog/?p=304</guid>

					<description><![CDATA[En este artículo veremos un enfoque general para implementar una sincronización de stock usando Laravel, sin depender de una plataforma específica. La idea es explicar la arquitectura, las decisiones técnicas y las buenas prácticas que pueden aplicarse cuando un sistema interno necesita enviar información de inventario hacia un servicio externo. Mantener actualizado el inventario entre [&#8230;]]]></description>
										<content:encoded><![CDATA[
<p class="has-text-align-left wp-block-paragraph">En este artículo veremos un enfoque general para implementar una sincronización de stock usando Laravel, sin depender de una plataforma específica. La idea es explicar la arquitectura, las decisiones técnicas y las buenas prácticas que pueden aplicarse cuando un sistema interno necesita enviar información de inventario hacia un servicio externo.</p>



<p class="has-text-align-left wp-block-paragraph">Mantener actualizado el inventario entre un sistema interno y una plataforma externa es una tarea clave para cualquier negocio que vende productos en distintos canales. Cuando el stock no se sincroniza correctamente, pueden aparecer problemas como ventas sin existencia, información desactualizada o diferencias entre lo que ve el cliente y lo que realmente existe en almacén.</p>



<h2 class="wp-block-heading">El reto de sincronizar inventario</h2>



<p class="has-text-align-left wp-block-paragraph">En muchos sistemas, el inventario principal se administra desde una aplicación interna. Esta aplicación puede controlar productos, almacenes, existencias, movimientos, pedidos y configuraciones propias del negocio.</p>



<p class="has-text-align-left wp-block-paragraph">Por otro lado, una plataforma externa puede funcionar como canal de venta, catálogo público o punto de contacto con los clientes. El problema aparece cuando el stock cambia dentro del sistema interno, pero esa actualización no llega de inmediato a la plataforma externa.</p>



<p class="has-text-align-left wp-block-paragraph">Esto puede provocar que un producto aparezca disponible cuando realmente ya no hay existencias, o que el equipo operativo tenga que corregir manualmente la información en más de un lugar.</p>



<p class="has-text-align-left wp-block-paragraph">Por eso es importante diseñar un proceso de sincronización que permita mantener ambos sistemas alineados.</p>



<h2 class="wp-block-heading">Definir una fuente principal de información</h2>



<p class="wp-block-paragraph">Antes de implementar la sincronización, es necesario definir qué sistema será la fuente principal del stock.</p>



<p class="wp-block-paragraph">En la mayoría de los casos, el sistema interno debe ser la fuente confiable, ya que ahí se registran los movimientos reales de inventario. La plataforma externa solo debería recibir el dato actualizado.</p>



<p class="wp-block-paragraph">Este principio evita conflictos como:</p>



<ul class="wp-block-list">
<li>Actualizar stock desde dos lugares diferentes.</li>



<li>Sobrescribir información correcta con datos desactualizados.</li>



<li>Perder control sobre qué sistema tiene la información válida.</li>



<li>Generar diferencias entre almacenes y publicaciones externas.</li>
</ul>



<p class="wp-block-paragraph">Cuando el sistema interno es la fuente principal, la sincronización se vuelve más clara: se calcula el stock disponible internamente y después se envía hacia la plataforma externa.</p>



<h2 class="wp-block-heading">Relacionar productos internos con elementos externos</h2>



<p class="wp-block-paragraph">Un paso fundamental es relacionar cada producto interno con su equivalente en la plataforma externa.</p>



<p class="wp-block-paragraph">No se debe enviar stock si no existe una relación clara entre ambos elementos. De lo contrario, el sistema podría actualizar el producto equivocado o enviar información incorrecta.</p>



<p class="wp-block-paragraph">Una relación puede contener datos generales como:</p>



<ul class="wp-block-list">
<li>Identificador del producto interno.</li>



<li>Identificador del elemento externo.</li>



<li>Estado de la relación.</li>



<li>Última fecha de sincronización.</li>



<li>Stock enviado más recientemente.</li>
</ul>



<p class="wp-block-paragraph">Esta relación permite saber qué elementos pueden sincronizarse y cuáles necesitan revisión antes de participar en el proceso.</p>



<h2 class="wp-block-heading">Obtener el stock correcto</h2>



<p class="wp-block-paragraph">Cuando el sistema interno maneja varios almacenes, no siempre conviene enviar el stock total de todos ellos. En algunos casos, la plataforma externa debe trabajar únicamente con un almacén específico.</p>



<p class="wp-block-paragraph">Por ejemplo, una empresa puede tener inventario en sucursales, almacenes físicos o puntos de distribución diferentes. La sincronización debe respetar la configuración del negocio y tomar el stock desde el almacén autorizado para ese canal.</p>



<p class="wp-block-paragraph">También es recomendable validar que el stock enviado nunca sea negativo. Si el sistema interno llegara a tener un valor menor que cero por algún ajuste o movimiento pendiente, lo más seguro es enviar cero a la plataforma externa.</p>



<h2 class="wp-block-heading">Separar la lógica usando Jobs</h2>



<p class="wp-block-paragraph">En Laravel, una buena práctica es separar este tipo de procesos en Jobs.</p>



<p class="wp-block-paragraph">Un Job permite ejecutar la sincronización sin cargar toda la lógica dentro del controlador. Esto hace que el código sea más ordenado, más fácil de mantener y más flexible.</p>



<p class="wp-block-paragraph">El flujo general puede verse así:</p>



<pre class="wp-block-code"><code>Usuario o tarea programada inicia la sincronización
↓
Laravel dispara un Job
↓
El Job busca productos relacionados
↓
Calcula el stock interno
↓
Envía la información al servicio externo
↓
Registra el resultado</code></pre>



<p class="wp-block-paragraph">Esta estructura permite ejecutar la misma lógica desde distintos puntos, como un botón manual, una tarea programada o un comando de consola.</p>



<h2 class="wp-block-heading">Usar un servicio para comunicarse con la plataforma externa</h2>



<p class="wp-block-paragraph">La comunicación con la API externa debería estar aislada en una clase de servicio.</p>



<p class="wp-block-paragraph">Esto evita que el controlador o el Job tengan demasiada responsabilidad. El servicio puede encargarse de preparar la petición, enviar el stock, interpretar la respuesta y devolver un resultado claro.</p>



<p class="wp-block-paragraph">De forma conceptual, el servicio debería realizar estas tareas:</p>



<ul class="wp-block-list">
<li>Validar que exista una configuración activa.</li>



<li>Obtener el token o credenciales necesarias.</li>



<li>Enviar la cantidad disponible a la plataforma externa.</li>



<li>Revisar si la respuesta fue exitosa.</li>



<li>Detectar errores conocidos.</li>



<li>Devolver el resultado al proceso principal.</li>
</ul>



<p class="wp-block-paragraph">Separar esta lógica facilita futuras modificaciones, especialmente si la plataforma externa cambia sus reglas, endpoints o formato de respuesta.</p>



<h2 class="wp-block-heading">Actualizar información local solo cuando la API responda correctamente</h2>



<p class="wp-block-paragraph">Una parte importante del proceso es no asumir que la sincronización fue exitosa solo porque se envió una petición.</p>



<p class="wp-block-paragraph">La plataforma externa puede rechazar el cambio por varias razones: credenciales inválidas, producto no encontrado, permisos insuficientes, reglas del canal o restricciones sobre ciertos campos.</p>



<p class="wp-block-paragraph">Por eso, el sistema interno solo debería actualizar su registro local de “último stock sincronizado” cuando la API confirme que la operación fue aceptada.</p>



<p class="wp-block-paragraph">Esto evita mostrar información incorrecta dentro del sistema administrativo.</p>



<h2 class="wp-block-heading">Registrar una bitácora de sincronización</h2>



<p class="wp-block-paragraph">Toda integración con servicios externos debería contar con una bitácora.</p>



<p class="wp-block-paragraph">La bitácora permite saber qué ocurrió en cada intento de sincronización. Esto es útil para auditoría, soporte y diagnóstico de errores.</p>



<p class="wp-block-paragraph">Una buena bitácora puede registrar información como:</p>



<ul class="wp-block-list">
<li>Fecha del intento.</li>



<li>Producto interno relacionado.</li>



<li>Elemento externo actualizado.</li>



<li>Stock calculado internamente.</li>



<li>Stock enviado a la plataforma externa.</li>



<li>Estado de la operación.</li>



<li>Código de respuesta.</li>



<li>Mensaje descriptivo del resultado.</li>
</ul>



<p class="wp-block-paragraph">Con esta información, el equipo puede saber si una actualización fue exitosa, si la plataforma externa rechazó el cambio o si ocurrió un problema de configuración.</p>



<h2 class="wp-block-heading">Mostrar la bitácora en el panel administrativo</h2>



<p class="wp-block-paragraph">Guardar la bitácora en la base de datos es útil, pero mostrarla en la interfaz administrativa mejora mucho la experiencia del usuario.</p>



<p class="wp-block-paragraph">Una pestaña de historial o bitácora permite revisar rápidamente los últimos intentos de sincronización sin entrar directamente a la base de datos.</p>



<p class="wp-block-paragraph">Esta sección puede mostrar columnas como:</p>



<ul class="wp-block-list">
<li>Fecha.</li>



<li>Producto.</li>



<li>Stock interno.</li>



<li>Stock enviado.</li>



<li>Estado.</li>



<li>Código de respuesta.</li>



<li>Mensaje.</li>
</ul>



<p class="wp-block-paragraph">De esta manera, el usuario puede identificar rápidamente si una sincronización fue exitosa o si necesita atención.</p>



<h2 class="wp-block-heading">Sincronización automática y sincronización manual</h2>



<p class="wp-block-paragraph">Una sincronización puede ejecutarse de forma automática mediante tareas programadas. Esto ayuda a mantener el inventario actualizado sin intervención constante del usuario.</p>



<p class="wp-block-paragraph">Sin embargo, también es útil contar con una opción manual desde el panel administrativo.</p>



<p class="wp-block-paragraph">El botón manual no reemplaza el proceso automático, sino que lo complementa. Puede servir cuando el usuario necesita actualizar el stock inmediatamente después de hacer cambios importantes en el inventario o en las relaciones con la plataforma externa.</p>



<p class="wp-block-paragraph">Para evitar ejecuciones accidentales, conviene mostrar una confirmación antes de iniciar el proceso.</p>



<h2 class="wp-block-heading">Manejo de errores</h2>



<p class="wp-block-paragraph">No todos los errores significan que la lógica interna esté mal. A veces la plataforma externa rechaza una actualización por sus propias reglas.</p>



<p class="wp-block-paragraph">Algunos casos comunes son:</p>



<ul class="wp-block-list">
<li>Token expirado.</li>



<li>Permisos insuficientes.</li>



<li>Producto externo no encontrado.</li>



<li>Campo no editable.</li>



<li>Restricciones por estado del producto.</li>



<li>Errores temporales del servicio externo.</li>
</ul>



<p class="wp-block-paragraph">Por eso es importante que el sistema muestre mensajes claros y registre la respuesta técnica para diagnóstico.</p>



<p class="wp-block-paragraph">Un mensaje útil para el usuario debe explicar el problema sin saturarlo con detalles técnicos innecesarios.</p>



<h2 class="wp-block-heading">Buenas prácticas</h2>



<p class="wp-block-paragraph">Al implementar sincronización de stock entre un sistema interno y una plataforma externa, conviene aplicar estas buenas prácticas:</p>



<ul class="wp-block-list">
<li>Definir claramente cuál sistema es la fuente principal del inventario.</li>



<li>Sincronizar solo productos correctamente relacionados.</li>



<li>Tomar el stock desde el almacén configurado.</li>



<li>Evitar enviar cantidades negativas.</li>



<li>Separar responsabilidades usando controladores, Jobs y servicios.</li>



<li>Registrar cada intento en una bitácora.</li>



<li>Actualizar datos locales solo cuando la API confirme éxito.</li>



<li>Mostrar resultados de sincronización en el panel administrativo.</li>



<li>Manejar errores de forma clara.</li>



<li>Mantener una opción manual como apoyo al proceso automático.</li>
</ul>



<h2 class="wp-block-heading">Conclusión</h2>



<p class="wp-block-paragraph">Sincronizar stock entre un sistema interno y una plataforma externa no consiste solamente en enviar una cantidad mediante una API. También implica validar relaciones, respetar configuraciones, manejar errores, registrar evidencia y mantener consistencia entre ambos sistemas.</p>



<p class="wp-block-paragraph">Laravel ofrece herramientas muy útiles para construir este tipo de procesos de forma ordenada, como Jobs, servicios, comandos de consola, tareas programadas y manejo de peticiones HTTP.</p>



<p class="wp-block-paragraph">Una implementación bien diseñada permite que el sistema interno conserve el control principal del inventario, mientras la plataforma externa recibe actualizaciones confiables y trazables.</p>



<p class="wp-block-paragraph">El resultado es una integración más segura, más fácil de mantener y más útil para los usuarios que necesitan operar productos en diferentes canales de venta.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.mariatech.com.mx/blog/laravel-php/como-sincronizar-stock-entre-un-sistema-interno-y-una-plataforma-externa-usando-laravel/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Cómo solucionar la discrepancia de protocolos (HTTP vs HTTPS) en integraciones OAuth 2.0</title>
		<link>https://www.mariatech.com.mx/blog/laravel-php/como-solucionar-la-discrepancia-de-protocolos-http-vs-https-en-integraciones-oauth-2-0/</link>
					<comments>https://www.mariatech.com.mx/blog/laravel-php/como-solucionar-la-discrepancia-de-protocolos-http-vs-https-en-integraciones-oauth-2-0/#respond</comments>
		
		<dc:creator><![CDATA[Luis Duran]]></dc:creator>
		<pubDate>Tue, 16 Jun 2026 00:12:41 +0000</pubDate>
				<category><![CDATA[Laravel / PHP]]></category>
		<category><![CDATA[Apps]]></category>
		<guid isPermaLink="false">https://www.mariatech.com.mx/blog/?p=294</guid>

					<description><![CDATA[Si estás integrando una API externa en tu aplicación web y te has topado con mensajes de error como &#8220;La aplicación no puede conectarse&#8221;, &#8220;Invalid redirect URI&#8221; o errores del navegador como ERR_CONNECTION_REFUSED al intentar autenticar usuarios mediante OAuth 2.0, no estás solo. Este es uno de los problemas más frecuentes durante el desarrollo local [&#8230;]]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">Si estás integrando una API externa en tu aplicación web y te has topado con mensajes de error como <strong>&#8220;La aplicación no puede conectarse&#8221;</strong>, <strong>&#8220;Invalid redirect URI&#8221;</strong> o errores del navegador como <code><strong>ERR_CONNECTION_REFUSED</strong></code> al intentar autenticar usuarios mediante OAuth 2.0, no estás solo.</p>



<p class="wp-block-paragraph">Este es uno de los problemas más frecuentes durante el desarrollo local y suele estar relacionado con una causa muy específica: <strong>la discrepancia de protocolos en la URI de redirección (</strong><code><strong>redirect_uri</strong></code><strong>)</strong>.</p>



<p class="wp-block-paragraph">A continuación, veremos por qué ocurre este problema y cómo solucionarlo para que tu flujo de autenticación funcione correctamente tanto en desarrollo como en producción.</p>



<h2 class="wp-block-heading">El origen del problema: la seguridad en OAuth 2.0</h2>



<p class="wp-block-paragraph">OAuth 2.0 es el estándar utilizado por numerosas plataformas y servicios para permitir que aplicaciones externas accedan a recursos protegidos de forma segura. Durante la configuración de una aplicación, normalmente se debe registrar una o más URI de redirección.</p>



<p class="wp-block-paragraph">Estas direcciones son utilizadas por el proveedor de autenticación para devolver al usuario a tu aplicación una vez que ha iniciado sesión y autorizado el acceso.</p>



<p class="wp-block-paragraph">El conflicto suele aparecer debido a la diferencia entre los requisitos de seguridad de la plataforma y las características de un entorno local de desarrollo:</p>



<ol start="1" class="wp-block-list">
<li><strong>Las plataformas exigen HTTPS:</strong> Muchos proveedores requieren que la <code>redirect_uri</code> utilice un protocolo seguro (<code>https://</code>) para proteger el intercambio de información sensible.</li>



<li><strong>Los entornos locales suelen usar HTTP:</strong> Herramientas de desarrollo como Laragon, XAMPP, WAMP o configuraciones locales de servidores web suelen funcionar inicialmente mediante <code>http://</code>.</li>
</ol>



<p class="wp-block-paragraph">Cuando la aplicación genera una URL con HTTPS pero el servidor local no está preparado para responder mediante ese protocolo, el navegador no puede establecer la conexión. Por otro lado, si se utiliza HTTP, muchos proveedores rechazarán la solicitud por razones de seguridad o porque la URL no coincide exactamente con la registrada.</p>



<h2 class="wp-block-heading">La solución en 3 pasos</h2>



<p class="wp-block-paragraph">La clave consiste en lograr que tu entorno local soporte HTTPS y que las URLs generadas por tu aplicación coincidan exactamente con las registradas en el proveedor de autenticación.</p>



<h3 class="wp-block-heading">Paso 1: Habilitar HTTPS en tu entorno local</h3>



<p class="wp-block-paragraph">Configura un certificado SSL local para que tu aplicación pueda responder mediante HTTPS.</p>



<p class="wp-block-paragraph">Algunas opciones comunes son:</p>



<ul class="wp-block-list">
<li><strong>Laragon:</strong> Permite habilitar certificados SSL locales de forma automática.</li>



<li><strong>Laravel Valet:</strong> Puede generar certificados locales seguros mediante comandos específicos.</li>



<li><strong>mkcert:</strong> Herramienta ampliamente utilizada para crear certificados confiables en entornos de desarrollo.</li>



<li><strong>Docker o servidores personalizados:</strong> Configura certificados SSL dentro del contenedor o servidor correspondiente.</li>
</ul>



<p class="wp-block-paragraph">Una vez configurado, tu aplicación debería ser accesible mediante una URL similar a:</p>



<pre class="wp-block-code"><code>https:&#047;&#047;miaplicacion.local</code></pre>



<h3 class="wp-block-heading">Paso 2: Generar URLs seguras desde la aplicación</h3>



<p class="wp-block-paragraph">Es importante que la URI de redirección se genere utilizando HTTPS para garantizar que coincida con la configuración registrada en el proveedor.</p>



<p class="wp-block-paragraph">Por ejemplo, en Laravel:</p>



<pre class="wp-block-code"><code>$redirect_uri = secure_url(route('oauth.callback', &#091;], false));</code></pre>



<p class="wp-block-paragraph">El objetivo no es utilizar una función específica, sino asegurarse de que la URL enviada al proveedor tenga exactamente el protocolo HTTPS esperado.</p>



<h3 class="wp-block-heading">Paso 3: Verificar la configuración del proveedor</h3>



<p class="wp-block-paragraph">La URI configurada en el panel del proveedor debe coincidir exactamente con la generada por tu aplicación.</p>



<p class="wp-block-paragraph">Ejemplos:</p>



<ul class="wp-block-list">
<li><strong>Correcto:</strong> <code>https://miaplicacion.local/oauth/callback</code></li>



<li><strong>Incorrecto:</strong> <code>http://miaplicacion.local/oauth/callback</code></li>



<li><strong>Incorrecto:</strong> <code>https://miaplicacion.local/oauth/callback/</code></li>



<li><strong>Incorrecto:</strong> <code>https://www.miaplicacion.local/oauth/callback</code> (si el dominio registrado no incluye <code>www</code>)</li>
</ul>



<p class="wp-block-paragraph">Las plataformas suelen realizar comparaciones estrictas, por lo que cualquier diferencia en protocolo, dominio, ruta, mayúsculas o barras finales puede provocar errores de autenticación.</p>



<h2 class="wp-block-heading">Buenas prácticas adicionales</h2>



<p class="wp-block-paragraph">Además de corregir el problema inmediato, es recomendable seguir estas prácticas:</p>



<ul class="wp-block-list">
<li>Mantener la misma estructura de URLs entre desarrollo, pruebas y producción.</li>



<li>Utilizar variables de entorno para configurar dominios y rutas de autenticación.</li>



<li>Evitar construir manualmente las URLs de redirección cuando el framework puede generarlas automáticamente.</li>



<li>Verificar periódicamente la configuración registrada en los proveedores externos.</li>



<li>Probar el flujo completo de autenticación antes de desplegar cambios en producción.</li>
</ul>



<h2 class="wp-block-heading">Conclusión</h2>



<p class="wp-block-paragraph">La mayoría de los problemas relacionados con OAuth 2.0 en entornos de desarrollo no provienen de errores en el código, sino de diferencias entre la configuración local y los requisitos de seguridad exigidos por los proveedores de autenticación.</p>



<p class="wp-block-paragraph">Al habilitar HTTPS localmente, generar URLs seguras desde la aplicación y asegurarte de que las URIs registradas coincidan exactamente con las utilizadas en el código, podrás evitar errores de autenticación y contar con un flujo de integración mucho más estable y seguro.</p>



<p class="wp-block-paragraph">Estos mismos principios aplican a cualquier integración basada en OAuth 2.0, independientemente del proveedor o servicio externo con el que estés trabajando.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.mariatech.com.mx/blog/laravel-php/como-solucionar-la-discrepancia-de-protocolos-http-vs-https-en-integraciones-oauth-2-0/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Cómo asegurar conexiones a APIs externas usando OAuth 2.0 en Laravel</title>
		<link>https://www.mariatech.com.mx/blog/laravel-php/como-asegurar-conexiones-a-apis-externas-usando-oauth-2-0-en-laravel/</link>
					<comments>https://www.mariatech.com.mx/blog/laravel-php/como-asegurar-conexiones-a-apis-externas-usando-oauth-2-0-en-laravel/#respond</comments>
		
		<dc:creator><![CDATA[Luis Duran]]></dc:creator>
		<pubDate>Tue, 09 Jun 2026 20:39:53 +0000</pubDate>
				<category><![CDATA[Laravel / PHP]]></category>
		<category><![CDATA[MySQL]]></category>
		<guid isPermaLink="false">https://www.mariatech.com.mx/blog/?p=291</guid>

					<description><![CDATA[Aprende a implementar OAuth 2.0 en Laravel para conectar tu aplicación o ERP con APIs externas de forma segura. Protege tus credenciales paso a paso.]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">Si alguna vez has tenido que conectar un sistema local (como un ERP) con plataformas de terceros como Mercado Libre, Google o Facebook, sabes que la comunicación mediante APIs es el pan de cada día. Sin embargo, el mayor reto no es lograr que la conexión funcione, sino garantizar que <strong>funcione de manera segura</strong>.</p>



<p class="wp-block-paragraph">En este artículo, exploraremos las mejores prácticas para implementar OAuth 2.0 en Laravel, protegiendo las credenciales de tu aplicación y evitando vulnerabilidades críticas.</p>



<h2 class="wp-block-heading">¿Por qué no deberías guardar tokens en texto plano?</h2>



<p class="wp-block-paragraph">Cuando trabajas con integraciones de terceros, el flujo de OAuth 2.0 te entrega un <code>access_token</code> y, a menudo, un <code>refresh_token</code>. El error más común en desarrolladores junior es guardar estas cadenas directamente en la base de datos como texto plano.</p>



<p class="wp-block-paragraph">Si un atacante lograra vulnerar tu base de datos, tendría acceso total a las cuentas de tus clientes en esas plataformas externas. En Laravel, la solución es sumamente sencilla gracias a sus Facades de encriptación.</p>



<p class="wp-block-paragraph">Al guardar tus credenciales, asegúrate de utilizar la función nativa de Laravel para encriptar los datos sensibles, de manera que solo tu aplicación pueda interpretarlos usando la llave maestra (<code>APP_KEY</code>) de tu entorno.</p>



<figure class="wp-block-image size-full"><img fetchpriority="high" decoding="async" width="648" height="127" src="https://www.mariatech.com.mx/blog/wp-content/uploads/2026/06/Captura-de-pantalla-2026-06-09-130023.png" alt="Uso de la función encrypt en Laravel" class="wp-image-292" srcset="https://www.mariatech.com.mx/blog/wp-content/uploads/2026/06/Captura-de-pantalla-2026-06-09-130023.png 648w, https://www.mariatech.com.mx/blog/wp-content/uploads/2026/06/Captura-de-pantalla-2026-06-09-130023-300x59.png 300w" sizes="(max-width: 648px) 100vw, 648px" /></figure>



<h2 class="wp-block-heading">Creando una arquitectura segura en la Base de Datos</h2>



<p class="wp-block-paragraph">Otro aspecto vital es la separación de responsabilidades. No mezcles la configuración de tus APIs con la tabla general de usuarios de tu sistema.</p>



<p class="wp-block-paragraph">Lo ideal es diseñar una tabla específica (por ejemplo, <code>api_configurations</code>) que contenga únicamente los datos de autenticación: el <em>App ID</em>, el <em>Secret Key</em> y los <em>tokens</em>. Esto te permite aplicar reglas estrictas de acceso, implementar restricciones de unicidad para evitar duplicados en las configuraciones y aislar la información crítica del resto de la lógica de negocio.</p>



<h2 class="wp-block-heading">El flujo de autorización y el control de Callbacks</h2>



<p class="wp-block-paragraph">OAuth 2.0 requiere que el usuario sea redirigido a la plataforma externa para dar su consentimiento y luego regrese a tu aplicación a través de una URL de retorno (Callback).</p>



<p class="wp-block-paragraph">Un buen desarrollador debe blindar esta ruta de retorno. Nunca asumas que la redirección viene con los parámetros correctos. Es indispensable:</p>



<ul class="wp-block-list">
<li>Validar que el código de autorización realmente venga en la URL.</li>



<li>Implementar validaciones de estado para evitar peticiones redundantes.</li>



<li>Manejar bloqueos para que un usuario no pueda acceder a rutas de &#8220;desconexión&#8221; si la cuenta ya ha sido desvinculada previamente.</li>
</ul>



<h2 class="wp-block-heading">Conclusión: La seguridad no es opcional</h2>



<p class="wp-block-paragraph">Conectar una API es un logro técnico, pero asegurarla es lo que distingue a un desarrollo amateur de uno profesional. Laravel nos ofrece todas las herramientas necesarias —desde rutas nombradas hasta encriptación nativa y middlewares— para que nuestras integraciones sean robustas y a prueba de balas.</p>



<p class="wp-block-paragraph"></p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.mariatech.com.mx/blog/laravel-php/como-asegurar-conexiones-a-apis-externas-usando-oauth-2-0-en-laravel/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Cómo cambiar la autenticación de GitHub de Access Token a SSH en un proyecto Laravel</title>
		<link>https://www.mariatech.com.mx/blog/laravel-php/como-cambiar-la-autenticacion-de-github-de-access-token-a-ssh-en-un-proyecto-laravel/</link>
					<comments>https://www.mariatech.com.mx/blog/laravel-php/como-cambiar-la-autenticacion-de-github-de-access-token-a-ssh-en-un-proyecto-laravel/#respond</comments>
		
		<dc:creator><![CDATA[Victor Santillan]]></dc:creator>
		<pubDate>Sun, 31 May 2026 23:23:59 +0000</pubDate>
				<category><![CDATA[Laravel / PHP]]></category>
		<category><![CDATA[Git]]></category>
		<guid isPermaLink="false">https://www.mariatech.com.mx/blog/?p=278</guid>

					<description><![CDATA[En este artículo veremos cómo migrar la autenticación de un proyecto desde HTTPS con token hacia SSH, cómo validar que la conexión funcione...]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">Cuando trabajamos con proyectos alojados en GitHub, es común que al principio usemos una URL HTTPS con un <strong>Personal Access Token</strong> para clonar, descargar o actualizar el repositorio. Sin embargo, estos tokens pueden vencer, ser revocados o simplemente dejar de funcionar por cambios de seguridad.</p>



<p class="wp-block-paragraph">En este artículo veremos cómo migrar la autenticación de un proyecto desde HTTPS con token hacia SSH, cómo validar que la conexión funcione correctamente y qué hacer cuando Git no permite cambiar de rama porque existen archivos locales modificados, como puede pasar con archivos de configuración en Laravel.</p>



<h2 class="wp-block-heading">El problema: el token de acceso ya venció</h2>



<p class="wp-block-paragraph">Supongamos que tenemos un proyecto Laravel en un servidor de pruebas o demo. Al intentar ejecutar comandos como <code>git fetch</code>, <code>git pull</code> o <code>git checkout</code>, GitHub responde con un error de autenticación porque el token que se usaba anteriormente ya venció.</p>



<p class="wp-block-paragraph">En muchos casos, aunque ya hayamos creado una llave SSH en el servidor, Git seguirá usando el token anterior si el repositorio todavía tiene configurada una URL remota con HTTPS.</p>



<p class="wp-block-paragraph">Es decir, crear una llave SSH no cambia automáticamente la forma en que Git se conecta al repositorio. También debemos actualizar la URL remota del proyecto.</p>



<h2 class="wp-block-heading">1. Revisar la URL remota actual</h2>



<p class="wp-block-paragraph">Primero debemos entrar a la carpeta del proyecto y revisar qué URL remota está usando Git.</p>



<pre class="wp-block-code"><code>cd /ruta/general/del/proyecto

git remote -v</code></pre>



<p class="wp-block-paragraph">Si la respuesta muestra una URL similar a esta:</p>



<pre class="wp-block-code"><code>origin  https://github.com/usuario/repositorio.git (fetch)
origin  https://github.com/usuario/repositorio.git (push)</code></pre>



<p class="wp-block-paragraph">significa que el proyecto todavía está usando HTTPS.</p>



<p class="wp-block-paragraph">En algunos casos, incluso puede aparecer un token dentro de la URL, algo parecido a esto:</p>



<pre class="wp-block-code"><code>origin  https://TOKEN@github.com/usuario/repositorio.git</code></pre>



<p class="wp-block-paragraph">Ese es el escenario que debemos corregir.</p>



<h2 class="wp-block-heading">2. Cambiar la URL remota de HTTPS a SSH</h2>



<p class="wp-block-paragraph">Para que Git use la llave SSH, debemos cambiar la URL remota del repositorio usando el comando <code>git remote set-url</code>.</p>



<pre class="wp-block-code"><code>git remote set-url origin git@github.com:usuario/repositorio.git</code></pre>



<p class="wp-block-paragraph">Este comando cambia el origen del repositorio para que Git deje de usar HTTPS y empiece a usar SSH.</p>



<p class="wp-block-paragraph">Después podemos verificar nuevamente la configuración:</p>



<pre class="wp-block-code"><code>git remote -v</code></pre>



<p class="wp-block-paragraph">La salida esperada sería algo como:</p>



<pre class="wp-block-code"><code>origin  git@github.com:usuario/repositorio.git (fetch)
origin  git@github.com:usuario/repositorio.git (push)</code></pre>



<h2 class="wp-block-heading">3. Probar la conexión SSH con GitHub</h2>



<p class="wp-block-paragraph">Antes de intentar descargar ramas o actualizar el proyecto, conviene probar que la llave SSH realmente esté funcionando.</p>



<pre class="wp-block-code"><code>ssh -T git@github.com</code></pre>



<p class="wp-block-paragraph">Si la autenticación es correcta, GitHub responderá con un mensaje indicando que la conexión fue exitosa, aunque no permita acceso de shell. Esto es normal.</p>



<p class="wp-block-paragraph">Una respuesta esperada puede ser similar a:</p>



<pre class="wp-block-code"><code>Hi usuario! You've successfully authenticated, but GitHub does not provide shell access.</code></pre>



<p class="wp-block-paragraph">Con eso confirmamos que la autenticación SSH ya está funcionando.</p>



<h2 class="wp-block-heading">4. Descargar información actualizada del repositorio</h2>



<p class="wp-block-paragraph">Una vez configurada la URL SSH, podemos ejecutar:</p>



<pre class="wp-block-preformatted">git fetch origin</pre>



<p class="wp-block-paragraph">Este comando descarga la información actualizada de las ramas remotas sin mezclar cambios todavía en la rama actual.</p>



<h2 class="wp-block-heading">5. Cambiar a una rama remota</h2>



<p class="wp-block-paragraph">Si necesitamos crear una rama local a partir de una rama remota, podemos usar:</p>



<pre class="wp-block-code"><code>git checkout -b nombre-rama origin/nombre-rama</code></pre>



<p class="wp-block-paragraph">Sin embargo, en proyectos reales puede aparecer un error indicando que un archivo local tiene cambios y que Git necesita sobrescribirlo para cambiar de rama.</p>



<p class="wp-block-paragraph">Un caso común en Laravel puede ser un archivo como:</p>



<pre class="wp-block-code"><code>config/app.php</code></pre>



<h2 class="wp-block-heading">6. ¿Por qué pasa este error?</h2>



<p class="wp-block-paragraph">Git protege los cambios locales para evitar que se pierdan accidentalmente. Si tenemos modificaciones en un archivo y la rama a la que queremos cambiar también modifica ese archivo, Git detiene el proceso.</p>



<p class="wp-block-paragraph">A veces se intenta usar el comando <code>assume-unchanged</code> para que Git ignore temporalmente un archivo:</p>



<pre class="wp-block-code"><code>git update-index --assume-unchanged config/app.php</code></pre>



<p class="wp-block-paragraph">Este comando puede ayudar en algunos escenarios locales, pero no es una solución ideal para manejar archivos de configuración modificados en servidores o ambientes de demo.</p>



<p class="wp-block-paragraph">Además, aunque un archivo esté marcado como <code>assume-unchanged</code>, Git aún puede bloquear operaciones si necesita proteger cambios locales antes de cambiar de rama.</p>



<h2 class="wp-block-heading">7. Revisar si un archivo está marcado como assume-unchanged</h2>



<p class="wp-block-paragraph">Podemos revisar el estado del archivo con:</p>



<pre class="wp-block-code"><code>git ls-files -v config/app.php</code></pre>



<p class="wp-block-paragraph">Si el archivo aparece con una letra minúscula al inicio, significa que está marcado como <code>assume-unchanged</code>.</p>



<h2 class="wp-block-heading">8. Quitar assume-unchanged</h2>



<p class="wp-block-paragraph">Antes de resolver el conflicto, es recomendable quitar esa marca para que Git vuelva a tratar el archivo normalmente.</p>



<pre class="wp-block-code"><code>git update-index --no-assume-unchanged config/app.php</code></pre>



<h2 class="wp-block-heading">9. Opción segura: guardar temporalmente los cambios con stash</h2>



<p class="wp-block-paragraph">Si necesitamos conservar los cambios locales del archivo, podemos guardarlos temporalmente con <code>git stash</code>.</p>



<pre class="wp-block-code"><code>git stash push -m "Backup config app" -- config/app.php</code></pre>



<p class="wp-block-paragraph">Después ya podemos intentar cambiar de rama:</p>



<pre class="wp-block-code"><code>git checkout -b nombre-rama origin/nombre-rama</code></pre>



<p class="wp-block-paragraph">Y si necesitamos recuperar los cambios guardados:</p>



<pre class="wp-block-code"><code>git stash pop</code></pre>



<p class="wp-block-paragraph">Si al recuperar los cambios aparecen conflictos, deberán resolverse manualmente.</p>



<h2 class="wp-block-heading">10. Opción rápida: descartar cambios locales</h2>



<p class="wp-block-paragraph">Si los cambios locales no son importantes, podemos descartarlos.</p>



<pre class="wp-block-code"><code>git restore config/app.php</code></pre>



<p class="wp-block-paragraph">Después intentamos nuevamente cambiar de rama:</p>



<pre class="wp-block-code"><code>git checkout -b nombre-rama origin/nombre-rama</code></pre>



<p class="wp-block-paragraph">Esta opción debe usarse con cuidado, porque elimina los cambios locales del archivo.</p>



<h2 class="wp-block-heading">11. Mejor práctica en Laravel: usar .env para configuración por ambiente</h2>



<p class="wp-block-paragraph">En Laravel, lo más recomendable es evitar modificar archivos como <code>config/app.php</code> directamente en cada servidor. Si una configuración cambia dependiendo del ambiente, lo ideal es manejarla desde el archivo <code>.env</code>.</p>



<p class="wp-block-paragraph">Por ejemplo, en lugar de dejar un valor fijo dentro de <code>config/app.php</code>, podemos leerlo desde una variable de entorno:</p>



<pre class="wp-block-code"><code>'custom_product' =&gt; env('APP_CUSTOM_PRODUCT', false),</code></pre>



<p class="wp-block-paragraph">Y en el archivo <code>.env</code> del servidor:</p>



<pre class="wp-block-code"><code>APP_CUSTOM_PRODUCT=true</code></pre>



<p class="wp-block-paragraph">De esta manera cada ambiente puede tener su propia configuración sin modificar archivos versionados en Git.</p>



<h2 class="wp-block-heading">12. Flujo recomendado completo</h2>



<p class="wp-block-paragraph">Un flujo seguro para este tipo de situación sería:</p>



<pre class="wp-block-code"><code>cd /ruta/general/del/proyecto

git remote -v

git remote set-url origin git@github.com:usuario/repositorio.git

git remote -v

ssh -T git@github.com

git fetch origin

git update-index --no-assume-unchanged config/app.php

git stash push -m "Backup config app" -- config/app.php

git checkout -b nombre-rama origin/nombre-rama</code></pre>



<p class="wp-block-paragraph">Si después necesitamos recuperar los cambios locales:</p>



<pre class="wp-block-code"><code>git stash pop</code></pre>



<h2 class="wp-block-heading">Conclusión</h2>



<p class="wp-block-paragraph">Cuando un proyecto deja de autenticarse con GitHub porque venció un access token, una buena solución es migrar la conexión del repositorio a SSH. Para que esto funcione, no basta con crear la llave SSH: también debemos cambiar la URL remota del repositorio.</p>



<p class="wp-block-paragraph">Además, si Git bloquea un cambio de rama por archivos modificados, debemos decidir si queremos conservar esos cambios con <code>git stash</code> o descartarlos con <code>git restore</code>.</p>



<p class="wp-block-paragraph">En proyectos Laravel, la recomendación más limpia es mantener los archivos de configuración versionados sin modificaciones locales y mover los valores específicos de cada ambiente al archivo <code>.env</code>. Esto evita conflictos, mejora el flujo de despliegue y reduce errores al trabajar con ramas en Git.</p>



<p class="wp-block-paragraph">Si necesitas ayuda para crear un proyecto de software en <strong>Maria Tech </strong>contamos con los expertos para apoyarte.</p>



<div class="wp-block-columns is-layout-flex wp-container-core-columns-is-layout-794e3cfa wp-block-columns-is-layout-flex">
<div class="wp-block-column is-layout-flow wp-block-column-is-layout-flow">
<div class="wp-block-buttons is-layout-flex wp-block-buttons-is-layout-flex">
<div class="wp-block-button"><a class="wp-block-button__link wp-element-button" href="https://wa.me/523414305984" target="_blank" rel="noreferrer noopener">(+52) 341-430-59-84</a></div>
</div>
</div>



<div class="wp-block-column is-layout-flow wp-block-column-is-layout-flow">
<div class="wp-block-buttons is-layout-flex wp-block-buttons-is-layout-flex">
<div class="wp-block-button"><a class="wp-block-button__link wp-element-button" href="mailto:info@mariatech.com.mx" target="_blank" rel="noreferrer noopener">info@mariatech.com.mx</a></div>
</div>
</div>
</div>
]]></content:encoded>
					
					<wfw:commentRss>https://www.mariatech.com.mx/blog/laravel-php/como-cambiar-la-autenticacion-de-github-de-access-token-a-ssh-en-un-proyecto-laravel/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Cómo usar Form Request Validation para formularios enviados con Ajax</title>
		<link>https://www.mariatech.com.mx/blog/laravel-php/como-usar-form-request-validation-para-formularios-enviados-con-ajax/</link>
					<comments>https://www.mariatech.com.mx/blog/laravel-php/como-usar-form-request-validation-para-formularios-enviados-con-ajax/#respond</comments>
		
		<dc:creator><![CDATA[Victor Santillan]]></dc:creator>
		<pubDate>Sun, 31 May 2026 04:38:25 +0000</pubDate>
				<category><![CDATA[Laravel / PHP]]></category>
		<guid isPermaLink="false">https://www.mariatech.com.mx/blog/?p=275</guid>

					<description><![CDATA[En este artículo veremos una forma práctica de trabajar con Request validation con formularios Ajax...]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">Cuando trabajamos con formularios en Laravel, lo más común es validar la información usando <strong>Form Requests</strong>. Esto permite mantener los controladores limpios y centralizar las reglas de validación en clases independientes.</p>



<p class="wp-block-paragraph">Sin embargo, cuando el formulario se envía por medio de <strong>Ajax</strong>, especialmente desde modales o componentes dinámicos, surge una duda común: ¿cómo devolvemos los errores de validación en formato JSON sin dejar de usar la estructura nativa de Laravel?</p>



<p class="wp-block-paragraph">En este artículo veremos una forma práctica de trabajar con <strong>Request validation</strong> con formularios Ajax, respuestas JSON y códigos HTTP correctos.</p>



<h2 class="wp-block-heading">El problema común</h2>



<p class="wp-block-paragraph">En un formulario tradicional, si la validación falla, Laravel redirige automáticamente al usuario a la página anterior y muestra los errores en la vista.</p>



<p class="wp-block-paragraph">Pero cuando usamos Ajax, no queremos una redirección. Queremos una respuesta JSON que podamos procesar desde JavaScript para mostrar los errores dentro de un modal, un toast o una alerta.</p>



<p class="wp-block-paragraph">Una solución rápida suele ser hacer la validación directamente en el controlador:</p>



<pre class="wp-block-code"><code>$validator = Validator::make($request-&gt;all(), $rules, $messages);

if ($validator-&gt;fails()) {
    return response()-&gt;json(&#91;
        'success' =&gt; false,
        'errors' =&gt; $validator-&gt;errors()-&gt;all(),
    ], 422);
}</code></pre>



<p class="wp-block-paragraph">Aunque esto funciona, tiene una desventaja importante: la validación queda mezclada dentro del controlador. Conforme el proyecto crece, esto vuelve el código más difícil de mantener.</p>



<h2 class="wp-block-heading">La mejor opción: seguir usando Form Request</h2>



<p class="wp-block-paragraph">Laravel permite seguir usando Form Requests aun cuando el formulario se envía por Ajax. Lo único que necesitamos es personalizar la respuesta cuando la validación falla.</p>



<p class="wp-block-paragraph">Primero, podemos crear un Form Request:</p>



<pre class="wp-block-code"><code>php artisan make:request Admin/User/StoreUserRequest</code></pre>



<p class="wp-block-paragraph">Luego definimos las reglas de validación:</p>



<pre class="wp-block-code"><code>namespace App\Http\Requests\Admin\User;

use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;

class StoreUserRequest extends FormRequest
{
    public function authorize(): bool
    {
        return true;
    }

    public function rules(): array
    {
        return &#91;
            'name' => &#91;'required', 'string', 'max:255'],
            'last_name' => &#91;'required', 'string', 'max:255'],
            'email' => &#91;'required', 'email', 'max:255', Rule::unique('users', 'email')],
            'phone' => &#91;'nullable', 'digits:10'],
            'id_profile_type' => &#91;'required', 'exists:profile_types,id'],
            'password' => &#91;'required', 'string', 'min:8', 'confirmed'],
            'enabled' => &#91;'nullable', 'boolean'],
        ];
    }
}</code></pre>



<h2 class="wp-block-heading">Personalizar la respuesta JSON de validación</h2>



<p class="wp-block-paragraph">Por defecto, Laravel devuelve los errores agrupados por campo:</p>



<pre class="wp-block-code"><code>{
    "message": "The given data was invalid.",
    "errors": {
        "email": &#91;
            "Este correo electrónico ya está registrado."
        ],
        "password": &#91;
            "La contraseña debe tener al menos 8 caracteres."
        ]
    }
}</code></pre>



<p class="wp-block-paragraph">Este formato es útil cuando queremos marcar errores campo por campo. Pero si nuestra interfaz usa modales y queremos mostrar una lista general de errores, puede ser más cómodo devolver una lista plana.</p>



<p class="wp-block-paragraph">Para eso podemos sobrescribir el método <code>failedValidation</code> dentro del StoreUserRequest:</p>



<pre class="wp-block-code"><code>use Illuminate\Contracts\Validation\Validator;
use Illuminate\Http\Exceptions\HttpResponseException;

protected function failedValidation(Validator $validator): void
{
    throw new HttpResponseException(response()-&gt;json(&#91;
        'success' =&gt; false,
        'message' =&gt; 'Hay errores en el formulario.',
        'errors' =&gt; $validator-&gt;errors()-&gt;all(),
    ], 422));
}</code></pre>



<p class="wp-block-paragraph">Con esto, la respuesta quedará así:</p>



<pre class="wp-block-code"><code>{
    "success": false,
    "message": "Hay errores en el formulario.",
    "errors": &#91;
        "El nombre es obligatorio.",
        "Este correo electrónico ya está registrado.",
        "La contraseña debe tener al menos 8 caracteres."
    ]
}</code></pre>



<h2 class="wp-block-heading">Controlador limpio usando el Form Request</h2>



<p class="wp-block-paragraph">Una vez que el Form Request está listo, el controlador puede mantenerse limpio:</p>



<pre class="wp-block-code"><code>use App\Http\Requests\Admin\User\StoreUserRequest;
use App\Models\User;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\Hash;
use Throwable;

public function store(StoreUserRequest $request): JsonResponse
{
    try {
        $validated = $request-&gt;validated();

        $user = User::create(&#91;
            'name' =&gt; $validated&#91;'name'],
            'last_name' =&gt; $validated&#91;'last_name'],
            'email' =&gt; $validated&#91;'email'],
            'phone' =&gt; $validated&#91;'phone'] ?? null,
            'id_profile_type' =&gt; $validated&#91;'id_profile_type'],
            'password' =&gt; Hash::make($validated&#91;'password']),
            'enabled' =&gt; $validated&#91;'enabled'] ?? 1,
        ]);

        return response()-&gt;json(&#91;
            'success' =&gt; true,
            'message' =&gt; 'Usuario registrado correctamente.',
            'user_id' =&gt; $user-&gt;id,
        ], 201);

    } catch (Throwable $e) {
        report($e);

        return response()-&gt;json(&#91;
            'success' =&gt; false,
            'message' =&gt; 'No fue posible registrar el usuario. Inténtalo nuevamente.',
        ], 500);
    }
}</code></pre>



<h2 class="wp-block-heading">Códigos HTTP recomendados</h2>



<p class="wp-block-paragraph">Es importante regresar códigos HTTP correctos para que el frontend pueda interpretar bien la respuesta.</p>



<ul class="wp-block-list">
<li><strong>200:</strong> operación correcta.</li>



<li><strong>201:</strong> registro creado correctamente.</li>



<li><strong>401:</strong> usuario no autenticado.</li>



<li><strong>403:</strong> usuario autenticado, pero sin permisos.</li>



<li><strong>404:</strong> recurso no encontrado.</li>



<li><strong>422:</strong> error de validación.</li>



<li><strong>500:</strong> error inesperado del servidor.</li>
</ul>



<p class="wp-block-paragraph">Para formularios Ajax, una convención práctica sería:</p>



<pre class="wp-block-code"><code>Validación fallida  → 422
Creación correcta   → 201
Actualización       → 200
Eliminación         → 200
No encontrado       → 404
Sin permisos        → 403
Error inesperado    → 500</code></pre>



<h2 class="wp-block-heading">Conclusión</h2>



<p class="wp-block-paragraph">Usar Form Requests con formularios Ajax es una forma limpia y profesional de validar datos en Laravel sin sacrificar la experiencia de usuario.</p>



<p class="wp-block-paragraph">La clave está en no llevar las reglas de validación al controlador, sino mantenerlas dentro del Request y personalizar la respuesta JSON cuando la validación falla.</p>



<p class="wp-block-paragraph">Con esta estructura logramos:</p>



<ul class="wp-block-list">
<li>Controladores más limpios.</li>



<li>Validaciones reutilizables.</li>



<li>Respuestas JSON consistentes.</li>



<li>Códigos HTTP correctos.</li>



<li>Mejor integración con formularios en modales.</li>



<li>Mejor experiencia de usuario usando Ajax.</li>
</ul>



<p class="wp-block-paragraph">Esta forma de trabajo es especialmente útil en paneles administrativos, sistemas internos, CRMs, ERPs y plataformas donde los formularios se muestran en modales o secciones dinámicas.</p>



<p class="wp-block-paragraph">Si necesitas ayuda para crear un proyecto de software en <strong>Maria Tech </strong>contamos con los expertos para apoyarte.</p>



<div class="wp-block-columns is-layout-flex wp-container-core-columns-is-layout-794e3cfa wp-block-columns-is-layout-flex">
<div class="wp-block-column is-layout-flow wp-block-column-is-layout-flow">
<div class="wp-block-buttons is-layout-flex wp-block-buttons-is-layout-flex">
<div class="wp-block-button"><a class="wp-block-button__link wp-element-button" href="https://wa.me/523414305984" target="_blank" rel="noreferrer noopener">(+52) 341-430-59-84</a></div>
</div>
</div>



<div class="wp-block-column is-layout-flow wp-block-column-is-layout-flow">
<div class="wp-block-buttons is-layout-flex wp-block-buttons-is-layout-flex">
<div class="wp-block-button"><a class="wp-block-button__link wp-element-button" href="mailto:info@mariatech.com.mx" target="_blank" rel="noreferrer noopener">info@mariatech.com.mx</a></div>
</div>
</div>
</div>



<p class="wp-block-paragraph"></p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.mariatech.com.mx/blog/laravel-php/como-usar-form-request-validation-para-formularios-enviados-con-ajax/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Cómo implementar recuperación de contraseña para administradores y clientes en Laravel 13</title>
		<link>https://www.mariatech.com.mx/blog/laravel-php/como-implementar-recuperacion-de-contrasena-para-administradores-y-clientes-en-laravel-13/</link>
					<comments>https://www.mariatech.com.mx/blog/laravel-php/como-implementar-recuperacion-de-contrasena-para-administradores-y-clientes-en-laravel-13/#respond</comments>
		
		<dc:creator><![CDATA[Victor Santillan]]></dc:creator>
		<pubDate>Sun, 24 May 2026 02:56:48 +0000</pubDate>
				<category><![CDATA[Laravel / PHP]]></category>
		<guid isPermaLink="false">https://www.mariatech.com.mx/blog/?p=267</guid>

					<description><![CDATA[En este artículo veremos cómo estructurar un proceso de recuperación de contraseña en Laravel para dos tipos de usuarios: administradores y clientes.]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">En proyectos web con áreas separadas para administradores y clientes, es común que ambos tipos de usuarios necesiten iniciar sesión, cerrar sesión y recuperar su contraseña. El reto técnico aparece cuando cada tipo de usuario vive en una tabla diferente y debe tener su propio flujo de autenticación.</p>



<p class="wp-block-paragraph">En este artículo veremos cómo estructurar un proceso de recuperación de contraseña en Laravel 13 para dos tipos de usuarios: <strong>administradores</strong> y <strong>clientes</strong>. El objetivo es mantener una arquitectura limpia, segura y fácil de mantener.</p>



<h2 class="wp-block-heading">Contexto del problema</h2>



<p class="wp-block-paragraph">Laravel Breeze instala un sistema de autenticación funcional, pero por defecto está pensado para trabajar principalmente con una sola tabla de usuarios, normalmente <code>users</code>.</p>



<p class="wp-block-paragraph">Sin embargo, en proyectos más completos podemos necesitar algo como esto:</p>



<ul class="wp-block-list">
<li><strong>Administradores:</strong> usuarios internos del sistema, almacenados en la tabla <code>users</code>.</li>



<li><strong>Clientes:</strong> usuarios externos que acceden al sitio o portal del cliente, almacenados en la tabla <code>customers</code>.</li>
</ul>



<p class="wp-block-paragraph">En este escenario no conviene mezclar ambos tipos de usuario en la misma tabla, porque normalmente tienen permisos, vistas, reglas de negocio y flujos diferentes.</p>



<h2 class="wp-block-heading">Estructura recomendada</h2>



<pre class="wp-block-code"><code>app/
├── Http/
│   ├── Controllers/
│   │   ├── Admin/
│   │   │   └── Auth/
│   │   │       ├── PasswordResetLinkController.php
│   │   │       └── NewPasswordController.php
│   │   └── Customer/
│   │       └── Auth/
│   │           ├── PasswordResetLinkController.php
│   │           └── NewPasswordController.php

resources/
├── views/
│   ├── admin/
│   │   └── auth/
│   │       ├── forgot-password.blade.php
│   │       └── reset-password.blade.php
│   └── customer/
│       └── auth/
│           ├── forgot-password.blade.php
│           └── reset-password.blade.php

routes/
├── admin.php
└── customer.php</code></pre>



<p class="wp-block-paragraph">La idea es separar cada flujo para evitar dependencias innecesarias y tener rutas, controladores, vistas y brokers independientes.</p>



<h2 class="wp-block-heading">Configuración de autenticación</h2>



<p class="wp-block-paragraph">En <code>config/auth.php</code> podemos definir un guard y un provider para cada tipo de usuario.</p>



<pre class="wp-block-code"><code>&lt;?php

use App\Models\User;
use App\Models\Customer;

return &#91;

    'defaults' =&gt; &#91;
        'guard' =&gt; 'web',
        'passwords' =&gt; 'users',
    ],

    'guards' =&gt; &#91;
        'web' =&gt; &#91;
            'driver' =&gt; 'session',
            'provider' =&gt; 'users',
        ],

        'customer' =&gt; &#91;
            'driver' =&gt; 'session',
            'provider' =&gt; 'customers',
        ],
    ],

    'providers' =&gt; &#91;
        'users' =&gt; &#91;
            'driver' =&gt; 'eloquent',
            'model' =&gt; User::class,
        ],

        'customers' =&gt; &#91;
            'driver' =&gt; 'eloquent',
            'model' =&gt; Customer::class,
        ],
    ],

    'passwords' =&gt; &#91;
        'users' =&gt; &#91;
            'provider' =&gt; 'users',
            'table' =&gt; 'password_reset_tokens',
            'expire' =&gt; 60,
            'throttle' =&gt; 60,
        ],

        'customers' =&gt; &#91;
            'provider' =&gt; 'customers',
            'table' =&gt; 'customer_password_reset_tokens',
            'expire' =&gt; 60,
            'throttle' =&gt; 60,
        ],
    ],

];</code></pre>



<p class="wp-block-paragraph">El punto clave está en la sección <code>passwords</code>. Ahí definimos dos brokers: <code>users</code> para administradores y <code>customers</code> para clientes. Cada broker usa su propio provider y su propia tabla de tokens.</p>



<h2 class="wp-block-heading">Migración para tokens de clientes</h2>



<pre class="wp-block-code"><code>Schema::create('customer_password_reset_tokens', function (Blueprint $table) {
    $table-&gt;string('email')-&gt;primary();
    $table-&gt;string('token');
    $table-&gt;timestamp('created_at')-&gt;nullable();
});</code></pre>



<p class="wp-block-paragraph">Esto permite que el flujo de administradores y clientes quede completamente separado.</p>



<h2 class="wp-block-heading">Rutas para administradores</h2>



<pre class="wp-block-code"><code>use App\Http\Controllers\Admin\Auth\PasswordResetLinkController;
use App\Http\Controllers\Admin\Auth\NewPasswordController;
use Illuminate\Support\Facades\Route;

Route::middleware('admin.guest')->group(function () {
    Route::get('/forgot-password', &#91;PasswordResetLinkController::class, 'create'])
        ->name('password.request');

    Route::post('/forgot-password', &#91;PasswordResetLinkController::class, 'store'])
        ->name('password.email');

    Route::get('/reset-password/{token}', &#91;NewPasswordController::class, 'create'])
        ->name('password.reset');

    Route::post('/reset-password', &#91;NewPasswordController::class, 'store'])
        ->name('password.store');
});</code></pre>



<h2 class="wp-block-heading">Rutas para clientes</h2>



<pre class="wp-block-code"><code>use App\Http\Controllers\Customer\Auth\PasswordResetLinkController;
use App\Http\Controllers\Customer\Auth\NewPasswordController;
use Illuminate\Support\Facades\Route;

Route::middleware('customer.guest')->group(function () {
    Route::get('/forgot-password', &#91;PasswordResetLinkController::class, 'create'])
        ->name('password.request');

    Route::post('/forgot-password', &#91;PasswordResetLinkController::class, 'store'])
        ->name('password.email');

    Route::get('/reset-password/{token}', &#91;NewPasswordController::class, 'create'])
        ->name('password.reset');

    Route::post('/reset-password', &#91;NewPasswordController::class, 'store'])
        ->name('password.store');
});</code></pre>



<h2 class="wp-block-heading">Controlador para enviar el enlace de recuperación</h2>



<p class="wp-block-paragraph">El controlador encargado de enviar el enlace de recuperación debe usar el broker correcto.</p>



<h3 class="wp-block-heading">Administrador</h3>



<pre class="wp-block-code"><code>namespace App\Http\Controllers\Admin\Auth;

use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Password;
use Illuminate\Validation\ValidationException;

class PasswordResetLinkController extends Controller
{
    public function create()
    {
        return view('admin.auth.forgot-password');
    }

    public function store(Request $request)
    {
        $request-&gt;validate(&#91;
            'email' =&gt; &#91;'required', 'email'],
        ]);

        $status = Password::broker('users')-&gt;sendResetLink(
            $request-&gt;only('email')
        );

        if ($status === Password::RESET_LINK_SENT) {
            return back()-&gt;with('status', __($status));
        }

        throw ValidationException::withMessages(&#91;
            'email' =&gt; __($status),
        ]);
    }
}</code></pre>



<h3 class="wp-block-heading">Cliente</h3>



<pre class="wp-block-code"><code>namespace App\Http\Controllers\Customer\Auth;

use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Password;
use Illuminate\Validation\ValidationException;

class PasswordResetLinkController extends Controller
{
    public function create()
    {
        return view('customer.auth.forgot-password');
    }

    public function store(Request $request)
    {
        $request-&gt;validate(&#91;
            'email' =&gt; &#91;'required', 'email'],
        ]);

        $status = Password::broker('customers')-&gt;sendResetLink(
            $request-&gt;only('email')
        );

        if ($status === Password::RESET_LINK_SENT) {
            return back()-&gt;with('status', __($status));
        }

        throw ValidationException::withMessages(&#91;
            'email' =&gt; __($status),
        ]);
    }
}</code></pre>



<p class="wp-block-paragraph">El cambio central es el broker:</p>



<pre class="wp-block-code"><code>Password::broker('users')      // Administradores
Password::broker('customers')  // Clientes</code></pre>



<h2 class="wp-block-heading">Controlador para actualizar la contraseña</h2>



<h3 class="wp-block-heading">Administrador</h3>



<pre class="wp-block-code"><code>namespace App\Http\Controllers\Admin\Auth;

use App\Http\Controllers\Controller;
use Illuminate\Auth\Events\PasswordReset;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Password;
use Illuminate\Support\Str;
use Illuminate\Validation\Rules;

class NewPasswordController extends Controller
{
    public function create(Request $request)
    {
        return view('admin.auth.reset-password', &#91;
            'request' =&gt; $request,
        ]);
    }

    public function store(Request $request)
    {
        $request-&gt;validate(&#91;
            'token' =&gt; &#91;'required'],
            'email' =&gt; &#91;'required', 'email'],
            'password' =&gt; &#91;'required', 'confirmed', Rules\Password::defaults()],
        ]);

        $status = Password::broker('users')-&gt;reset(
            $request-&gt;only('email', 'password', 'password_confirmation', 'token'),
            function ($user) use ($request) {
                $user-&gt;forceFill(&#91;
                    'password' =&gt; Hash::make($request-&gt;password),
                    'remember_token' =&gt; Str::random(60),
                ])-&gt;save();

                event(new PasswordReset($user));
            }
        );

        return $status === Password::PASSWORD_RESET
            ? redirect()-&gt;route('admin.login')-&gt;with('status', __($status))
            : back()-&gt;withInput($request-&gt;only('email'))
                -&gt;withErrors(&#91;'email' =&gt; __($status)]);
    }
}</code></pre>



<h3 class="wp-block-heading">Cliente</h3>



<pre class="wp-block-code"><code>namespace App\Http\Controllers\Customer\Auth;

use App\Http\Controllers\Controller;
use Illuminate\Auth\Events\PasswordReset;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Password;
use Illuminate\Support\Str;
use Illuminate\Validation\Rules;

class NewPasswordController extends Controller
{
    public function create(Request $request)
    {
        return view('customer.auth.reset-password', &#91;
            'request' =&gt; $request,
        ]);
    }

    public function store(Request $request)
    {
        $request-&gt;validate(&#91;
            'token' =&gt; &#91;'required'],
            'email' =&gt; &#91;'required', 'email'],
            'password' =&gt; &#91;'required', 'confirmed', Rules\Password::defaults()],
        ]);

        $status = Password::broker('customers')-&gt;reset(
            $request-&gt;only('email', 'password', 'password_confirmation', 'token'),
            function ($customer) use ($request) {
                $customer-&gt;forceFill(&#91;
                    'password' =&gt; Hash::make($request-&gt;password),
                    'remember_token' =&gt; Str::random(60),
                ])-&gt;save();

                event(new PasswordReset($customer));
            }
        );

        return $status === Password::PASSWORD_RESET
            ? redirect()-&gt;route('customer.login')-&gt;with('status', __($status))
            : back()-&gt;withInput($request-&gt;only('email'))
                -&gt;withErrors(&#91;'email' =&gt; __($status)]);
    }
}</code></pre>



<h2 class="wp-block-heading">Personalizar la URL del correo de recuperación</h2>



<p class="wp-block-paragraph">Un detalle importante es que Laravel genera el enlace de recuperación usando una notificación. Si no personalizamos ese enlace, puede terminar enviando al usuario al flujo equivocado.</p>



<p class="wp-block-paragraph">Para resolverlo, podemos agregar la lógica en <code>app/Providers/AppServiceProvider.php</code>.</p>



<pre class="wp-block-code"><code>namespace App\Providers;

use App\Models\Customer;
use App\Models\User;
use Illuminate\Auth\Notifications\ResetPassword;
use Illuminate\Support\Facades\URL;
use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
    public function register(): void
    {
        //
    }

    public function boot(): void
    {
        ResetPassword::createUrlUsing(function (object $notifiable, string $token): string {
            if ($notifiable instanceof Customer) {
                return URL::route('customer.password.reset', &#91;
                    'token' =&gt; $token,
                    'email' =&gt; $notifiable-&gt;getEmailForPasswordReset(),
                ]);
            }

            if ($notifiable instanceof User) {
                return URL::route('admin.password.reset', &#91;
                    'token' =&gt; $token,
                    'email' =&gt; $notifiable-&gt;getEmailForPasswordReset(),
                ]);
            }

            return URL::route('customer.password.reset', &#91;
                'token' =&gt; $token,
                'email' =&gt; $notifiable-&gt;getEmailForPasswordReset(),
            ]);
        });
    }
}</code></pre>



<p class="wp-block-paragraph">Con esto, cuando el modelo sea <code>User</code>, Laravel enviará al flujo de administrador. Cuando el modelo sea <code>Customer</code>, enviará al flujo de cliente.</p>



<h2 class="wp-block-heading">Vistas de recuperación de contraseña</h2>



<p class="wp-block-paragraph">En cada formulario es importante apuntar a la ruta correcta.</p>



<h3 class="wp-block-heading">Formulario para solicitar enlace</h3>



<p class="wp-block-paragraph">Administrador:</p>



<pre class="wp-block-code"><code>&lt;form method="POST" action="{{ route('admin.password.email') }}">
    @csrf
    &lt;input type="email" name="email" required>
    &lt;button type="submit">
        Enviar enlace
    &lt;/button>
&lt;/form></code></pre>



<p class="wp-block-paragraph">Cliente:</p>



<pre class="wp-block-code"><code>&lt;form method="POST" action="{{ route('customer.password.email') }}">
    @csrf
    &lt;input type="email" name="email" required>
    &lt;button type="submit">
        Enviar enlace
    &lt;/button>
&lt;/form></code></pre>



<h3 class="wp-block-heading">Formulario para crear nueva contraseña</h3>



<p class="wp-block-paragraph">Administrador:</p>



<pre class="wp-block-code"><code>&lt;form method="POST" action="{{ route('admin.password.store') }}">
    @csrf
    &lt;input type="hidden" name="token" value="{{ $request->route('token') }}">
    &lt;input type="email" name="email" value="{{ old('email', $request->email) }}" required>
    &lt;input type="password" name="password" required>
    &lt;input type="password" name="password_confirmation" required>
    &lt;button type="submit">
        Actualizar contraseña
    &lt;/button>
&lt;/form></code></pre>



<p class="wp-block-paragraph">Cliente:</p>



<pre class="wp-block-code"><code>&lt;form method="POST" action="{{ route('customer.password.store') }}">
    @csrf
    &lt;input type="hidden" name="token" value="{{ $request->route('token') }}">
    &lt;input type="email" name="email" value="{{ old('email', $request->email) }}" required>
    &lt;input type="password" name="password" required>
    &lt;input type="password" name="password_confirmation" required>
    &lt;button type="submit">
        Actualizar contraseña
    &lt;/button>
&lt;/form></code></pre>



<h2 class="wp-block-heading">Buenas prácticas</h2>



<ul class="wp-block-list">
<li>Usar modelos separados cuando los usuarios internos y externos tienen responsabilidades diferentes.</li>



<li>Separar rutas, vistas y controladores por tipo de usuario.</li>



<li>Usar un broker de contraseña diferente para cada tabla.</li>



<li>Personalizar la URL del correo para evitar que el usuario llegue al flujo equivocado.</li>



<li>No depender completamente de la estructura por defecto de Breeze cuando el proyecto requiere múltiples áreas de autenticación.</li>



<li>Mantener nombres claros como <code>Admin</code> y <code>Customer</code> para facilitar el mantenimiento.</li>
</ul>



<h2 class="wp-block-heading">Conclusión</h2>



<p class="wp-block-paragraph">La recuperación de contraseña para múltiples tipos de usuarios en Laravel requiere separar correctamente guards, providers, brokers, rutas, controladores y vistas. Aunque Breeze ofrece una buena base inicial, en proyectos con administradores y clientes conviene tomar el control del flujo para evitar cruces entre autenticaciones.</p>



<p class="wp-block-paragraph">La clave está en usar un broker distinto para cada tipo de usuario:</p>



<pre class="wp-block-code"><code>Password::broker('users')      // Administradores
Password::broker('customers')  // Clientes</code></pre>



<p class="wp-block-paragraph">Y personalizar el enlace generado por correo desde <code>AppServiceProvider</code>, para que cada usuario llegue al formulario correcto.</p>



<p class="wp-block-paragraph">Con esta estructura, el sistema queda más limpio, seguro y preparado para crecer.</p>



<p class="wp-block-paragraph">¡Si te ha servido de algo este artículo no olvides dejarnos un comentario!</p>



<p class="wp-block-paragraph"></p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.mariatech.com.mx/blog/laravel-php/como-implementar-recuperacion-de-contrasena-para-administradores-y-clientes-en-laravel-13/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Cómo implementar autenticación para dos tipos de usuarios en Laravel: administradores y clientes</title>
		<link>https://www.mariatech.com.mx/blog/laravel-php/como-implementar-autenticacion-para-dos-tipos-de-usuarios-en-laravel-administradores-y-clientes/</link>
					<comments>https://www.mariatech.com.mx/blog/laravel-php/como-implementar-autenticacion-para-dos-tipos-de-usuarios-en-laravel-administradores-y-clientes/#respond</comments>
		
		<dc:creator><![CDATA[Victor Santillan]]></dc:creator>
		<pubDate>Wed, 20 May 2026 04:00:56 +0000</pubDate>
				<category><![CDATA[Laravel / PHP]]></category>
		<guid isPermaLink="false">https://www.mariatech.com.mx/blog/?p=259</guid>

					<description><![CDATA[En este artículo veremos cómo estructurar en Laravel una autenticación separada para dos tipos de usuarios: administradores y clientes...]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">En proyectos web donde existen diferentes tipos de usuarios, una sola autenticación puede quedarse corta. Un caso común es cuando el sistema tiene un panel administrativo para colaboradores y, al mismo tiempo, una página publica donde los clientes pueden iniciar sesión para consultar o gestionar su información.</p>



<p class="wp-block-paragraph">En este artículo veremos cómo estructurar en Laravel una autenticación separada para dos tipos de usuarios: <strong>administradores</strong> y <strong>clientes</strong>. La idea es mantener una arquitectura limpia, segura y fácil de mantener.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">Contexto del proyecto</h2>



<p class="wp-block-paragraph">El escenario es el siguiente:</p>



<ul class="wp-block-list">
<li>El administrador accede desde una URL privada, por ejemplo: <code>/admin</code>.</li>



<li>Los clientes acceden desde el sitio público, por ejemplo: <code>/</code>.</li>



<li>Los administradores se almacenan en la tabla <code>users</code>.</li>



<li>Los clientes se almacenan en la tabla <code>customers</code>.</li>



<li>Cada sección usa vistas, layouts, assets y controladores separados.</li>



<li>Se utiliza Laravel Breeze como punto de partida, pero se adapta la estructura para soportar dos autenticaciones independientes.</li>
</ul>



<p class="wp-block-paragraph">La decisión principal fue no mezclar administradores y clientes en una misma tabla, ya que tienen responsabilidades, permisos y flujos completamente diferentes.</p>



<h2 class="wp-block-heading">Estructura recomendada del proyecto</h2>



<p class="wp-block-paragraph">Una estructura limpia puede organizarse así:</p>



<pre class="wp-block-code"><code>app/
├── Http/
│   ├── Controllers/
│   │   ├── Admin/
│   │   │   ├── Auth/
│   │   │   └── DashboardController.php
│   │   │
│   │   ├── Customer/
│   │   │   ├── Auth/
│   │   │   └── AccountController.php
│   │   │
│   │   └── Controller.php
│   │
│   ├── Requests/
│   │   ├── Admin/
│   │   │   └── Auth/
│   │   │       └── LoginRequest.php
│   │   │
│   │   └── Customer/
│   │       └── Auth/
│   │           └── LoginRequest.php
│
resources/
├── views/
│   ├── admin/
│   │   ├── auth/
│   │   ├── layouts/
│   │   └── dashboard/
│   │
│   └── customer/
│       ├── auth/
│       ├── layouts/
│       └── account/
│
routes/
├── admin.php
├── customer.php
├── web.php
└── console.php

public/
└── assets/
    ├── admin/
    └── customer/</code></pre>



<p class="wp-block-paragraph">Esta separación permite que cada sección tenga su propia lógica, sus propios recursos visuales y sus propios middlewares.</p>



<h2 class="wp-block-heading">Configuración de guards y providers</h2>



<p class="wp-block-paragraph">Laravel permite manejar múltiples sistemas de autenticación mediante <strong>guards</strong> y <strong>providers</strong>.</p>



<p class="wp-block-paragraph">En este caso usamos:</p>



<ul class="wp-block-list">
<li><code>web</code> para administradores.</li>



<li><code>customer</code> para clientes.</li>
</ul>



<p class="wp-block-paragraph">La configuración se realiza en <code>config/auth.php</code>:</p>



<pre class="wp-block-code"><code>&lt;?php

use App\Models\User;
use App\Models\Customer;

return &#91;

    'defaults' =&gt; &#91;
        'guard' =&gt; env('AUTH_GUARD', 'web'),
        'passwords' =&gt; env('AUTH_PASSWORD_BROKER', 'users'),
    ],

    'guards' =&gt; &#91;
        'web' =&gt; &#91;
            'driver' =&gt; 'session',
            'provider' =&gt; 'users',
        ],

        'customer' =&gt; &#91;
            'driver' =&gt; 'session',
            'provider' =&gt; 'customers',
        ],
    ],

    'providers' =&gt; &#91;
        'users' =&gt; &#91;
            'driver' =&gt; 'eloquent',
            'model' =&gt; User::class,
        ],

        'customers' =&gt; &#91;
            'driver' =&gt; 'eloquent',
            'model' =&gt; Customer::class,
        ],
    ],

    'passwords' =&gt; &#91;
        'users' =&gt; &#91;
            'provider' =&gt; 'users',
            'table' =&gt; env('AUTH_PASSWORD_RESET_TOKEN_TABLE', 'password_reset_tokens'),
            'expire' =&gt; 60,
            'throttle' =&gt; 60,
        ],

        'customers' =&gt; &#91;
            'provider' =&gt; 'customers',
            'table' =&gt; env('CUSTOMER_AUTH_PASSWORD_RESET_TOKEN_TABLE', 'customer_password_reset_tokens'),
            'expire' =&gt; 60,
            'throttle' =&gt; 60,
        ],
    ],

    'password_timeout' =&gt; env('AUTH_PASSWORD_TIMEOUT', 10800),

];</code></pre>



<p class="wp-block-paragraph">Un detalle importante es no usar la misma variable <code>AUTH_MODEL</code> para ambos providers, porque podría provocar que los clientes intenten autenticarse con el modelo de usuarios administrativos.</p>



<h2 class="wp-block-heading">Modelo Customer autenticable</h2>



<p class="wp-block-paragraph">El modelo de cliente no debe extender directamente de <code>Model</code>, sino de <code>Authenticatable</code>, igual que el modelo <code>User</code>.</p>



<pre class="wp-block-code"><code>&lt;?php

namespace App\Models;

use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;

class Customer extends Authenticatable
{
    use Notifiable;

    protected $guard = 'customer';

    protected $fillable = &#91;
        'name',
        'last_name',
        'phone',
        'email',
        'password',
        'enabled',
    ];

    protected $hidden = &#91;
        'password',
        'remember_token',
    ];
}</code></pre>



<p class="wp-block-paragraph">Esto permite que Laravel pueda autenticar clientes usando el guard <code>customer</code>.</p>



<h2 class="wp-block-heading">Migraciones principales</h2>



<p class="wp-block-paragraph">Para los clientes se puede crear una tabla independiente:</p>



<pre class="wp-block-code"><code>Schema::create('customers', function (Blueprint $table) {
    $table-&gt;id();
    $table-&gt;string('name');
    $table-&gt;string('last_name');
    $table-&gt;char('phone', 10)-&gt;nullable();
    $table-&gt;string('email')-&gt;unique();
    $table-&gt;timestamp('email_verified_at')-&gt;nullable();
    $table-&gt;string('password');
    $table-&gt;rememberToken();
    $table-&gt;tinyInteger('enabled')-&gt;default(1);
    $table-&gt;timestamps();
});</code></pre>



<p class="wp-block-paragraph">También se puede manejar una tabla independiente para recuperación de contraseñas:</p>



<pre class="wp-block-code"><code>Schema::create('customer_password_reset_tokens', function (Blueprint $table) {
    $table-&gt;string('email')-&gt;primary();
    $table-&gt;string('token');
    $table-&gt;timestamp('created_at')-&gt;nullable();
});</code></pre>



<p class="wp-block-paragraph">En cuanto a sesiones, no es obligatorio crear una tabla diferente para clientes. Laravel usa la tabla <code>sessions</code> para guardar la sesión del navegador, no una sesión independiente por guard.</p>



<h2 class="wp-block-heading">Rutas separadas</h2>



<p class="wp-block-paragraph">Una buena práctica es separar las rutas del administrador y del cliente.</p>



<p class="wp-block-paragraph">Ejemplo de <code>routes/admin.php</code>:</p>



<pre class="wp-block-code"><code>use Illuminate\Support\Facades\Route;
use App\Http\Controllers\Admin\Auth\AuthenticatedSessionController;
use App\Http\Controllers\Admin\DashboardController;

Route::middleware('admin.guest')->group(function () {
    Route::get('/login', &#91;AuthenticatedSessionController::class, 'create'])
        ->name('login');

    Route::post('/login', &#91;AuthenticatedSessionController::class, 'store'])
        ->name('login.store');
});

Route::middleware('admin.auth')->group(function () {
    Route::get('/dashboard', &#91;DashboardController::class, 'index'])
        ->name('dashboard');

    Route::post('/logout', &#91;AuthenticatedSessionController::class, 'destroy'])
        ->name('logout');
});</code></pre>



<p class="wp-block-paragraph">Ejemplo de <code>routes/customer.php</code>:</p>



<pre class="wp-block-code"><code>use Illuminate\Support\Facades\Route;
use App\Http\Controllers\Customer\Auth\AuthenticatedSessionController;
use App\Http\Controllers\Customer\HomeController;
use App\Http\Controllers\Customer\AccountController;

Route::get('/', &#91;HomeController::class, 'index'])
    ->name('home');

Route::middleware('customer.guest')->group(function () {
    Route::get('/login', &#91;AuthenticatedSessionController::class, 'create'])
        ->name('login');

    Route::post('/login', &#91;AuthenticatedSessionController::class, 'store'])
        ->name('login.store');
});

Route::middleware('customer.auth')->group(function () {
    Route::get('/mi-cuenta', &#91;AccountController::class, 'index'])
        ->name('account');

    Route::post('/logout', &#91;AuthenticatedSessionController::class, 'destroy'])
        ->name('logout');
});</code></pre>



<h2 class="wp-block-heading">Cargar rutas desde bootstrap/app.php</h2>



<p class="wp-block-paragraph">En Laravel moderno, las rutas adicionales pueden registrarse desde <code>bootstrap/app.php</code>:</p>



<pre class="wp-block-code"><code>use Illuminate\Support\Facades\Route;

->withRouting(
    web: __DIR__.'/../routes/web.php',
    commands: __DIR__.'/../routes/console.php',
    then: function () {
        Route::middleware('web')
            ->prefix('admin')
            ->name('admin.')
            ->group(base_path('routes/admin.php'));

        Route::middleware('web')
            ->name('customer.')
            ->group(base_path('routes/customer.php'));
    },
)</code></pre>



<p class="wp-block-paragraph">Con esta configuración se obtienen rutas como:</p>



<ul class="wp-block-list">
<li><code>/admin/login</code> → <code>admin.login</code></li>



<li><code>/admin/dashboard</code> → <code>admin.dashboard</code></li>



<li><code>/login</code> → <code>customer.login</code></li>



<li><code>/mi-cuenta</code> → <code>customer.account</code></li>
</ul>



<h2 class="wp-block-heading">Middlewares propios por tipo de usuario</h2>



<p class="wp-block-paragraph">Para evitar lógica condicional dentro de <code>bootstrap/app.php</code>, se pueden crear middlewares específicos para cada sección.</p>



<p class="wp-block-paragraph">Por ejemplo:</p>



<ul class="wp-block-list">
<li><code>admin.auth</code>: valida que el usuario admin esté autenticado y activo.</li>



<li><code>admin.guest</code>: evita que un admin autenticado vuelva al login.</li>



<li><code>customer.auth</code>: valida que el cliente esté autenticado y activo.</li>



<li><code>customer.guest</code>: evita que un cliente autenticado vuelva al login.</li>
</ul>



<p class="wp-block-paragraph">Middleware de administrador autenticado:</p>



<pre class="wp-block-code"><code>&lt;?php

namespace App\Http\Middleware\Admin;

use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Symfony\Component\HttpFoundation\Response;

class EnsureAdminIsAuthenticated
{
    public function handle(Request $request, Closure $next): Response
    {
        if (! Auth::guard('web')-&gt;check()) {
            return redirect()-&gt;route('admin.login');
        }

        $user = Auth::guard('web')-&gt;user();

        if (! $user-&gt;enabled) {
            Auth::guard('web')-&gt;logout();

            $request-&gt;session()-&gt;invalidate();
            $request-&gt;session()-&gt;regenerateToken();

            return redirect()
                -&gt;route('admin.login')
                -&gt;withErrors(&#91;
                    'email' =&gt; 'Tu cuenta se encuentra deshabilitada.',
                ]);
        }

        return $next($request);
    }
}</code></pre>



<p class="wp-block-paragraph">Middleware de cliente autenticado:</p>



<pre class="wp-block-code"><code>&lt;?php

namespace App\Http\Middleware\Customer;

use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Symfony\Component\HttpFoundation\Response;

class EnsureCustomerIsAuthenticated
{
    public function handle(Request $request, Closure $next): Response
    {
        if (! Auth::guard('customer')-&gt;check()) {
            return redirect()-&gt;route('customer.login');
        }

        $customer = Auth::guard('customer')-&gt;user();

        if (! $customer-&gt;enabled) {
            Auth::guard('customer')-&gt;logout();

            $request-&gt;session()-&gt;invalidate();
            $request-&gt;session()-&gt;regenerateToken();

            return redirect()
                -&gt;route('customer.login')
                -&gt;withErrors(&#91;
                    'email' =&gt; 'Tu cuenta se encuentra deshabilitada.',
                ]);
        }

        return $next($request);
    }
}</code></pre>



<p class="wp-block-paragraph">Después se registran los alias de middleware en <code>bootstrap/app.php</code>:</p>



<pre class="wp-block-code"><code>use Illuminate\Foundation\Configuration\Middleware;
use App\Http\Middleware\Admin\EnsureAdminIsAuthenticated;
use App\Http\Middleware\Admin\RedirectIfAdminAuthenticated;
use App\Http\Middleware\Customer\EnsureCustomerIsAuthenticated;
use App\Http\Middleware\Customer\RedirectIfCustomerAuthenticated;

->withMiddleware(function (Middleware $middleware): void {
    $middleware->alias(&#91;
        'admin.auth' => EnsureAdminIsAuthenticated::class,
        'admin.guest' => RedirectIfAdminAuthenticated::class,

        'customer.auth' => EnsureCustomerIsAuthenticated::class,
        'customer.guest' => RedirectIfCustomerAuthenticated::class,
    ]);
})</code></pre>



<h2 class="wp-block-heading">LoginRequest para clientes</h2>



<p class="wp-block-paragraph">Uno de los errores más comunes al trabajar con múltiples guards es usar <code>Auth::attempt()</code> sin especificar guard. Cuando se hace eso, Laravel utiliza el guard por defecto, normalmente <code>web</code>.</p>



<p class="wp-block-paragraph">Para el login de clientes debe usarse:</p>



<pre class="wp-block-code"><code>Auth::guard('customer')-&gt;attempt($credentials, $this-&gt;boolean('remember'))</code></pre>



<p class="wp-block-paragraph">Ejemplo:</p>



<pre class="wp-block-code"><code>public function authenticate(): void
{
    $this-&gt;ensureIsNotRateLimited();

    $credentials = $this-&gt;only('email', 'password');
    $credentials&#91;'enabled'] = 1;

    if (! Auth::guard('customer')-&gt;attempt($credentials, $this-&gt;boolean('remember'))) {
        RateLimiter::hit($this-&gt;throttleKey());

        throw ValidationException::withMessages(&#91;
            'email' =&gt; trans('auth.failed'),
        ]);
    }

    Auth::guard('web')-&gt;logout();

    RateLimiter::clear($this-&gt;throttleKey());
}</code></pre>



<p class="wp-block-paragraph">En este ejemplo, después de iniciar sesión como cliente, se cierra cualquier sesión activa del administrador en el mismo navegador.</p>



<h2 class="wp-block-heading">LoginRequest para administradores</h2>



<p class="wp-block-paragraph">Para el administrador se hace lo mismo, pero usando el guard <code>web</code>:</p>



<pre class="wp-block-code"><code>public function authenticate(): void
{
    $this-&gt;ensureIsNotRateLimited();

    $credentials = $this-&gt;only('email', 'password');
    $credentials&#91;'enabled'] = 1;

    if (! Auth::guard('web')-&gt;attempt($credentials, $this-&gt;boolean('remember'))) {
        RateLimiter::hit($this-&gt;throttleKey());

        throw ValidationException::withMessages(&#91;
            'email' =&gt; trans('auth.failed'),
        ]);
    }

    Auth::guard('customer')-&gt;logout();

    RateLimiter::clear($this-&gt;throttleKey());
}</code></pre>



<p class="wp-block-paragraph">De esta manera se evita que el mismo navegador mantenga una sesión de administrador y una sesión de cliente al mismo tiempo.</p>



<h2 class="wp-block-heading">Uso correcto en Blade</h2>



<p class="wp-block-paragraph">Otro detalle importante es que la directiva <code>@auth</code> usa el guard por defecto si no se especifica uno. Por eso, en vistas de cliente se debe indicar explícitamente el guard.</p>



<p class="wp-block-paragraph">Para clientes:</p>



<pre class="wp-block-code"><code>@auth('customer')
    Hola, {{ auth('customer')->user()->name }}
@endauth

@guest('customer')
    &lt;a href="{{ route('customer.login') }}">Iniciar sesión&lt;/a>
@endguest</code></pre>



<p class="wp-block-paragraph">Para administradores:</p>



<pre class="wp-block-code"><code>@auth('web')
    Hola, {{ auth('web')->user()->name }}
@endauth

@guest('web')
    &lt;a href="{{ route('admin.login') }}">Iniciar sesión&lt;/a>
@endguest</code></pre>



<h2 class="wp-block-heading">Sesiones en base de datos</h2>



<p class="wp-block-paragraph">Cuando se usa <code>SESSION_DRIVER=database</code>, Laravel guarda la sesión del navegador en la tabla <code>sessions</code>. Es importante entender que no se crea una sesión independiente por guard.</p>



<p class="wp-block-paragraph">Si se inicia sesión como administrador y después como cliente en el mismo navegador, Laravel puede seguir usando el mismo registro de sesión. Lo importante es que el guard correcto esté autenticado y el otro haya sido cerrado.</p>



<p class="wp-block-paragraph">Para validar:</p>



<pre class="wp-block-code"><code>auth('web')-&gt;check();   // Admin
auth('customer')-&gt;check(); // Cliente</code></pre>



<p class="wp-block-paragraph">También es normal que el registro en la tabla <code>sessions</code> permanezca por un tiempo aunque el usuario haya cerrado sesión. Laravel limpia las sesiones expiradas según su configuración de expiración.</p>



<h2 class="wp-block-heading">Extras</h2>



<h3 class="wp-block-heading">Assets separados con Vite</h3>



<p class="wp-block-paragraph">Si cada sección utiliza un template diferente, también conviene separar los assets.</p>



<pre class="wp-block-code"><code>resources/
├── admin/
│   ├── css/
│   │   └── app.css
│   └── js/
│       └── app.js
│
└── customer/
    ├── css/
    │   └── app.css
    └── js/
        └── app.js</code></pre>



<p class="wp-block-paragraph">Ejemplo de <code>vite.config.js</code>:</p>



<pre class="wp-block-code"><code>import { defineConfig } from 'vite';
import laravel from 'laravel-vite-plugin';

export default defineConfig({
    plugins: &#91;
        laravel({
            input: &#91;
                'resources/admin/css/app.css',
                'resources/admin/js/app.js',

                'resources/customer/css/app.css',
                'resources/customer/js/app.js',
            ],
            refresh: true,
        }),
    ],
});</code></pre>



<p class="wp-block-paragraph">En el layout de administrador:</p>



<pre class="wp-block-code"><code>@vite(&#91;
    'resources/admin/css/app.css',
    'resources/admin/js/app.js'
])</code></pre>



<p class="wp-block-paragraph">En el layout de cliente:</p>



<pre class="wp-block-code"><code>@vite(&#91;
    'resources/customer/css/app.css',
    'resources/customer/js/app.js'
])</code></pre>



<h2 class="wp-block-heading">Conclusión</h2>



<p class="wp-block-paragraph">Implementar autenticación para dos tipos de usuarios en Laravel requiere separar responsabilidades desde el inicio. La clave está en no mezclar modelos, guards, rutas ni vistas.</p>



<p class="wp-block-paragraph">Una arquitectura limpia para este caso sería:</p>



<ul class="wp-block-list">
<li><code>users</code> para administradores.</li>



<li><code>customers</code> para clientes.</li>



<li><code>web</code> como guard de administradores.</li>



<li><code>customer</code> como guard de clientes.</li>



<li><code>resources/views/admin</code> para el panel administrativo.</li>



<li><code>resources/views/customer</code> para el portal público.</li>



<li><code>routes/admin.php</code> y <code>routes/customer.php</code> para separar rutas.</li>



<li>Middlewares propios para controlar accesos y redirecciones.</li>
</ul>



<p class="wp-block-paragraph">Esta estructura evita confusiones, mejora la seguridad y permite que el proyecto crezca de forma ordenada.</p>



<p class="wp-block-paragraph">En sistemas donde existen usuarios internos y clientes finales, separar la autenticación no solo es una buena práctica técnica: también facilita el mantenimiento, las pruebas y la evolución del proyecto.</p>



<p class="wp-block-paragraph"></p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.mariatech.com.mx/blog/laravel-php/como-implementar-autenticacion-para-dos-tipos-de-usuarios-en-laravel-administradores-y-clientes/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Cómo instalar y usar una versión diferente de PHP con Laravel Valet en Mac</title>
		<link>https://www.mariatech.com.mx/blog/laravel-php/como-instalar-y-usar-una-version-diferente-de-php-con-laravel-valet-en-mac/</link>
					<comments>https://www.mariatech.com.mx/blog/laravel-php/como-instalar-y-usar-una-version-diferente-de-php-con-laravel-valet-en-mac/#respond</comments>
		
		<dc:creator><![CDATA[Victor Santillan]]></dc:creator>
		<pubDate>Mon, 04 May 2026 06:35:18 +0000</pubDate>
				<category><![CDATA[Laravel / PHP]]></category>
		<guid isPermaLink="false">https://www.mariatech.com.mx/blog/?p=248</guid>

					<description><![CDATA[Cuando trabajamos con Laravel en macOS usando Laravel Valet, una de las ventajas más importantes es...]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">Cuando trabajamos con Laravel en macOS usando <strong>Laravel Valet</strong>, una de las ventajas más importantes es que podemos cambiar la versión de PHP de forma relativamente sencilla.</p>



<p class="wp-block-paragraph">Esto es muy útil cuando un proyecto requiere una versión específica de PHP o cuando algún paquete de Composer todavía no es compatible con la versión más reciente.</p>



<p class="wp-block-paragraph">En este artículo veremos cómo instalar y usar una versión diferente de PHP con Laravel Valet, tomando como ejemplo un problema común al instalar paquetes como <code>maatwebsite/excel</code>.</p>



<h2 class="wp-block-heading">El problema: incompatibilidad entre paquetes y versión de PHP</h2>



<p class="wp-block-paragraph">En nuestro caso, al intentar instalar <code>maatwebsite/excel</code>, Composer mostró un error relacionado con <code>phpoffice/phpspreadsheet</code>:</p>



<pre class="wp-block-code"><code>phpoffice/phpspreadsheet 1.30.4 requires php &gt;=7.4.0 &lt;8.5.0 -&gt; your php version (8.5.5) does not satisfy that requirement.</code></pre>



<p class="wp-block-paragraph">El mensaje es bastante claro: la versión instalada de PHP era <strong>8.5.5</strong>, pero <code>phpoffice/phpspreadsheet</code> en la versión requerida por el proyecto solo permite PHP menor a <strong>8.5.0</strong>.</p>



<p class="wp-block-paragraph">Esto no significa que Laravel Valet esté mal configurado. Simplemente quiere decir que el paquete aún no es compatible con la versión de PHP que estamos usando.</p>



<p class="wp-block-paragraph"><code>maatwebsite/excel</code> utiliza <code>phpoffice/phpspreadsheet</code> como dependencia para trabajar con archivos Excel y CSV.</p>



<h2 class="wp-block-heading">¿Por qué conviene cambiar de PHP en lugar de forzar Composer?</h2>



<p class="wp-block-paragraph">Cuando Composer marca un error de compatibilidad, una tentación común es usar banderas como:</p>



<pre class="wp-block-code"><code>composer require paquete --ignore-platform-reqs</code></pre>



<p class="wp-block-paragraph">Aunque esto puede “saltarse” temporalmente la validación, no es recomendable como solución principal.</p>



<p class="wp-block-paragraph">El problema es que Composer dejaría instalar un paquete que oficialmente no soporta tu versión de PHP. Eso puede provocar errores más adelante en ejecución, especialmente cuando se trata de librerías que manipulan archivos, fechas, memoria, formatos o extensiones de PHP.</p>



<p class="wp-block-paragraph">La solución más limpia es usar una versión de PHP compatible con el proyecto.</p>



<p class="wp-block-paragraph">En este caso, la solución fue bajar de PHP 8.5 a <strong>PHP 8.4</strong>.</p>



<h2 class="wp-block-heading">Verificar la versión actual de PHP</h2>



<p class="wp-block-paragraph">Antes de cambiar nada, conviene revisar qué versión está usando nuestra terminal:</p>



<pre class="wp-block-code"><code>php -v</code></pre>



<p class="wp-block-paragraph">También podemos revisar qué versión de PHP está usando Valet:</p>



<pre class="wp-block-code"><code>valet which-php</code></pre>



<p class="wp-block-paragraph">Esto ayuda a confirmar si Composer, Laravel y Valet están trabajando con la versión esperada.</p>



<h2 class="wp-block-heading">Instalar PHP 8.4 con Homebrew</h2>



<p class="wp-block-paragraph">Si estamos usando Valet en macOS, lo normal es instalar PHP mediante Homebrew.</p>



<p class="wp-block-paragraph">Para instalar PHP 8.4 ejecutamos:</p>



<pre class="wp-block-code"><code>brew install php@8.4</code></pre>



<p class="wp-block-paragraph">Si ya lo teníamos instalado, podemos asegurarnos de que esté actualizado con:</p>



<pre class="wp-block-code"><code>brew update
brew upgrade php@8.4</code></pre>



<h2 class="wp-block-heading">Cambiar Laravel Valet a PHP 8.4</h2>



<p class="wp-block-paragraph">Laravel Valet permite cambiar la versión global de PHP con el comando <code>valet use</code>.</p>



<p class="wp-block-paragraph">Para cambiar Valet a PHP 8.4:</p>



<pre class="wp-block-code"><code>valet use php@8.4</code></pre>



<p class="wp-block-paragraph">Se reiniciará Valet de manera automática, si no lo hace lo reiniciamos nosotros:</p>



<pre class="wp-block-code"><code>valet restart</code></pre>



<p class="wp-block-paragraph">Y validamos:</p>



<pre class="wp-block-code"><code>php -v
valet which-php</code></pre>



<p class="wp-block-paragraph">La salida debería mostrar una versión PHP 8.4.x.</p>



<h2 class="wp-block-heading">Si la terminal sigue usando otra versión</h2>



<p class="wp-block-paragraph">Puede pasar que Valet ya esté usando PHP 8.4, pero la terminal siga apuntando a PHP 8.5.</p>



<p class="wp-block-paragraph">En ese caso, podemos forzar el link de Homebrew:</p>



<pre class="wp-block-code"><code>brew unlink php
brew link --force --overwrite php@8.4</code></pre>



<p class="wp-block-paragraph">Luego volvemos a validar:</p>



<pre class="wp-block-code"><code>php -v</code></pre>



<p class="wp-block-paragraph">Si todo está correcto, Composer ya debería detectar PHP 8.4.</p>



<h2 class="wp-block-heading">Instalar nuevamente el paquete</h2>



<p class="wp-block-paragraph">Una vez cambiada la versión de PHP, regresamos al proyecto Laravel:</p>



<pre class="wp-block-code"><code>cd ~/Developments/LaravelProjects/nombre-del-proyecto</code></pre>



<p class="wp-block-paragraph">Y ejecutamos:</p>



<pre class="wp-block-code"><code>composer require maatwebsite/excel</code></pre>



<p class="wp-block-paragraph">Si el problema era la incompatibilidad con PHP 8.5, ahora Composer debería poder resolver las dependencias correctamente.</p>



<h2 class="wp-block-heading">Usar una versión de PHP solo para un proyecto</h2>



<p class="wp-block-paragraph">Una opción muy útil de Valet es aislar una versión de PHP por proyecto.</p>



<p class="wp-block-paragraph">Esto sirve cuando tenemos varios proyectos con diferentes necesidades. Por ejemplo:</p>



<ul class="wp-block-list">
<li>Un proyecto moderno usando PHP 8.4.</li>



<li>Un proyecto legacy usando PHP 8.2.</li>



<li>Un proyecto experimental usando PHP 8.5.</li>
</ul>



<p class="wp-block-paragraph">Para aislar un proyecto con PHP 8.4:</p>



<pre class="wp-block-code"><code>cd ~/Developments/LaravelProjects/foresta
valet isolate php@8.4</code></pre>



<p class="wp-block-paragraph">Para ver los sitios aislados:</p>



<pre class="wp-block-code"><code>valet isolated</code></pre>



<p class="wp-block-paragraph">Para quitar el aislamiento y volver a usar la versión global de Valet:</p>



<pre class="wp-block-code"><code>valet unisolate</code></pre>



<p class="wp-block-paragraph">Esta opción es muy práctica porque evita estar cambiando la versión global de PHP cada vez que trabajamos en proyectos distintos.</p>



<h2 class="wp-block-heading">Crear un archivo .valetrc</h2>



<p class="wp-block-paragraph">Otra alternativa es crear un archivo <code>.valetrc</code> dentro del proyecto indicando la versión de PHP que debe usar.</p>



<p class="wp-block-paragraph">Dentro del proyecto:</p>



<pre class="wp-block-code"><code>touch .valetrc</code></pre>



<p class="wp-block-paragraph">Y agregamos:</p>



<pre class="wp-block-code"><code>php=php@8.4</code></pre>



<p class="wp-block-paragraph">Después podemos ejecutar:</p>



<pre class="wp-block-code"><code>valet use</code></pre>



<p class="wp-block-paragraph">Valet leerá el archivo y aplicará la versión indicada.</p>



<h2 class="wp-block-heading">Comandos útiles de diagnóstico</h2>



<p class="wp-block-paragraph">Cuando Composer marque errores de compatibilidad, estos comandos ayudan bastante:</p>



<pre class="wp-block-code"><code>php -v</code></pre>



<pre class="wp-block-code"><code>valet which-php</code></pre>



<pre class="wp-block-code"><code>composer check-platform-reqs</code></pre>



<pre class="wp-block-code"><code>composer why-not maatwebsite/excel</code></pre>



<p class="wp-block-paragraph">También podemos revisar qué paquetes están bloqueando una instalación específica:</p>



<pre class="wp-block-code"><code>composer why-not phpoffice/phpspreadsheet</code></pre>



<h2 class="wp-block-heading">Conclusión</h2>



<p class="wp-block-paragraph">Laravel Valet facilita bastante el manejo de versiones de PHP en macOS. Cuando un paquete no es compatible con la versión actual, lo ideal no es forzar Composer, sino revisar los requisitos y utilizar una versión de PHP compatible.</p>



<p class="wp-block-paragraph"></p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.mariatech.com.mx/blog/laravel-php/como-instalar-y-usar-una-version-diferente-de-php-con-laravel-valet-en-mac/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
	</channel>
</rss>
