InterviewHack.ai
Start free
Blog/Preguntas de entrevista de PHP y Laravel — 50 con código y respuestas

Preguntas de entrevista de PHP y Laravel — 50 con código y respuestas

16 de septiembre de 2026

phplaravel

Las 50 preguntas de PHP y Laravel que más caen en entrevistas: OOP en PHP, traits, service container, Eloquent, migrations, queues, testing con Pest/PHPUnit. Con código real.

Preguntas de entrevista de PHP y Laravel — 50 con código y respuestas

Si estás preparando una entrevista para un rol de backend PHP o Laravel, esta guía te cubre. Acá reunimos las 50 preguntas que más aparecen, con respuestas detalladas y código real. Sin relleno: cada pregunta viene con lo que el entrevistador realmente quiere escuchar.


PHP Moderno

1. ¿Qué son los tipos estrictos en PHP y cómo los habilitás?

Con declare(strict_types=1) al inicio del archivo, PHP ya no hace conversiones automáticas de tipos. Si pasás una cadena donde se espera un int, obtenés un TypeError en vez de un comportamiento silencioso.

php
<?php
declare(strict_types=1);

function sumar(int $a, int $b): int {
    return $a + $b;
}

sumar(2, "3"); // TypeError: Argument #2 must be of type int, string given

Lo que busca el entrevistador: que entiendas la diferencia entre el modo coercitivo (default) y el modo estricto, y que sepas cuándo conviene usarlo (casi siempre en proyectos serios).


2. ¿Qué son los enums en PHP 8.1 y cuándo los usarías?

Los enums son tipos con un conjunto fijo de valores posibles. Pueden ser puros (sin valor) o backed (con un valor string o int asociado).

php
<?php

enum Estado: string {
    case Pendiente = 'pendiente';
    case Activo    = 'activo';
    case Cerrado   = 'cerrado';
}

// Uso en una clase
class Ticket {
    public function __construct(
        public readonly Estado $estado = Estado::Pendiente
    ) {}
}

$ticket = new Ticket();
echo $ticket->estado->value; // pendiente

// Comparación segura
if ($ticket->estado === Estado::Activo) {
    // ...
}

Los enums backed también pueden implementar interfaces y tener métodos, lo que los hace muy versátiles para representar estados de negocio.


3. ¿Qué son las Fibers en PHP 8.1?

Las Fibers son una forma de concurrencia cooperativa. Te permiten pausar y reanudar la ejecución de una función sin bloquear el proceso entero. Son la base para implementar corutinas en PHP.

php
<?php

$fiber = new Fiber(function(): void {
    $valor = Fiber::suspend('primer suspend');
    echo "Fiber recibió: {$valor}\n";
});

$resultado = $fiber->start();
echo "Main recibió: {$resultado}\n"; // Main recibió: primer suspend

$fiber->resume('hola desde main');
// Fiber recibió: hola desde main

Nota práctica: No vas a escribir Fibers a mano en proyectos normales. Las usan internamente frameworks como ReactPHP y Revolt. Pero entender el concepto muestra que sabés lo que pasa debajo del capó.


4. ¿Qué son las readonly properties y para qué sirven?

Las propiedades readonly solo pueden asignarse una vez, en la inicialización. Son ideales para value objects y DTOs.

php
<?php

class Usuario {
    public function __construct(
        public readonly int    $id,
        public readonly string $email,
    ) {}
}

$u = new Usuario(id: 1, email: 'juan@ejemplo.com');
$u->email = 'otro@ejemplo.com'; // Error: Cannot modify readonly property

En PHP 8.2 llegaron las readonly classes, donde todas las propiedades son readonly por defecto:

php
<?php

readonly class PedidoDTO {
    public function __construct(
        public int    $id,
        public string $producto,
        public float  $precio,
    ) {}
}

5. ¿Cómo funciona `match` y en qué se diferencia de `switch`?

match es una expresión (devuelve un valor), usa comparación estricta (===) y no tiene fall-through. Es más seguro y conciso que switch.

php
<?php

$codigo = 200;

$mensaje = match($codigo) {
    200, 201 => 'Éxito',
    400      => 'Petición inválida',
    404      => 'No encontrado',
    500      => 'Error del servidor',
    default  => 'Código desconocido',
};

echo $mensaje; // Éxito

Error común: usar switch con tipos mixtos y no darse cuenta de que hace comparación laxa. Con match eso no pasa.


6. ¿Qué es el operador nullsafe `?->` y cómo simplifica el código?

php
<?php

// Antes
$ciudad = null;
if ($usuario !== null && $usuario->perfil !== null) {
    $ciudad = $usuario->perfil->ciudad;
}

// Con nullsafe
$ciudad = $usuario?->perfil?->ciudad;

Si cualquier parte de la cadena es null, el resultado es null sin lanzar excepción. Funciona también con métodos: $usuario?->getPerfil()?->getCiudad().


Programación Orientada a Objetos

7. ¿Cuándo usás un trait en lugar de herencia?

Los traits resuelven el problema de la herencia múltiple: te permiten reutilizar métodos en clases sin relación jerárquica.

php
<?php

trait Timestampable {
    private \DateTimeImmutable $creadoEn;
    private \DateTimeImmutable $actualizadoEn;

    public function setCreadoEn(): void {
        $this->creadoEn = new \DateTimeImmutable();
    }

    public function getCreadoEn(): \DateTimeImmutable {
        return $this->creadoEn;
    }
}

class Producto {
    use Timestampable;
    // ...
}

class Entrada {
    use Timestampable;
    // ...
}

Regla práctica: si dos clases sin relación comparten comportamiento, usá un trait. Si comparten identidad ("es un tipo de"), usá herencia o interfaces.


8. ¿Cuál es la diferencia entre interfaz y clase abstracta?

| | Interfaz | Clase abstracta |

|---|---|---|

| Múltiple implementación | Sí | No |

| Estado (propiedades) | No | Sí |

| Métodos con implementación | Solo desde PHP 8 (default) | Sí |

| Constructor | No | Sí |

php
<?php

interface Exportable {
    public function exportar(): string;
}

abstract class DocumentoBase implements Exportable {
    abstract protected function contenido(): string;

    public function exportar(): string {
        return "<doc>" . $this->contenido() . "</doc>";
    }
}

class Factura extends DocumentoBase {
    protected function contenido(): string {
        return "Datos de factura";
    }
}

9. ¿Qué son los union types y los intersection types?

php
<?php

// Union type: acepta int O string
function procesar(int|string $valor): void {
    echo $valor;
}

// Intersection type (PHP 8.1): debe implementar AMBAS interfaces
function guardar(Exportable&Serializable $obj): void {
    // ...
}

Los intersection types son útiles cuando necesitás garantizar que un objeto cumple múltiples contratos.


10. ¿Cómo funciona la covarianza y contravarianza en PHP?

Son conceptos de tipado en herencia. La covarianza permite que un método override devuelva un tipo más específico. La contravarianza permite que acepte un tipo más amplio.

php
<?php

class Animal {}
class Perro extends Animal {}

class Repositorio {
    public function encontrar(int $id): Animal {
        return new Animal();
    }
}

class RepositorioPerros extends Repositorio {
    // Covariante: devuelve un tipo más específico (Perro extends Animal)
    public function encontrar(int $id): Perro {
        return new Perro();
    }
}

Laravel: Service Container y Facades

11. ¿Qué es el service container de Laravel y para qué sirve?

El service container (IoC container) es el núcleo de Laravel. Se encarga de resolver dependencias automáticamente. Cuando definís que una clase necesita una interfaz en su constructor, el container sabe qué clase concreta inyectar.

php
<?php

// Binding en un ServiceProvider
public function register(): void
{
    $this->app->bind(
        \App\Contracts\MailerInterface::class,
        \App\Services\SendgridMailer::class
    );
}

// En un controlador
class UserController extends Controller {
    public function __construct(
        private readonly MailerInterface $mailer
    ) {}
}
// Laravel inyecta SendgridMailer automáticamente

12. ¿Cuál es la diferencia entre `bind`, `singleton` y `instance` en el container?

php
<?php

// bind: nueva instancia cada vez que se resuelve
$this->app->bind(Logger::class, FileLogger::class);

// singleton: misma instancia en toda la request
$this->app->singleton(Cache::class, RedisCache::class);

// instance: registrás un objeto ya creado
$logger = new FileLogger('/var/log/app.log');
$this->app->instance(Logger::class, $logger);

Truco de entrevista: preguntarte cuándo usarías singleton vs bind. Respuesta: singleton para servicios costosos de instanciar (conexiones DB, clientes HTTP), bind cuando necesitás estado limpio por request.


13. ¿Qué es una Facade en Laravel y cómo funciona internamente?

Las Facades son proxies estáticos que internamente resuelven una clase del service container. Cache::get('key') no es realmente un método estático: getFacadeAccessor() devuelve la clave del container.

php
<?php

// Así está implementada la Facade de Cache
class Cache extends Facade {
    protected static function getFacadeAccessor(): string {
        return 'cache'; // clave en el container
    }
}

// Vos llamás:
Cache::get('usuarios');

// Laravel hace internamente:
app('cache')->get('usuarios');

14. ¿Cuándo preferís la inyección de dependencias por sobre las Facades?

En clases que van a testear con mocks. Las Facades tienen shouldReceive() pero la inyección de dependencias hace el código más explícito sobre sus dependencias.

php
<?php

// Con inyección: las dependencias son visibles en el constructor
class PedidoService {
    public function __construct(
        private readonly MailerInterface   $mailer,
        private readonly PedidoRepository $repo,
    ) {}
}

// En el test:
$mailer = Mockery::mock(MailerInterface::class);
$mailer->shouldReceive('enviar')->once();
$service = new PedidoService($mailer, $this->repo);

15. ¿Qué es un ServiceProvider y cuándo creás uno propio?

Los ServiceProviders bootstrapean componentes de la aplicación. Creás uno propio cuando tenés que registrar bindings, configurar paquetes externos o cargar configuración condicional.

php
<?php

class PagosServiceProvider extends ServiceProvider {
    public function register(): void {
        $this->app->singleton(GatewayInterface::class, function ($app) {
            return new MercadoPagoGateway(
                config('pagos.mercadopago_key'),
                config('pagos.mercadopago_secret'),
            );
        });
    }

    public function boot(): void {
        $this->publishes([
            __DIR__.'/../config/pagos.php' => config_path('pagos.php'),
        ]);
    }
}

Eloquent ORM

16. ¿Cómo funciona eager loading y cuándo es obligatorio usarlo?

Eager loading previene el problema N+1: en vez de hacer una query por cada relación en un loop, trae todo de una.

php
<?php

// MAL — problema N+1: 1 query para posts + N queries para autores
$posts = Post::all();
foreach ($posts as $post) {
    echo $post->autor->nombre; // query por cada post
}

// BIEN — eager loading: 2 queries en total
$posts = Post::with('autor')->get();
foreach ($posts as $post) {
    echo $post->autor->nombre; // sin queries adicionales
}

// Con múltiples relaciones
$posts = Post::with(['autor', 'etiquetas', 'comentarios.usuario'])->get();

17. ¿Cómo detectás el problema N+1 en producción?

php
<?php

// En AppServiceProvider::boot()
// Solo en desarrollo:
if (app()->isLocal()) {
    DB::listen(function ($query) {
        if ($query->time > 100) {
            logger()->warning('Query lenta', ['sql' => $query->sql]);
        }
    });
}

También podés usar el paquete barryvdh/laravel-debugbar en desarrollo o clockwork para ver las queries por request. En producción, Laravel Telescope es la herramienta nativa.


18. ¿Cuál es la diferencia entre `hasOne`, `hasMany`, `belongsTo` y `belongsToMany`?

php
<?php

class Usuario extends Model {
    // Un usuario tiene un perfil (1:1)
    public function perfil(): HasOne {
        return $this->hasOne(Perfil::class);
    }

    // Un usuario tiene muchos posts (1:N)
    public function posts(): HasMany {
        return $this->hasMany(Post::class);
    }

    // Un usuario tiene muchos roles (N:M)
    public function roles(): BelongsToMany {
        return $this->belongsToMany(Rol::class, 'usuario_rol');
    }
}

class Post extends Model {
    // Un post pertenece a un usuario
    public function usuario(): BelongsTo {
        return $this->belongsTo(Usuario::class);
    }
}

La regla de la foreign key: hasMany la tiene la otra tabla (posts.usuario_id). belongsTo la tenés en la propia tabla.


19. ¿Qué son los scopes locales y globales en Eloquent?

php
<?php

class Post extends Model {
    // Scope global: se aplica SIEMPRE a todas las queries del modelo
    protected static function booted(): void {
        static::addGlobalScope('publicado', function (Builder $builder) {
            $builder->where('estado', 'publicado');
        });
    }

    // Scope local: se aplica cuando lo pedís explícitamente
    public function scopeRecientes(Builder $query): void {
        $query->orderBy('created_at', 'desc')->limit(10);
    }

    public function scopePorAutor(Builder $query, int $autorId): void {
        $query->where('usuario_id', $autorId);
    }
}

// Uso de scopes locales
$posts = Post::recientes()->porAutor(5)->get();

// Ignorar global scope
$todos = Post::withoutGlobalScope('publicado')->get();

20. ¿Qué son los observers de Eloquent y cuándo los usás?

Los observers centralizan la lógica de eventos del modelo (created, updated, deleted, etc.) en una sola clase.

php
<?php

class PedidoObserver {
    public function created(Pedido $pedido): void {
        // Enviar email de confirmación
        Mail::to($pedido->usuario->email)
            ->queue(new PedidoConfirmado($pedido));
    }

    public function updated(Pedido $pedido): void {
        if ($pedido->wasChanged('estado')) {
            Cache::forget("pedido_{$pedido->id}");
        }
    }

    public function deleted(Pedido $pedido): void {
        $pedido->items()->delete();
    }
}

// En AppServiceProvider o en el Model con $dispatchesEvents
Pedido::observe(PedidoObserver::class);

Cuándo NO usarlos: cuando la lógica es compleja o tiene muchas dependencias. En esos casos preferí un event/listener explícito para mayor trazabilidad.


21. ¿Cómo funciona `withCount` y `loadAggregate`?

php
<?php

// withCount: trae el conteo de relaciones sin cargarlas
$categorias = Categoria::withCount('productos')->get();
echo $categorias[0]->productos_count; // 42

// withSum, withAvg, withMin, withMax
$usuarios = Usuario::withSum('pedidos', 'total')->get();
echo $usuarios[0]->pedidos_sum_total; // 15000.00

// loadAggregate en instancias ya cargadas
$categoria->loadCount('productos');

22. ¿Qué es `upsert` y cuándo lo usás sobre `updateOrCreate`?

php
<?php

// updateOrCreate: una query SELECT + una INSERT o UPDATE
// Útil para un registro a la vez
$producto = Producto::updateOrCreate(
    ['sku' => 'ABC-123'],
    ['nombre' => 'Auriculares', 'precio' => 9999]
);

// upsert: una sola query, ideal para bulk operations
Producto::upsert(
    [
        ['sku' => 'ABC-123', 'nombre' => 'Auriculares', 'precio' => 9999],
        ['sku' => 'DEF-456', 'nombre' => 'Teclado', 'precio' => 5499],
    ],
    uniqueBy: ['sku'],
    update: ['nombre', 'precio']
);

upsert es mucho más eficiente para importaciones masivas.


Migrations y Seeders

23. ¿Cómo diseñás una migración para agregar un índice a una tabla con datos en producción?

php
<?php

// Migración segura para tabla grande en producción
public function up(): void
{
    // Agregar la columna primero como nullable
    Schema::table('pedidos', function (Blueprint $table) {
        $table->string('referencia_externa')->nullable()->after('id');
    });

    // Poblar los datos en chunks para no lockear la tabla
    Pedido::whereNull('referencia_externa')
        ->chunkById(1000, function ($pedidos) {
            foreach ($pedidos as $pedido) {
                $pedido->update([
                    'referencia_externa' => 'REF-' . $pedido->id
                ]);
            }
        });

    // Agregar índice y constraint
    Schema::table('pedidos', function (Blueprint $table) {
        $table->string('referencia_externa')->nullable(false)->change();
        $table->unique('referencia_externa');
    });
}

24. ¿Cuál es la diferencia entre `truncate` y `delete` en seeders?

php
<?php

class ProductoSeeder extends Seeder {
    public function run(): void
    {
        // DELETE respeta foreign keys pero es más lento (logs de transacción)
        Producto::query()->delete();

        // TRUNCATE es más rápido pero falla si hay FK sin deshabilitar
        DB::statement('SET FOREIGN_KEY_CHECKS=0;');
        Producto::truncate();
        DB::statement('SET FOREIGN_KEY_CHECKS=1;');

        // Inserción masiva eficiente
        $productos = collect(range(1, 100))->map(fn($i) => [
            'nombre'     => "Producto {$i}",
            'precio'     => fake()->randomFloat(2, 100, 9999),
            'created_at' => now(),
            'updated_at' => now(),
        ])->all();

        Producto::insert($productos); // Una sola query
    }
}

25. ¿Cómo hacés rollback de una migration específica?

bash
# Rollback de la última batch
php artisan migrate:rollback

# Rollback de N pasos
php artisan migrate:rollback --step=3

# Ver el estado de migraciones
php artisan migrate:status

# En tests, usá RefreshDatabase que hace rollback automático

Truco: en producción, nunca hagas migrate:fresh. Usá migrate:rollback solo en desarrollo. Si necesitás "deshacer" algo en producción, creá una nueva migración.


Queues y Jobs

26. ¿Cómo implementás un Job en Laravel con retry y manejo de fallos?

php
<?php

class EnviarEmailBienvenida implements ShouldQueue {
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    public int $tries = 3;
    public int $backoff = 60; // segundos entre reintentos

    public function __construct(
        private readonly Usuario $usuario
    ) {}

    public function handle(MailerInterface $mailer): void
    {
        $mailer->enviar(
            new EmailBienvenida($this->usuario)
        );
    }

    public function failed(\Throwable $exception): void
    {
        // Se llama cuando se agotaron los reintentos
        logger()->error('Falló envío de email', [
            'usuario_id' => $this->usuario->id,
            'error'      => $exception->getMessage(),
        ]);

        // Podés notificar al equipo acá
    }

    public function retryUntil(): \DateTime
    {
        // Reintentar por no más de 24hs
        return now()->addHours(24);
    }
}

// Despachar
EnviarEmailBienvenida::dispatch($usuario);

// Con delay
EnviarEmailBienvenida::dispatch($usuario)->delay(now()->addMinutes(10));

// En una queue específica
EnviarEmailBienvenida::dispatch($usuario)->onQueue('emails');

27. ¿Cuál es la diferencia entre `ShouldQueue` y `ShouldQueueAfterCommit`?

php
<?php

// ShouldQueueAfterCommit: el job no se encola hasta que la transacción DB
// se confirme. Evita el race condition donde el job se ejecuta antes
// de que los datos estén en la base.

class NotificarNuevoPedido implements ShouldQueue, ShouldQueueAfterCommit {
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    public function __construct(private Pedido $pedido) {}

    public function handle(): void
    {
        // Garantizado: el pedido ya está commiteado en DB
        $this->pedido->refresh();
        // ...
    }
}

28. ¿Qué son los Job Batches?

Permiten agrupar múltiples jobs y ejecutar callbacks cuando todos terminen (o cuando alguno falle).

php
<?php

use Illuminate\Bus\Batch;
use Illuminate\Support\Facades\Bus;

$batch = Bus::batch([
    new ProcesarImagen($imagen1),
    new ProcesarImagen($imagen2),
    new ProcesarImagen($imagen3),
])->then(function (Batch $batch) {
    // Todos completaron OK
    logger('Procesamiento completo');
})->catch(function (Batch $batch, \Throwable $e) {
    // Al menos uno falló
    logger()->error('Error en batch', ['error' => $e->getMessage()]);
})->finally(function (Batch $batch) {
    // Se ejecuta siempre, independientemente del resultado
})->dispatch();

echo $batch->id; // UUID del batch, podés usarlo para tracking

Middleware

29. ¿Cómo creás un middleware y cuándo lo terminás antes?

php
<?php

class VerificarSuscripcion {
    public function handle(Request $request, Closure $next): Response
    {
        $usuario = $request->user();

        if (!$usuario || !$usuario->suscripcion?->activa()) {
            // Terminar la request antes de llegar al controlador
            return response()->json([
                'mensaje' => 'Suscripción requerida',
                'codigo'  => 'SUBSCRIPTION_REQUIRED',
            ], 402);
        }

        return $next($request);
    }
}

// Registrar en bootstrap/app.php (Laravel 11+)
->withMiddleware(function (Middleware $middleware) {
    $middleware->alias([
        'suscripcion' => VerificarSuscripcion::class,
    ]);
})

// Uso en rutas
Route::get('/premium', PremiumController::class)
    ->middleware('suscripcion');

30. ¿Qué son los middleware groups y cómo los usás?

php
<?php

// Agrupar middleware para APIs
->withMiddleware(function (Middleware $middleware) {
    $middleware->group('api-privada', [
        'auth:sanctum',
        'suscripcion',
        \Illuminate\Routing\Middleware\ThrottleRequests::class . ':60,1',
    ]);
})

// En rutas
Route::middleware('api-privada')->group(function () {
    Route::get('/reportes', ReportesController::class);
    Route::get('/exportar', ExportarController::class);
});

Gates y Policies

31. ¿Cuál es la diferencia entre Gates y Policies?

Gates: autorizaciones simples, no ligadas a un modelo específico. Se definen como closures.

Policies: autorizaciones para un modelo específico, organizadas en una clase.

php
<?php

// Gate: para acciones de aplicación (no CRUD de un modelo)
Gate::define('acceder-panel-admin', function (Usuario $usuario) {
    return $usuario->rol === 'admin';
});

// Uso
if (Gate::allows('acceder-panel-admin')) {
    // ...
}

// Policy: para CRUD de un modelo
class PostPolicy {
    public function update(Usuario $usuario, Post $post): bool
    {
        return $usuario->id === $post->usuario_id;
    }

    public function delete(Usuario $usuario, Post $post): bool
    {
        return $usuario->id === $post->usuario_id
            || $usuario->rol === 'admin';
    }
}

// Registrar (Laravel auto-descubre si seguís la convención)
Gate::policy(Post::class, PostPolicy::class);

// Uso en controlador
$this->authorize('update', $post);

// Uso en Blade
@can('update', $post)
    <button>Editar</button>
@endcan

32. ¿Cómo implementás autorización basada en roles con Policies?

php
<?php

class PostPolicy {
    // Antes de cualquier método de la policy, el "before" tiene prioridad
    public function before(Usuario $usuario, string $ability): bool|null
    {
        if ($usuario->esAdmin()) {
            return true; // Admin puede todo
        }

        return null; // null significa "seguir evaluando la policy"
    }

    public function viewAny(Usuario $usuario): bool
    {
        return true; // Cualquier usuario puede listar
    }

    public function create(Usuario $usuario): bool
    {
        return $usuario->suscripcion?->activa() ?? false;
    }

    public function update(Usuario $usuario, Post $post): bool
    {
        return $usuario->id === $post->usuario_id;
    }
}

Testing con PHPUnit y Pest

33. ¿Cuál es la diferencia entre Pest y PHPUnit en Laravel?

Pest es una capa encima de PHPUnit con una API más expresiva. Los tests de Pest son más legibles pero al final corren con PHPUnit.

php
<?php
// PHPUnit style
class PostTest extends TestCase {
    public function test_un_usuario_puede_crear_un_post(): void
    {
        $usuario = Usuario::factory()->create();
        $response = $this->actingAs($usuario)
            ->postJson('/api/posts', ['titulo' => 'Mi post', 'contenido' => '...']);
        $response->assertStatus(201);
        $this->assertDatabaseHas('posts', ['titulo' => 'Mi post']);
    }
}

// Pest style — mismo test, más legible
it('permite a un usuario crear un post', function () {
    $usuario = Usuario::factory()->create();

    actingAs($usuario)
        ->postJson('/api/posts', ['titulo' => 'Mi post', 'contenido' => '...'])
        ->assertStatus(201);

    assertDatabaseHas('posts', ['titulo' => 'Mi post']);
});

34. ¿Cómo mockeás servicios externos en tests de Laravel?

php
<?php

it('envía email al crear un pedido', function () {
    // Fake del mailer: intercepta todos los emails sin enviarlos
    Mail::fake();

    $usuario = Usuario::factory()->create();
    $response = actingAs($usuario)
        ->postJson('/api/pedidos', ['producto_id' => 1]);

    $response->assertStatus(201);

    // Verificar que se envió el email correcto
    Mail::assertSent(PedidoConfirmado::class, function ($mail) use ($usuario) {
        return $mail->hasTo($usuario->email);
    });
});

it('encola el job de procesamiento', function () {
    Queue::fake();

    actingAs(Usuario::factory()->create())
        ->postJson('/api/importar', ['archivo' => 'datos.csv'])
        ->assertAccepted();

    Queue::assertPushed(ProcesarImportacion::class);
});

35. ¿Cómo usás factories con estados en tests?

php
<?php

class UsuarioFactory extends Factory {
    public function definition(): array {
        return [
            'nombre' => fake()->name(),
            'email'  => fake()->unique()->safeEmail(),
            'estado' => 'activo',
        ];
    }

    public function inactivo(): static {
        return $this->state(['estado' => 'inactivo']);
    }

    public function admin(): static {
        return $this->state(['rol' => 'admin']);
    }

    public function conSuscripcion(): static {
        return $this->has(
            Suscripcion::factory()->activa(),
            'suscripcion'
        );
    }
}

// En tests
$admin = Usuario::factory()->admin()->conSuscripcion()->create();
$inactivos = Usuario::factory()->inactivo()->count(5)->create();

36. ¿Qué es `RefreshDatabase` vs `DatabaseTransactions` en tests?

php
<?php

// RefreshDatabase: ejecuta las migrations en cada test suite
// Más lento pero garantiza un estado limpio
uses(RefreshDatabase::class);

// DatabaseTransactions: envuelve cada test en una transacción
// y hace rollback al terminar. Más rápido pero no funciona
// con jobs o código que corre fuera de la transacción
uses(DatabaseTransactions::class);

// Para tests de API rápidos, preferí DatabaseTransactions
// Para tests con queues o eventos, usá RefreshDatabase

37. ¿Cómo testeas un artisan command?

php
<?php

it('el comando de limpieza elimina registros viejos', function () {
    // Crear datos de prueba
    $viejos = Pedido::factory()
        ->count(5)
        ->create(['created_at' => now()->subDays(90)]);

    $recientes = Pedido::factory()
        ->count(3)
        ->create(['created_at' => now()]);

    // Correr el comando
    $this->artisan('pedidos:limpiar --dias=60')
         ->assertExitCode(0)
         ->expectsOutput('5 pedidos eliminados.');

    // Verificar resultado
    expect(Pedido::count())->toBe(3);
});

Optimización de Performance

38. ¿Cómo optimizás una query Eloquent compleja?

php
<?php

// MAL: carga objetos completos cuando solo necesitás ciertos campos
$usuarios = Usuario::all();

// BIEN: solo los campos necesarios
$usuarios = Usuario::select(['id', 'nombre', 'email'])->get();

// Para queries de reporte, usá Query Builder directo (sin hidratación)
$reporte = DB::table('pedidos')
    ->selectRaw('DATE(created_at) as fecha, COUNT(*) as total, SUM(monto) as facturacion')
    ->where('estado', 'completado')
    ->whereBetween('created_at', [$inicio, $fin])
    ->groupBy('fecha')
    ->orderBy('fecha')
    ->get();

// Para grandes volúmenes, usá cursor() o chunk()
Pedido::where('estado', 'pendiente')
    ->cursor() // Usa generators, muy eficiente en memoria
    ->each(function (Pedido $pedido) {
        // Procesar uno a la vez
    });

39. ¿Cómo implementás cache en Laravel para evitar queries pesadas?

php
<?php

class ReporteService {
    public function getTopProductos(): Collection
    {
        return Cache::remember('top-productos', ttl: 3600, function () {
            return Producto::withCount('ventas')
                ->orderByDesc('ventas_count')
                ->limit(10)
                ->get();
        });
    }

    // Cache por usuario (cache tagging)
    public function getDashboard(int $usuarioId): array
    {
        return Cache::tags(["usuario:{$usuarioId}", 'dashboard'])
            ->remember("dashboard:{$usuarioId}", 300, function () use ($usuarioId) {
                return [
                    'pedidos'   => $this->contarPedidos($usuarioId),
                    'facturas'  => $this->sumarFacturas($usuarioId),
                ];
            });
    }

    // Invalidar cache cuando cambian los datos
    public function actualizarPedido(Pedido $pedido): void
    {
        $pedido->save();
        Cache::tags(["usuario:{$pedido->usuario_id}", 'dashboard'])->flush();
    }
}

40. ¿Qué son los query scopes con índices y cómo los diseñás?

php
<?php

// Asegurate de que los campos en WHERE tengan índices en la migración
Schema::create('pedidos', function (Blueprint $table) {
    $table->id();
    $table->foreignId('usuario_id')->constrained()->index();
    $table->string('estado', 20)->index();
    $table->timestamp('created_at')->index();

    // Índice compuesto para queries frecuentes
    $table->index(['estado', 'created_at']);
    $table->index(['usuario_id', 'estado']);
});

// Scope que aprovecha el índice compuesto
public function scopePendientesRecientes(Builder $query): void
{
    $query->where('estado', 'pendiente')
          ->where('created_at', '>=', now()->subDays(7))
          ->orderBy('created_at', 'desc');
}

41. ¿Cómo usás `lazy loading` de módulos vs `eager loading` en la app?

En el contexto de la aplicación Laravel (no Eloquent), podés diferir la carga de configuración y bindings pesados para mejorar el tiempo de boot.

php
<?php

class ReportingServiceProvider extends ServiceProvider {
    // Solo registrar cuando realmente se necesite
    public function register(): void
    {
        $this->app->bind(ReportGenerator::class, function ($app) {
            // Esta closure solo se ejecuta cuando alguien resuelve ReportGenerator
            // No cuando la app hace bootstrap
            return new ReportGenerator(
                config('reporting.driver'),
                new ReportCache($app->make(Cache::class))
            );
        });
    }
}

42. ¿Qué es `dd`, `dump`, `ray` y cuándo usás cada uno?

php
<?php

// dd(): dump and die — para debug rápido, bloquea la ejecución
dd($variable, $otraVariable);

// dump(): muestra sin cortar la ejecución
dump($variable);
$resultado = hacerAlgo(); // esto sí se ejecuta

// ray() (paquete spatie/ray): envía a la app Ray, no interfiere con la respuesta
// Ideal para debugging en APIs donde dd() rompe el JSON
ray($variable)->label('mi variable');
ray()->measure(); // timing

// logger() para logging permanente
logger()->debug('Debug info', ['data' => $variable]);

43. ¿Cómo optimizás el autoloader de Composer en producción?

bash
# Generar classmap optimizado (1 archivo, sin filesystem lookups)
composer install --optimize-autoloader --no-dev

# Equivalente manual
composer dump-autoload --optimize --no-dev

# En el pipeline de CI/CD, siempre usar:
composer install --no-interaction --no-dev --prefer-dist --optimize-autoloader

44. ¿Cómo funcionan los eventos y listeners en Laravel?

php
<?php

// Definir el evento
class PedidoCreado {
    public function __construct(
        public readonly Pedido $pedido
    ) {}
}

// Listener
class EnviarEmailConfirmacion {
    public function handle(PedidoCreado $evento): void
    {
        Mail::to($evento->pedido->usuario->email)
            ->queue(new PedidoConfirmado($evento->pedido));
    }
}

// Registrar en EventServiceProvider (o con atributos PHP 8)
protected $listen = [
    PedidoCreado::class => [
        EnviarEmailConfirmacion::class,
        ActualizarInventario::class,
        NotificarVendedor::class,
    ],
];

// Disparar el evento
event(new PedidoCreado($pedido));
// o desde el modelo
PedidoCreado::dispatch($pedido);

45. ¿Qué es un Form Request y por qué es mejor que validar en el controlador?

php
<?php

class CrearPedidoRequest extends FormRequest {
    public function authorize(): bool
    {
        // Lógica de autorización aquí (no en el controlador)
        return $this->user()->puedeCrearPedidos();
    }

    public function rules(): array
    {
        return [
            'producto_id'    => ['required', 'integer', 'exists:productos,id'],
            'cantidad'       => ['required', 'integer', 'min:1', 'max:100'],
            'direccion_id'   => ['required', 'integer', 'exists:direcciones,id'],
        ];
    }

    public function messages(): array
    {
        return [
            'producto_id.exists' => 'El producto seleccionado no existe.',
            'cantidad.max'       => 'No podés pedir más de :max unidades.',
        ];
    }
}

// En el controlador: solo lógica de negocio
class PedidoController extends Controller {
    public function store(CrearPedidoRequest $request): JsonResponse
    {
        // Si llegamos acá, la validación y autorización ya pasaron
        $pedido = $this->pedidoService->crear($request->validated());
        return response()->json($pedido, 201);
    }
}

46. ¿Cómo implementás API Resources para formatear respuestas?

php
<?php

class PedidoResource extends JsonResource {
    public function toArray(Request $request): array
    {
        return [
            'id'        => $this->id,
            'estado'    => $this->estado,
            'total'     => number_format($this->total / 100, 2),
            'creado_en' => $this->created_at->toIso8601String(),
            'usuario'   => new UsuarioResource($this->whenLoaded('usuario')),
            'items'     => PedidoItemResource::collection(
                $this->whenLoaded('items')
            ),
            // Solo incluir si el usuario es admin
            'notas_internas' => $this->when(
                $request->user()?->esAdmin(),
                $this->notas_internas
            ),
        ];
    }
}

// Colección con metadata de paginación
class PedidoCollection extends ResourceCollection {
    public function toArray(Request $request): array
    {
        return [
            'data' => $this->collection,
            'meta' => [
                'total_facturado' => $this->collection->sum('total'),
            ],
        ];
    }
}

47. ¿Qué son los Accessors y Mutators en Eloquent?

php
<?php

use Illuminate\Database\Eloquent\Casts\Attribute;

class Usuario extends Model {
    // Accessor: transforma el valor al leerlo
    protected function nombreCompleto(): Attribute
    {
        return Attribute::make(
            get: fn () => "{$this->nombre} {$this->apellido}",
        );
    }

    // Mutator: transforma el valor al guardarlo
    protected function email(): Attribute
    {
        return Attribute::make(
            get: fn (string $value) => strtolower($value),
            set: fn (string $value) => strtolower(trim($value)),
        );
    }
}

// Uso
$usuario->nombre_completo; // "Juan García" — campo virtual
$usuario->email = '  JUAN@EJEMPLO.COM  '; // se guarda "juan@ejemplo.com"

48. ¿Cómo implementás soft deletes y cuándo los evitás?

php
<?php

class Producto extends Model {
    use SoftDeletes; // Agrega campo deleted_at

    // Los productos eliminados no aparecen en queries normales
}

// Queries con soft deletes
$activos   = Producto::all(); // Solo donde deleted_at IS NULL
$con_todos = Producto::withTrashed()->get();
$solo_elim = Producto::onlyTrashed()->get();

// Restaurar
Producto::withTrashed()->find(1)->restore();

// Eliminar permanentemente
Producto::withTrashed()->find(1)->forceDelete();

Cuándo NO usar soft deletes: tablas de auditoría (ahí querés eliminación real + log), tablas con muchísimas filas donde el índice se complica, o cuando la GDPR exige borrado real de datos personales.


49. ¿Qué son los Casts en Eloquent y cómo los usás para value objects?

php
<?php

// Cast nativo
class Pedido extends Model {
    protected $casts = [
        'metadata'   => 'array',
        'total'      => 'integer',
        'enviado_en' => 'immutable_datetime',
        'activo'     => 'boolean',
        'estado'     => Estado::class, // enum cast
    ];
}

// Cast personalizado para un value object
class DireccionCast implements CastsAttributes {
    public function get(Model $model, string $key, mixed $value, array $attributes): Direccion
    {
        $data = json_decode($value, true);
        return new Direccion($data['calle'], $data['ciudad'], $data['cp']);
    }

    public function set(Model $model, string $key, mixed $value, array $attributes): string
    {
        return json_encode([
            'calle'  => $value->calle,
            'ciudad' => $value->ciudad,
            'cp'     => $value->cp,
        ]);
    }
}

// En el modelo
protected $casts = [
    'direccion_envio' => DireccionCast::class,
];

50. ¿Cómo estructurás un proyecto Laravel grande para que sea mantenible?

La estructura por defecto de Laravel funciona hasta cierto tamaño. Para proyectos grandes conviene:

app/
├── Actions/          # Una clase por acción de negocio (CQRS-lite)
│   ├── Pedidos/
│   │   ├── CrearPedido.php
│   │   └── CancelarPedido.php
├── Contracts/        # Interfaces
├── Data/             # DTOs (spatie/laravel-data)
│   └── PedidoData.php
├── Http/
│   ├── Controllers/  # Solo glue: validar, llamar Action, devolver Resource
│   ├── Requests/     # Form Requests
│   └── Resources/    # API Resources
├── Models/           # Solo propiedades, relaciones, casts, scopes
├── Repositories/     # Abstracción de queries complejas
└── Services/         # Orquestación entre Actions
php
<?php

// Action: una responsabilidad, inyectable, testeable
class CrearPedido {
    public function __construct(
        private readonly PedidoRepository $repo,
        private readonly EventDispatcher  $events,
    ) {}

    public function ejecutar(PedidoData $datos): Pedido
    {
        $pedido = $this->repo->crear($datos);
        $this->events->dispatch(new PedidoCreado($pedido));
        return $pedido;
    }
}

// Controlador liviano
class PedidoController extends Controller {
    public function store(CrearPedidoRequest $request, CrearPedido $action): JsonResponse
    {
        $pedido = $action->ejecutar(PedidoData::fromRequest($request));
        return new PedidoResource($pedido);
    }
}

Tips finales para la entrevista

Lo que más valoran los entrevistadores:

  1. 1Que entiendas el "por qué" — no solo el código, sino cuándo usar cada herramienta
  2. 2Que hables de performance — N+1, índices, cache. Todo proyecto real tiene estas preguntas
  3. 3Que testees — preguntar "¿cómo testearías esto?" es casi seguro en cualquier entrevista técnica
  4. 4Que conozcas los trade-offs — no hay bala de plata; soft deletes sí/no, observer vs event, etc.

Errores comunes a evitar:

  • Confundir belongsTo con hasMany (la foreign key vive en la tabla de belongsTo)
  • Olvidar el eager loading en loops
  • Usar dd() en código de producción (sí pasa)
  • No validar los datos en Form Requests y hacerlo en el controlador
  • Usar env() fuera de archivos de configuración (los valores no se cachean correctamente)
php
<?php

// MAL: env() en un Service directamente
class PagoService {
    private string $key = env('STRIPE_KEY'); // No se cachea con config:cache
}

// BIEN: siempre pasar por config()
class PagoService {
    private string $key;

    public function __construct() {
        $this->key = config('services.stripe.key');
    }
}

FAQ

¿Qué preguntas de PHP caen más en entrevistas técnicas?+

Las preguntas más frecuentes son sobre tipos estrictos y tipos de retorno, diferencias entre interfaces y clases abstractas, traits, el operador nullsafe, enums (desde PHP 8.1), y el uso de match vs switch. También preguntan mucho sobre manejo de excepciones, corutinas y Fibers para roles más senior.

¿Qué es el problema N+1 en Laravel y cómo se resuelve?+

El problema N+1 ocurre cuando hacés 1 query para obtener una lista y luego N queries adicionales para cargar una relación de cada elemento. Se resuelve con eager loading usando el método with(). Por ejemplo: Post::with('autor')->get() en lugar de Post::all() seguido de $post->autor en un loop.

¿Cómo empezar a usar Pest en un proyecto Laravel existente?+

Instalás Pest con composer require pestphp/pest pestphp/pest-plugin-laravel --dev, luego corrés ./vendor/bin/pest --init. Podés migrar los tests gradualmente: Pest corre tests PHPUnit existentes sin cambios, y vas reescribiendo los nuevos en la sintaxis de Pest. No necesitás migrar todo de una.

¿Cuál es la diferencia entre eventos y observers en Laravel?+

Los observers son específicos de un modelo Eloquent y reaccionan a eventos del ciclo de vida del modelo (created, updated, deleted). Los eventos/listeners son más genéricos: vos disparás un evento desde cualquier parte del código y podés tener múltiples listeners. Para lógica ligada a un modelo específico, los observers son más simples; para lógica de negocio compleja con múltiples efectos secundarios, los eventos/listeners dan más flexibilidad y trazabilidad.

¿Qué es el service container de Laravel y por qué importa en entrevistas?+

El service container es el sistema de inyección de dependencias de Laravel. Maneja la resolución automática de dependencias en constructores, permite hacer binding de interfaces a implementaciones concretas, y es la base de las Facades. En entrevistas es un tema central porque demuestra que entendés la arquitectura del framework, no solo cómo usar sus helpers.

¿Cómo testeo código que usa queues o emails en Laravel sin enviarlos realmente?+

Laravel provee fakes para los componentes principales: Mail::fake(), Queue::fake(), Event::fake(), Storage::fake(), Http::fake(). Después de llamar el fake, ejecutás el código y usás los métodos assertSent(), assertPushed(), assertDispatched() para verificar que se llamó lo correcto. Esto hace los tests deterministas y rápidos sin necesitar servicios externos.

Related articles

Preguntas de entrevista de GraphQL — 35 con código y respuestas

35 preguntas de GraphQL para entrevistas backend y full-stack: queries, mutations, subscriptions, resolvers, DataLoader, N+1 problem, schema design. Con código.

Preguntas de entrevista de Redis — 35 con código y respuestas reales

35 preguntas de Redis para entrevistas backend y system design: estructuras de datos, pub/sub, persistencia, clustering, patrones de caché. Con ejemplos de código.

Preguntas de entrevista de Microservicios — 35 respuestas de arquitecto

35 preguntas de microservicios para roles backend y arquitecto: descomposición de servicios, saga pattern, event sourcing, circuit breaker, observabilidad. Respuestas de nivel senior.

System Design: cómo diseñar un sistema de pagos en una entrevista

Guía paso a paso para diseñar un sistema de pagos en una entrevista de system design: idempotencia, consistencia, colas de transacciones, reconciliación, seguridad PCI DSS.

Prepare for your real interview

Paste your job link: we research who's interviewing you and rehearse you live.

Start free →

Have an interview coming up? Install the live copilot →

InterviewHack.ai

Prepare for the exact interview: who's interviewing you, a tailored CV, and a real coach.

Product

JobsFree ATS checkerInterview-English checkSalary checkLATAM salary reportFree coursesBlogTailored CVSpoken practiceIt's free

Remote jobs

ReactPythonFull-StackLATAMArgentinaMexicoSee all →

Prepare

Spoken practiceFrontendBackendAI EngineerBy companySell with your CV

Company

For employersAboutContactPrivacyTerms

© 2026 InterviewHack.ai · Your CV is yours. Never used to train anything. · A product of IA-PTY