posts/q7T3UAw7iDQ75txHzdi4S1WwbWdtlBLBtrf31ojI.png

5 consejos avanzados de Laravel para elevar tus habilidades de desarrollo

Lleva tus proyectos Laravel al siguiente nivel con 5 prácticas avanzadas: optimización, arquitectura, automatización y más.

Basado en los temas del artículo “5 Advanced Laravel Tips to Elevate Your Development Skills” (scopes, DI, eventos, factories y scheduler), pero ampliado con ejemplos prácticos para Laravel 12 / PHP 8.3 y un enfoque de producción. Medium

1) Scopes de Eloquent de verdad útiles (y rápidos)

Los query scopes encapsulan lógica de filtrado para mantener tus controladores limpios y evitar repetir condiciones.

Ejemplo (local scopes con parámetros y búsqueda):

// app/Models/Post.php
namespace App\Models;

use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;

class Post extends Model
{
    // published_at no null y ya pasado
    public function scopePublished(Builder $q): Builder
    {
        return $q->whereNotNull('published_at')->where('published_at', '<=', now());
    }

    public function scopeFromAuthor(Builder $q, int|string $authorId): Builder
    {
        return $q->where('user_id', $authorId);
    }

    public function scopeSearch(Builder $q, ?string $term): Builder
    {
        return $q->when($term, fn ($q) =>
            $q->where(function ($q) use ($term) {
                $q->where('title', 'like', "%{$term}%")
                  ->orWhere('excerpt', 'like', "%{$term}%");
            })
        );
    }

    // ejemplo de scope con default param
    public function scopePopular(Builder $q, int $minLikes = 50): Builder
    {
        return $q->where('likes_count', '>=', $minLikes);
    }
}

Uso combinado y performante:

$posts = Post::query()
    ->with(['author:id,name'])          // evita N+1 y reduce columnas
    ->withCount('comments')             // metadatos baratos
    ->published()
    ->fromAuthor($request->user()->id)
    ->search($request->string('q'))
    ->popular(100)
    ->latest('published_at')
    ->paginate(20);

Tips rápidos

  • Prefiere

    withCount
    ,
    withExists
    y
    whereRelation
    para filtros por relación sin joins manuales.

  • Indiza las columnas de filtro (

    published_at
    ,
    user_id
    ,
    likes_count
    ) y las foreign keys más usadas.

  • Si un filtro aplica “siempre”, crea un Global Scope (pero documenta cómo deshabilitarlo con

    withoutGlobalScopes()
    ).

2) Inyección de dependencias y contenedor (código limpio y testeable)

Evita acoplar clases con implementaciones concretas. Programa contra interfaces y deja que el Service Container resuelva.

Contrato y dos implementaciones:

// app/Contracts/PaymentGateway.php
namespace App\Contracts;

interface PaymentGateway
{
    public function charge(int $cents, string $currency, array $meta = []): string; // returns transaction id
}
// app/Services/StripeGateway.php
namespace App\Services;

use App\Contracts\PaymentGateway;

class StripeGateway implements PaymentGateway
{
    public function __construct(private readonly string $key) {}

    public function charge(int $cents, string $currency, array $meta = []): string
    {
        // Llamada real a Stripe...
        return 'tx_'.bin2hex(random_bytes(8));
    }
}
// app/Services/FakeGateway.php (para tests / sandbox)
namespace App\Services;

use App\Contracts\PaymentGateway;

class FakeGateway implements PaymentGateway
{
    public function charge(int $cents, string $currency, array $meta = []): string
    {
        return 'fake_tx_'.($meta['order_id'] ?? '0');
    }
}

Enlaza en el contenedor (AppServiceProvider):

// app/Providers/AppServiceProvider.php
use App\Contracts\PaymentGateway;
use App\Services\StripeGateway;

public function register(): void
{
    $this->app->bind(PaymentGateway::class, function () {
        return new StripeGateway(
            key: config('services.stripe.secret') // lee de config/env
        );
    });

    // Ejemplo de binding contextual (admin usa otro gateway):
    // $this->app->when(AdminCheckoutController::class)
    //     ->needs(PaymentGateway::class)
    //     ->give(fn () => new AnotherGateway(...));
}

Consume por constructor (auto-wire):

// app/Http/Controllers/CheckoutController.php
use App\Contracts\PaymentGateway;

class CheckoutController
{
    public function __construct(private readonly PaymentGateway $gateway) {}

    public function __invoke(Request $request)
    {
        $tx = $this->gateway->charge(
            cents: (int)($request->integer('amount') * 100),
            currency: 'MXN',
            meta: ['order_id' => $request->string('order_id')]
        );

        return response()->json(['transaction' => $tx]);
    }
}

Testing sencillo reemplazando la implementación:

// tests/Feature/CheckoutTest.php
use App\Contracts\PaymentGateway;
use App\Services\FakeGateway;

test('cobra con gateway fake', function () {
    $this->app->bind(PaymentGateway::class, fn () => new FakeGateway());

    $res = $this->postJson('/checkout', ['amount' => 123, 'order_id' => 'A-1']);
    $res->assertOk()->assertJsonPath('transaction', 'fake_tx_A-1');
});

3) Eventos y listeners para modularizar (y escalar) tu app

Desacopla efectos colaterales con Event + Listener. Mantienes controladores mínimos y lógica extensible.

Definición del evento:

// app/Events/OrderPlaced.php
namespace App\Events;

use App\Models\Order;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;

class OrderPlaced
{
    use Dispatchable, SerializesModels;

    public function __construct(public Order $order) {}
}

Listeners queueables (seguro y rápido):

// app/Listeners/SendOrderConfirmation.php
namespace App\Listeners;

use App\Events\OrderPlaced;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use App\Mail\OrderConfirmation;
use Mail;

class SendOrderConfirmation implements ShouldQueue
{
    use InteractsWithQueue;

    public int $tries = 3;
    public int $backoff = 10; // segundos

    public function handle(OrderPlaced $event): void
    {
        Mail::to($event->order->customer_email)
            ->send(new OrderConfirmation($event->order));
    }
}

Registrar mapeos:

// app/Providers/EventServiceProvider.php
protected $listen = [
    \App\Events\OrderPlaced::class => [
        \App\Listeners\SendOrderConfirmation::class,
        \App\Listeners\RecalculateCustomerLifetimeValue::class,
        \App\Listeners\NotifyWarehouse::class,
    ],
];

Disparar evento en tu caso de uso:

Order::create($payload);
event(new OrderPlaced($order));

Tips

  • Marca listeners pesados como

    ShouldQueue
    .

  • Usa jobs dedicados dentro del listener si necesitas retry granular.

  • Agrega rate limiting por usuario si el evento se dispara desde inputs.

4) Testing rápido con Model Factories, estados y relaciones

Las factories modernas (clases) permiten componer escenarios complejos en una línea.

// database/factories/OrderFactory.php
namespace Database\Factories;

use App\Models\Order;
use Illuminate\Database\Eloquent\Factories\Factory;

class OrderFactory extends Factory
{
    protected $model = Order::class;

    public function definition(): array
    {
        return [
            'customer_email' => $this->faker->safeEmail(),
            'total_cents'    => $this->faker->numberBetween(1500, 25000),
            'status'         => 'pending',
        ];
    }

    public function paid(): self
    {
        return $this->state(fn () => ['status' => 'paid']);
    }

    public function withItems(int $count = 3): self
    {
        return $this->has(\App\Models\OrderItem::factory()->count($count), 'items');
    }
}

Test con Pest/PhpUnit:

use App\Models\Order;

uses(\Illuminate\Foundation\Testing\RefreshDatabase::class);

it('crea una orden pagada con 3 items', function () {
    $order = Order::factory()->paid()->withItems(3)->create();

    expect($order->status)->toBe('paid');
    expect($order->items)->toHaveCount(3);
});

Extras útiles

  • sequence()
    para variar valores por lote.

  • afterCreating()
    para enganchar lógica posterior (p.ej. recalcular totales).

  • Factories para pivots (

    ->hasAttached($products, ['qty' => 2])
    ).

5) Scheduler + comandos artesanales: automatiza la casa

Crea custom Artisan commands para mantenimiento y agenda tareas con el scheduler.

Comando para depurar órdenes antiguas:

// app/Console/Commands/PurgeOldOrders.php
namespace App\Console\Commands;

use App\Models\Order;
use Illuminate\Console\Command;

class PurgeOldOrders extends Command
{
    protected $signature = 'orders:purge {--days=30} {--dry}';
    protected $description = 'Elimina órdenes antiguas (soft-delete)';

    public function handle(): int
    {
        $days = (int) $this->option('days');
        $q = Order::where('status', 'cancelled')
                  ->where('updated_at', '<=', now()->subDays($days));

        $count = (clone $q)->count();

        if ($this->option('dry')) {
            $this->info("Se eliminarían {$count} órdenes.");
            return self::SUCCESS;
        }

        $deleted = $q->delete();
        $this->info("Eliminadas: {$deleted}");
        return self::SUCCESS;
    }
}

Agenda segura y sin solaparse:

// app/Console/Kernel.php
use Illuminate\Console\Scheduling\Schedule;

protected function schedule(Schedule $schedule): void
{
    $schedule->command('orders:purge --days=45')
        ->dailyAt('02:10')
        ->withoutOverlapping()      // evita ejecuciones concurrentes
        ->onOneServer()             // en clúster
        ->environments(['production'])
        ->sendOutputTo(storage_path('logs/schedule.log'))
        ->emailOutputOnFailure('ops@tuapp.com')
        // pings de salud (si usas un monitor)
        //->pingBefore('https://hc-ping/...')->pingAfter('https://hc-ping/...')
    ;
}

Ejecución en producción

  • Cron clásico:

    * * * * * php /path/artisan schedule:run >> /dev/null 2>&1

  • O proceso largo con

    php artisan schedule:work
    administrado por Supervisor/Systemd (latencia menor).

Bonus: checklist de producción (rápido)

  • Eager loading y

    select()
    reducido en listados grandes.

  • Cache para cálculos/consultas costosas (

    Cache::remember()
    + invalidación clara).

  • Métricas y trazas (Clockwork, Telescope/Sentry) para detectar cuellos de botella.

  • Validación estricta y DTOs/FormRequests para inputs críticos.

  • Rate limiting en endpoints sensibles.

Conclusión

Estos cinco frentes (scopes, DI, eventos, factories y scheduler) te dan código limpio, testeable y escalable. Empieza aplicando dos o tres hoy mismo (por ejemplo, extrae filtros repetidos a scopes y mueve efectos colaterales a listeners queueables) y notarás la diferencia en mantenibilidad y performance.

Comparte esta publicación

0 comentarios

Únete a la conversación y comparte tu experiencia.

Dejar un comentario

Comparte dudas, propuestas o mejoras para la comunidad.