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
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 givenLo 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
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
$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 mainNota 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
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 propertyEn PHP 8.2 llegaron las readonly classes, donde todas las propiedades son readonly por defecto:
<?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
$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; // ÉxitoError 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
// 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
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
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
// 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
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
// 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áticamente12. ¿Cuál es la diferencia entre `bind`, `singleton` y `instance` en el container?
<?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
// 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
// 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
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
// 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
// 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
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
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
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
// 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
// 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
// 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
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?
# 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áticoTruco: 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
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
// 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
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 trackingMiddleware
29. ¿Cómo creás un middleware y cuándo lo terminás antes?
<?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
// 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
// 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>
@endcan32. ¿Cómo implementás autorización basada en roles con Policies?
<?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
// 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
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
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
// 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á RefreshDatabase37. ¿Cómo testeas un artisan command?
<?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
// 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
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
// 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
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
// 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?
# 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-autoloader44. ¿Cómo funcionan los eventos y listeners en Laravel?
<?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
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
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
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
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
// 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
// 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:
- 1Que entiendas el "por qué" — no solo el código, sino cuándo usar cada herramienta
- 2Que hables de performance — N+1, índices, cache. Todo proyecto real tiene estas preguntas
- 3Que testees — preguntar "¿cómo testearías esto?" es casi seguro en cualquier entrevista técnica
- 4Que conozcas los trade-offs — no hay bala de plata; soft deletes sí/no, observer vs event, etc.
Errores comunes a evitar:
- Confundir
belongsToconhasMany(la foreign key vive en la tabla debelongsTo) - 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
// 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');
}
}