คำถามสัมภาษณ์ PHP Laravel Framework 2026: Eloquent, Queues และ Architecture Patterns

คำถามสัมภาษณ์ PHP Laravel framework สำหรับปี 2026 เชี่ยวชาญ Eloquent ORM, สถาปัตยกรรม queue, รูปแบบ service container และความท้าทายด้านการเขียนโค้ดสำหรับตำแหน่ง senior

คำถามสัมภาษณ์ PHP Laravel Framework 2026: Eloquent, Queues และ Architecture Patterns

คำถามสัมภาษณ์ PHP Laravel framework ทดสอบมากกว่าความรู้ด้าน syntax บริษัทที่รับสมัครงานในปี 2026 คาดหวังให้ผู้สมัครอธิบายการ optimize relationship ของ Eloquent, การจัดการ queue failure และ service container bindings ในระดับ production Laravel 13 นำเสนอ AI tooling, JSON:API resources และความสามารถ vector search ที่เพิ่มมิติใหม่ให้กับการสัมภาษณ์เชิงเทคนิค

สิ่งที่ผู้สัมภาษณ์ประเมิน

ตำแหน่ง senior Laravel มุ่งเน้นสามด้าน: ประสิทธิภาพ Eloquent (การป้องกัน N+1, chunking, cursor pagination), ความน่าเชื่อถือของ queue (retries, dead letter handling, job batching) และการตัดสินใจด้านสถาปัตยกรรม (service providers, facades vs injection, repository patterns)

Eloquent ORM: Relationship Loading และ Query Optimization

คำถามสัมภาษณ์ Eloquent มักมุ่งเป้าไปที่ปัญหา N+1 ผู้สมัครต้องแสดงให้เห็นว่าเมื่อใดควรใช้ with(), load() และ loadMissing() พร้อมอธิบาย tradeoffs ด้านหน่วยความจำของการ eager loading dataset ขนาดใหญ่

UserController.phpphp
// Eager load พร้อม constraints เพื่อหลีกเลี่ยงการโหลดข้อมูลที่ไม่จำเป็น
$users = User::query()
    ->with(['posts' => function (Builder $query) {
        $query->where('published', true)
              ->select('id', 'user_id', 'title', 'published_at');
    }])
    ->withCount('posts')
    ->paginate(25);

// ไม่ดี: ทำให้เกิด N+1 เมื่อเข้าถึง posts ใน loop
foreach ($users as $user) {
    $user->posts; // แต่ละ iteration ทำ query ไปยัง database
}

เมธอด withCount() เพิ่ม subquery ที่นับ related records โดยไม่โหลดข้อมูล ซึ่งหลีกเลี่ยง memory overhead เมื่อแสดง counts ในหน้า listing สำหรับ relationships ที่อาจไม่ถูกเข้าถึง loadMissing() ป้องกัน query ซ้ำซ้อนเมื่อ relationship ถูก eager load ไปแล้วก่อนหน้า

คำถาม follow-up ที่พบบ่อยถามเกี่ยวกับ cursor pagination vs offset pagination Cursor pagination ใช้ cursorPaginate() และหลีกเลี่ยง performance cliff ที่ offset pagination ประสบกับตารางขนาดใหญ่ Tradeoff คือ: cursor pagination ไม่รองรับการนำทางโดยตรงไปยังหน้าที่ต้องการ

php
// Cursor pagination สำหรับ dataset ขนาดใหญ่
$posts = Post::query()
    ->where('status', 'published')
    ->orderBy('published_at', 'desc')
    ->cursorPaginate(50);

// Chunk processing สำหรับ batch operations
User::query()
    ->where('last_login_at', '<', now()->subYear())
    ->chunkById(1000, function (Collection $users) {
        foreach ($users as $user) {
            $user->markAsInactive();
        }
    });

เมธอด chunkById() ประมวลผล records เป็น batch โดยไม่ทำให้หน่วยความจำหมด ต่างจาก chunk() เมธอดนี้รักษาลำดับที่สอดคล้องกันเมื่อ records ถูกแก้ไขระหว่างการ iterate

Query Scopes และ Reusable Query Logic

ผู้สัมภาษณ์คาดหวังให้ผู้สมัครจัดระเบียบ query logic โดยใช้ local และ global scopes Local scopes ห่อหุ้มเงื่อนไข filter ที่ใช้บ่อย ในขณะที่ global scopes ใช้ constraints โดยอัตโนมัติกับทุก queries

app/Models/Post.phpphp
class Post extends Model
{
    // Local scope สำหรับเนื้อหาที่เผยแพร่แล้ว
    public function scopePublished(Builder $query): Builder
    {
        return $query->where('status', 'published')
                     ->whereNotNull('published_at')
                     ->where('published_at', '<=', now());
    }

    // Local scope พร้อม parameter
    public function scopeByAuthor(Builder $query, int $authorId): Builder
    {
        return $query->where('author_id', $authorId);
    }

    // Scope ที่สามารถ chain ได้
    public function scopeRecent(Builder $query, int $days = 7): Builder
    {
        return $query->where('published_at', '>=', now()->subDays($days));
    }
}

// การใช้งาน: chaining multiple scopes
$recentPosts = Post::published()
    ->byAuthor($userId)
    ->recent(14)
    ->get();

Global scopes ใช้กับทุก queries บน model กรณีการใช้งานทั่วไปรวมถึง soft deletes, tenant isolation ในแอปพลิเคชัน multi-tenant และ status filtering

app/Models/Scopes/ActiveScope.phpphp
class ActiveScope implements Scope
{
    public function apply(Builder $builder, Model $model): void
    {
        $builder->where('is_active', true);
    }
}

// app/Models/Subscription.php
class Subscription extends Model
{
    protected static function booted(): void
    {
        static::addGlobalScope(new ActiveScope());
    }
}

// Bypass global scope เมื่อจำเป็น
$allSubscriptions = Subscription::withoutGlobalScope(ActiveScope::class)->get();

Queue System: Job Handling และ Failure Recovery

สถาปัตยกรรม queue ของ Laravel ให้ความน่าเชื่อถือผ่าน retries, timeouts และ dead letter queues คำถามสัมภาษณ์มุ่งเน้นการจัดการ failure และการรับประกัน job idempotency

app/Jobs/ProcessPayment.phpphp
class ProcessPayment implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    public int $tries = 3;
    public int $maxExceptions = 2;
    public int $timeout = 120;
    public int $backoff = 60;

    public function __construct(
        public readonly Order $order,
        public readonly string $idempotencyKey
    ) {}

    public function handle(PaymentGateway $gateway): void
    {
        // ตรวจสอบ idempotency เพื่อป้องกันการประมวลผลซ้ำ
        if (Cache::has("payment_processed:{$this->idempotencyKey}")) {
            return;
        }

        $result = $gateway->charge(
            $this->order->total,
            $this->order->payment_method_id
        );

        $this->order->update([
            'payment_status' => $result->status,
            'payment_id' => $result->id,
        ]);

        Cache::put(
            "payment_processed:{$this->idempotencyKey}",
            true,
            now()->addDays(7)
        );
    }

    public function failed(Throwable $exception): void
    {
        Log::error('Payment processing failed', [
            'order_id' => $this->order->id,
            'error' => $exception->getMessage(),
        ]);

        $this->order->update(['payment_status' => 'failed']);
        
        Notification::route('slack', config('services.slack.payments_channel'))
            ->notify(new PaymentFailedNotification($this->order, $exception));
    }

    public function retryUntil(): DateTime
    {
        return now()->addHours(6);
    }
}

Property $maxExceptions จำกัดจำนวนครั้งที่ job สามารถ throw exception ก่อนถูกพิจารณาว่าล้มเหลว แยกจาก $tries ที่นับจำนวน attempts ทั้งหมด Property $backoff กำหนดความล่าช้าระหว่าง retries

Job Batching และ Chaining

Laravel มี job batching สำหรับประมวลผลหลาย jobs เป็นหน่วยเดียวพร้อมการติดตามความคืบหน้าและการจัดการ failure

php
// Dispatch batch of jobs
$batch = Bus::batch([
    new ProcessPodcastEpisode($episode1),
    new ProcessPodcastEpisode($episode2),
    new ProcessPodcastEpisode($episode3),
])
->then(function (Batch $batch) {
    Log::info('All episodes processed successfully');
})
->catch(function (Batch $batch, Throwable $e) {
    Log::error('Batch processing failed', ['error' => $e->getMessage()]);
})
->finally(function (Batch $batch) {
    // Cleanup resources
})
->allowFailures()
->dispatch();

// Monitor batch progress
$progress = Bus::findBatch($batch->id);
echo $progress->progress(); // Percentage complete
echo $progress->failedJobs; // Count of failed jobs

Job chaining รัน jobs ตามลำดับ โดย job ถัดไปจะทำงานเฉพาะเมื่อ job ก่อนหน้าสำเร็จ

php
// Job chain สำหรับ multi-step workflow
Bus::chain([
    new ValidateOrder($order),
    new ProcessPayment($order),
    new SendConfirmationEmail($order),
    new UpdateInventory($order),
])->dispatch();

Service Container และ Dependency Injection

การเข้าใจ service container สำคัญมากสำหรับตำแหน่ง senior ผู้สัมภาษณ์ทดสอบความรู้เกี่ยวกับ binding types, contextual binding และเมื่อใดควรใช้ singleton vs transient bindings

app/Providers/PaymentServiceProvider.phpphp
class PaymentServiceProvider extends ServiceProvider
{
    public function register(): void
    {
        // Singleton: instance เดียวกันตลอด request
        $this->app->singleton(PaymentGateway::class, function ($app) {
            return new StripeGateway(
                config('services.stripe.secret'),
                $app->make(HttpClient::class)
            );
        });

        // Binding interface กับ implementation
        $this->app->bind(
            PaymentProcessorInterface::class,
            StripePaymentProcessor::class
        );

        // Contextual binding: implementation ที่แตกต่างกันตาม consumer
        $this->app->when(SubscriptionController::class)
            ->needs(PaymentProcessorInterface::class)
            ->give(RecurringPaymentProcessor::class);

        $this->app->when(CheckoutController::class)
            ->needs(PaymentProcessorInterface::class)
            ->give(OneTimePaymentProcessor::class);
    }
}

Contextual bindings อนุญาตให้ resolve interface ที่แตกต่างกันตาม class ที่ร้องขอ dependency ซึ่งหลีกเลี่ยง service locator pattern และรักษา dependency injection ที่สะอาด

Repository Pattern และ Service Layer

แม้ว่า Eloquent จะมี query building ที่ทรงพลัง ทีมมักจะ implement repository pattern เพื่อ abstraction และ testability

app/Repositories/Contracts/UserRepositoryInterface.phpphp
interface UserRepositoryInterface
{
    public function findById(int $id): ?User;
    public function findByEmail(string $email): ?User;
    public function createWithProfile(array $userData, array $profileData): User;
    public function getActiveWithSubscriptions(): Collection;
}

// app/Repositories/EloquentUserRepository.php
class EloquentUserRepository implements UserRepositoryInterface
{
    public function __construct(
        private readonly User $model
    ) {}

    public function findById(int $id): ?User
    {
        return $this->model->find($id);
    }

    public function findByEmail(string $email): ?User
    {
        return $this->model->where('email', $email)->first();
    }

    public function createWithProfile(array $userData, array $profileData): User
    {
        return DB::transaction(function () use ($userData, $profileData) {
            $user = $this->model->create($userData);
            $user->profile()->create($profileData);
            return $user->load('profile');
        });
    }

    public function getActiveWithSubscriptions(): Collection
    {
        return $this->model
            ->where('status', 'active')
            ->with(['subscriptions' => fn($q) => $q->active()])
            ->get();
    }
}

Service layer ประกอบด้วย business logic และประสานงานหลาย repositories

app/Services/UserRegistrationService.phpphp
class UserRegistrationService
{
    public function __construct(
        private readonly UserRepositoryInterface $userRepository,
        private readonly NotificationService $notificationService,
        private readonly EventDispatcher $events
    ) {}

    public function register(RegisterUserDTO $dto): User
    {
        $user = $this->userRepository->createWithProfile(
            $dto->toUserArray(),
            $dto->toProfileArray()
        );

        $this->notificationService->sendWelcomeEmail($user);
        $this->events->dispatch(new UserRegistered($user));

        return $user;
    }
}

พร้อมที่จะพิชิตการสัมภาษณ์ Laravel แล้วหรือยังครับ?

ฝึกฝนด้วยตัวจำลองแบบโต้ตอบ, flashcards และแบบทดสอบเทคนิคครับ

Event-Driven Architecture และ Observers

Laravel events แยก components และอนุญาตให้หลาย listeners ตอบสนองต่อ state changes ผู้สัมภาษณ์คาดหวังให้ผู้สมัครอธิบายเมื่อใดควรใช้ events vs observers vs model hooks

app/Events/OrderPlaced.phpphp
class OrderPlaced implements ShouldBroadcast
{
    use Dispatchable, InteractsWithSockets, SerializesModels;

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

    public function broadcastOn(): array
    {
        return [
            new PrivateChannel('orders.' . $this->order->user_id),
        ];
    }
}

// app/Listeners/SendOrderConfirmation.php
class SendOrderConfirmation implements ShouldQueue
{
    public function handle(OrderPlaced $event): void
    {
        Mail::to($event->order->user)
            ->send(new OrderConfirmationMail($event->order));
    }

    public function shouldQueue(OrderPlaced $event): bool
    {
        return $event->order->total > 0;
    }
}

// app/Providers/EventServiceProvider.php
protected $listen = [
    OrderPlaced::class => [
        SendOrderConfirmation::class,
        UpdateInventory::class,
        NotifyWarehouse::class,
        RecordAnalytics::class,
    ],
];

Model observers รวมกลุ่ม event handlers ที่เกี่ยวข้องกับ lifecycle ของ model

app/Observers/OrderObserver.phpphp
class OrderObserver
{
    public function created(Order $order): void
    {
        $order->generateOrderNumber();
        Cache::tags(['orders', "user:{$order->user_id}"])->flush();
    }

    public function updated(Order $order): void
    {
        if ($order->wasChanged('status')) {
            event(new OrderStatusChanged($order));
        }
    }

    public function deleted(Order $order): void
    {
        $order->lineItems()->delete();
        Storage::delete($order->attachments);
    }
}

การทดสอบแอปพลิเคชัน Laravel

การสัมภาษณ์ระดับ senior รวมถึงคำถามเกี่ยวกับกลยุทธ์การทดสอบ Laravel มี helpers สำหรับ feature testing, unit testing และ mocking external services

tests/Feature/OrderControllerTest.phpphp
class OrderControllerTest extends TestCase
{
    use RefreshDatabase;

    public function test_user_can_place_order(): void
    {
        $user = User::factory()->create();
        $product = Product::factory()->create(['price' => 5000]);

        $this->actingAs($user)
            ->postJson('/api/orders', [
                'items' => [
                    ['product_id' => $product->id, 'quantity' => 2]
                ],
                'shipping_address_id' => $user->addresses()->first()->id,
            ])
            ->assertCreated()
            ->assertJsonStructure([
                'data' => ['id', 'total', 'status', 'items']
            ]);

        $this->assertDatabaseHas('orders', [
            'user_id' => $user->id,
            'total' => 10000,
        ]);
    }

    public function test_payment_failure_does_not_create_order(): void
    {
        $this->mock(PaymentGateway::class, function ($mock) {
            $mock->shouldReceive('charge')
                ->once()
                ->andThrow(new PaymentFailedException('Card declined'));
        });

        $user = User::factory()->create();

        $this->actingAs($user)
            ->postJson('/api/orders', $this->validOrderData())
            ->assertStatus(422)
            ->assertJsonPath('message', 'Payment failed: Card declined');

        $this->assertDatabaseMissing('orders', ['user_id' => $user->id]);
    }
}

Middleware และ Request Lifecycle

การเข้าใจ request lifecycle และ middleware pipeline สำคัญมากสำหรับ debugging และ optimization

app/Http/Middleware/EnsureTeamAccess.phpphp
class EnsureTeamAccess
{
    public function handle(Request $request, Closure $next, string $permission): Response
    {
        $team = $request->route('team');
        $user = $request->user();

        if (!$user->belongsToTeam($team)) {
            abort(403, 'You are not a member of this team');
        }

        if (!$user->hasTeamPermission($team, $permission)) {
            abort(403, "Missing permission: {$permission}");
        }

        return $next($request);
    }
}

// routes/web.php
Route::middleware(['auth', 'team.access:manage-projects'])
    ->prefix('teams/{team}')
    ->group(function () {
        Route::resource('projects', ProjectController::class);
    });

บทสรุป

การเตรียมตัวสัมภาษณ์ Laravel ต้องการความเชี่ยวชาญใน Eloquent optimization, queue reliability และ architectural patterns ผู้สัมภาษณ์ในปี 2026 มุ่งเน้นความสามารถของผู้สมัครในการจัดการ workloads ระดับ production: การป้องกัน N+1 ผ่าน eager loading เชิงกลยุทธ์, job idempotency เพื่อป้องกันการประมวลผลซ้ำ และการเชี่ยวชาญ service container สำหรับ maintainable code การเข้าใจ tradeoffs ระหว่าง repository pattern vs Eloquent โดยตรง, event-driven architecture vs procedural code และ cursor vs offset pagination แยกแยะผู้สมัครระดับ senior จาก junior ฝึกฝนกับตัวอย่างโค้ดเหล่านี้และอธิบายเหตุผลเบื้องหลังการตัดสินใจด้านสถาปัตยกรรมแต่ละครั้ง

ชาเลนจ์ประจำวัน

คุณหาบั๊กใน Laravel เจอไหม

โค้ดจริงหนึ่งชิ้น บั๊กที่ซ่อนอยู่หนึ่งจุด วันละหนึ่งครั้ง ลองได้โดยไม่ต้องมีบัญชี

Anthony Fillion-Maillet

เขียนโดย

Anthony Fillion-Maillet

ผู้ก่อตั้ง SharpSkill

เป็นนักพัฒนาฟูลสแตกมากว่า 10 ปี ดูแล SharpSkill และรับผิดชอบทุกสิ่งที่เผยแพร่ที่นี่

อัปเดตเมื่อ 16 กันยายน 2569

แชร์

บทความที่เกี่ยวข้อง