Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Building Web Apps with Laravel  Module 19 – Interview preparation and timed practice

Laravel interview concepts and question bank

Answer Laravel interview questions with the mechanism, a trade-off and a recorded run, on the container, middleware, N+1 queries, transactions and security.

  • Intermediate
  • 15 minutes
  • Examples run with PHP 8.5.11, Composer 2.10.3, Laravel 13.34.0 and Python 3.14.8
  • By MySmartCoPilot
Free lesson, Pro answers: 15 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

What you will learn

  • Structure an answer as a definition, the mechanism, one trade-off, a failure mode and a proof
  • Explain bind, singleton and scoped bindings, facades and middleware order with a recorded run
  • Predict query counts and rollback outcomes in Eloquent code, and prove them with the query log
  • Answer request-forgery, XSS, mass-assignment and IDOR questions with Laravel's own classes and methods

Before you start

What you’ll learn · Before you start

What you will learn

  • Structure an answer as a definition, the mechanism, one trade-off, a failure mode and a proof
  • Explain bind, singleton and scoped bindings, facades and middleware order with a recorded run
  • Predict query counts and rollback outcomes in Eloquent code, and prove them with the query log
  • Answer request-forgery, XSS, mass-assignment and IDOR questions with Laravel's own classes and methods

Before you start

On this page

An interviewer shows you a loop over ten batches that prints each batch’s teacher, and asks how many SQL queries it sends. Run on Laravel 13.34.0, that loop sent 11, and the same loop with with('teacher') sent 2: a number you can predict, explain and then prove is the strongest answer you can give.

This lesson trains that kind of answer on six topics: the shape of a strong answer, the service container and facades, middleware, queries, transactions with queued jobs, and security. Each section shows a short program first, so you can write down your prediction, and then the output it printed when it ran. The questions at the end are original and graded by level.

A strong answer names the mechanism and how it fails

Take the question “What is a facade?”. A common reply is “a static class you call with ::”, and the next question exposes it: if it is static, how do you swap it for a fake in a test? The reply describes the syntax, not the thing.

A strong answer has four parts, and a fifth that you bring with you:

  1. A definition in one exact sentence.
  2. The mechanism: the class or method that does it in Laravel, by name.
  3. One trade-off: what the choice costs, and when you would choose otherwise.
  4. A failure mode: how it breaks, and how you would see it break.
  5. The proof: a run or a test whose output shows the claim.

Step through the facade answer built this way. Each step adds what the reply above lacked:

Interactive · stage by stage Four parts of a strong answer, on one question Step through an answer to "What is a facade?" and see what each part adds to the one before it.
Four parts of a strong answer, on one question, step 6 of 6. Showing What is a facade?, Definition, Mechanism, Trade-off, Failure mode, Proof. In focus: Proof.What is a facade?the questionDefinitionone exact sentenceMechanismthe class and the methodTrade-offwhat it costs youFailure modehow it breaksProofa run you can show

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. The question. An interviewer asks what a facade is. The common weak answer, "a static class", is wrong, because only the call looks static. Each part below fixes one thing that answer lacks.
  2. A definition in one sentence. A facade is a class whose static-looking calls are forwarded to one object that the service container builds. One sentence, and every word of it can be checked.
  3. The mechanism, by name. Name the parts: Cache extends Illuminate\Support\Facades\Facade, and its getFacadeAccessor() returns 'cache', the name of a container binding. The call runs on that object.
  4. One trade-off. Calls are short and each facade can be swapped for a fake in a test. The cost is that the dependency is not in the constructor, so a class can collect many without anyone seeing it.
  5. A real failure mode. Called before the application exists, a facade has nothing to forward to. The probe's first line threw "A facade root has not been set." This happens in a plain script that never boots the app.
  6. The proof. The probe compared Cache::getFacadeRoot() with app('cache') and printed "same object". That line turns the definition from a claim you remember into a fact you showed.

The first four parts can come from any page of the documentation, and the proof is what you bring. Each section below gives you one: a probe of a few lines whose output you can quote. The container probe in the next section supplied the facade answer’s failure mode and its proof, so you can check both lines yourself.

Key idea

An answer that names the class, the cost and the way it fails, and then shows a run, has already answered the next three questions.

Checkpoint

Check: The parts of a strong answer

  1. Question 1 of 2 Which part of a strong answer turns a definition you remember into a fact you showed?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: The proof, a run or a test whose output shows the claim

    Definition, mechanism, trade-off and failure mode are what you say; the proof is what you show. The container probe supplied the facade answer's proof, "same object".

  2. Question 2 of 2 For the answer to "What is a facade?", which of these is a failure mode rather than a trade-off?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: In a script that never boots the application, Cache::get() throws "A facade root has not been set."

    A failure mode is something you can make happen and see. The facade root error appears whenever a facade is called before an application has been created and bound to it.

What the container returns: bind, singleton and scoped

Ask the service container: The object that builds and stores the classes an application needs. Laravel's application object is the container, and providers put their bindings into it. More in “How Laravel handles a request” for the same thing twice, and what you get back depends on how it was registered. The probe below registers one small class three ways, asks for each twice, and then starts a new lifecycle the way a queue worker does before each job. Write down your prediction for every line before you read the run:

container.php: one class, three bindings and a facade PHP · predict/container.php
<?php

// Predict the output: what Laravel's service container hands back when you ask for the same thing
// twice. spl_object_id() names an object, so equal ids mean one object. The first line calls a facade
// before the application exists. Run:  php container.php
use Illuminate\Contracts\Console\Kernel;
use Illuminate\Support\Facades\Cache;

require __DIR__.'/vendor/autoload.php';

function show(string $case, string $result): void
{
    printf("%-34s %s\n", $case, $result);
}

function same(object $a, object $b): string
{
    return spl_object_id($a) === spl_object_id($b) ? 'same object' : 'two objects';
}

// 1. A facade with no application behind it yet.
try {
    Cache::get('fee-total');
} catch (RuntimeException $e) {
    show('Cache::get() before the app boots:', $e->getMessage());
}

$app = require __DIR__.'/bootstrap/app.php';
$app->make(Kernel::class)->bootstrap();

// A class with no constructor arguments, so the container can build it without being told how.
class FeeCalculator {}

$app->bind('fees.bind', fn () => new FeeCalculator);
$app->singleton('fees.singleton', fn () => new FeeCalculator);
$app->scoped('fees.scoped', fn () => new FeeCalculator);

// 2-4. Ask for each binding twice.
show('make() of a class, no binding:', same($app->make(FeeCalculator::class), $app->make(FeeCalculator::class)));
show('bind(), asked twice:', same($app->make('fees.bind'), $app->make('fees.bind')));
show('singleton(), asked twice:', same($app->make('fees.singleton'), $app->make('fees.singleton')));

// 5-6. A new lifecycle: what a queue worker does before each job, and Octane before each request.
$scoped = $app->make('fees.scoped');
$single = $app->make('fees.singleton');
show('scoped(), same lifecycle:', same($scoped, $app->make('fees.scoped')));
$app->forgetScopedInstances();
show('scoped(), next lifecycle:', same($scoped, $app->make('fees.scoped')));
show('singleton(), next lifecycle:', same($single, $app->make('fees.singleton')));

// 7. A facade is a static-looking front for one object in the container.
show('Cache facade vs app(\'cache\'):', same(Cache::getFacadeRoot(), $app->make('cache')));
show('the object behind the facade:', get_class(Cache::getFacadeRoot()));
What make() returned for each binding Python · container_session.py
from session import run

# Asks the service container for the same thing twice under each kind of binding, and calls a facade before
# and after the application boots. The overlay carries the probe; nothing secret is printed.
run(
    """
php container.php
""",
    app=True,
    overlay=("predict",),
)

Output

$ php container.php
Cache::get() before the app boots: A facade root has not been set.
make() of a class, no binding:     two objects
bind(), asked twice:               two objects
singleton(), asked twice:          same object
scoped(), same lifecycle:          same object
scoped(), next lifecycle:          two objects
singleton(), next lifecycle:       same object
Cache facade vs app('cache'):      same object
the object behind the facade:      Illuminate\Cache\CacheManager

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 container_session.py

Read the lines in pairs. bind(), and a class with no binding at all, built a new object on every make(). A singleton: A binding whose object is built the first time something asks for it and then shared until the process ends, registered with singleton() or the #[Singleton] attribute. In a queue worker every job of the worker shares it. More in “The service container and dependency injection” was built once and returned again, even after the new lifecycle. A scoped instance: An object the container shares within one request or one queued job and drops before the next one, registered with scoped() or the #[Scoped] attribute. Queue workers and Octane reset these between units of work. More in “The service container and dependency injection” binding was shared within one lifecycle, and forgetScopedInstances() dropped it, so the next lifecycle got a new object. The queue worker calls that method before every job, and the container documentation says scoped instances are flushed when an Octane worker starts a new request.

Binding Twice in one lifecycle After a new lifecycle
bind() two objects two objects
singleton() same object same object
scoped() same object a new object

The difference shows in an interview the moment the process lives longer than a request. Under PHP-FPM: PHP's FastCGI Process Manager: a service that keeps a pool of PHP processes running, takes each request from the web server over FastCGI, runs the script and returns the response. More in “Deploying Laravel to a Linux server” each request boots a new application, so a singleton lasts one request and the bug stays hidden. A queue worker runs job after job in one booted application, so a singleton that keeps the current user, centre or locale in a property hands it to the next job. Laravel Octane: A first-party package that serves a Laravel application from a long-running server such as FrankenPHP, Swoole or RoadRunner. The application boots once and stays in memory between requests, so state kept in a singleton carries over to the next request. also keeps the booted application in memory between requests, so a singleton can carry such state into a later request there as well.

Keeping the request inside a singleton

The Octane documentation warns against passing the request or the container to the constructor of an object bound as a singleton: the object keeps the first request it saw. Register per-request state with scoped(), or pass the request to the method that needs it.

The last two lines are the facade answer’s proof: Cache::getFacadeRoot() and app('cache') were one CacheManager, which is what a facade: A class that names one entry of Laravel's container and forwards every static call on it to the object stored there, as Cache::get() forwards to the cache manager. More in “Service providers, facades and contracts” is, a static-looking front for one object in the container. The first line was its failure mode, the facade called before any application existed.

Checkpoint

Check: Bindings, lifecycles and facades in the container

  1. Question 1 of 2 The probe asked for FeeCalculator::class twice with no binding registered for it. What did make() return?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Two different objects, because the container builds a class it was not told about each time

    Concrete classes are built on demand. Sharing needs singleton(), scoped() or instance(); everything else is a new object on each make().

  2. Question 2 of 2 Pathshala builds its centre reports in a queue worker. A service keeps the current centre's id in a property and is registered with singleton(). What does the next job in the same worker see, and what fixes it?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: The first job's centre; register the service with scoped(), which is dropped before each new job

    The probe's "singleton(), next lifecycle: same object" and "scoped(), next lifecycle: two objects" are this answer in two lines.

Middleware wrap the controller: in, then out in reverse

A route lists two middleware: A class that sits between the web server and the controller. It can inspect or change a request, stop it with its own response, or pass it inward and then change the response on the way out. More in “How Laravel handles a request”, Outer and then Inner. Each prints one line before it calls $next($request) and another after it, and Outer also has a terminate() method. The probe sends two requests through the real HTTP kernel: The class that runs a web request from start to finish: it boots the framework, sends the request through the middleware and the router, and returns a response. More in “How Laravel handles a request”, and the second asks for a closed report, which Inner refuses. Predict both sets of lines:

Two middleware, the route and the probe

predict/app/Http/Middleware/Outer.php

<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;

// The first route middleware of the probe. It prints a line before it passes the request on, a line
// when the response comes back, and a third from terminate(), which runs after the response is sent.
class Outer
{
    public function handle(Request $request, Closure $next): Response
    {
        echo "Outer: on the way in\n";
        $response = $next($request);
        echo "Outer: on the way out, status {$response->getStatusCode()}\n";

        return $response;
    }

    public function terminate(Request $request, Response $response): void
    {
        echo "Outer: terminate(), after the response\n";
    }
}

predict/app/Http/Middleware/Inner.php

<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;

// The second route middleware of the probe. When the request asks for a closed report it answers
// 403 itself and never calls $next, so the controller does not run.
class Inner
{
    public function handle(Request $request, Closure $next): Response
    {
        if ($request->boolean('closed')) {
            echo "Inner: answers 403 itself, never calls \$next\n";

            return response('this report is closed', 403);
        }

        echo "Inner: on the way in\n";
        $response = $next($request);
        echo "Inner: on the way out\n";

        return $response;
    }
}

routes/web.php · predict/routes/web.php

<?php

use App\Http\Middleware\Inner;
use App\Http\Middleware\Outer;
use Illuminate\Support\Facades\Route;

// Two routes for the predict-the-output probes. /report runs two route middleware in the order they
// are listed; /notices is a plain POST route inside the default web group, so the request-forgery
// check runs on it exactly as it would on a form.
Route::get('/report', function () {
    echo "Controller: builds the response\n";

    return response('the fee report');
})->middleware([Outer::class, Inner::class]);

Route::post('/notices', fn () => response('a notice was posted'));

predict/middleware.php

<?php

// Predict the output: two requests through the real HTTP kernel to a route with two middleware,
// Outer and Inner, listed in that order. The second request asks for a closed report, which Inner
// refuses. terminate() is called the way public/index.php calls it, after the response. Run:  php middleware.php
use Illuminate\Contracts\Http\Kernel;
use Illuminate\Http\Request;

require __DIR__.'/vendor/autoload.php';
$app = require __DIR__.'/bootstrap/app.php';
$kernel = $app->make(Kernel::class);

foreach (['/report', '/report?closed=1'] as $uri) {
    echo "--- GET {$uri}\n";
    $request = Request::create($uri, 'GET');
    $response = $kernel->handle($request);
    echo "Kernel: handle() returned {$response->getStatusCode()}\n";
    $kernel->terminate($request, $response);
}
The order in which each layer ran, for two requests Python · middleware_session.py
from session import run

# Sends two requests through the real HTTP kernel to a route with two middleware and prints the order in
# which each layer and the controller ran. The overlay carries the probe, the two middleware and the routes.
run(
    """
php middleware.php
""",
    app=True,
    overlay=("predict",),
)

Output

$ php middleware.php
--- GET /report
Outer: on the way in
Inner: on the way in
Controller: builds the response
Inner: on the way out
Outer: on the way out, status 200
Kernel: handle() returned 200
Outer: terminate(), after the response
--- GET /report?closed=1
Outer: on the way in
Inner: answers 403 itself, never calls $next
Outer: on the way out, status 403
Kernel: handle() returned 403
Outer: terminate(), after the response

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 middleware_session.py

The code before $next ran in the order the route lists, and the code after it ran in reverse, because each middleware wraps the ones listed after it. For the closed report, Inner returned its own 403 without calling $next: the controller never ran and Inner had no way out to print, yet Outer still printed the 403 on its way out. Step through both requests:

Interactive · message by message Two requests through Outer, Inner and a controller Step through GET /report and then GET /report?closed=1, and watch the order in which each layer runs.
Two requests through Outer, Inner and a controller, step 7 of 7. Messages so far: Outer to Inner: $next($request); Inner to Controller: $next($request); Controller to Inner: 200 response; Inner to Outer: 200 response; Inner to Outer: 403, no controller.Outer200 outInnersecondControllernever ran$next($request)$next($request)200 response200 response403, no controller

The drawing shows the whole scene; its 7 steps are listed below.

Every step as text
  1. Outer runs first. The kernel passes GET /report to the first middleware the route lists. Outer prints its line for the way in, then calls $next($request).
  2. Inner runs second. Inner prints its own line and calls $next in turn. Only when the last middleware has called $next does the router run the controller.
  3. The controller answers. The controller builds the response, a 200 with the fee report, and returns it to the layer that called it.
  4. Back out through Inner. Inner's code after $next runs now, with the response in hand. On the way out the layers run in reverse order.
  5. Outer runs last on the way out. Outer prints its line for the way out and could still change the response, for example add a header. The kernel's handle() returns 200.
  6. terminate() after the response. After the response is sent, the kernel calls terminate() on every middleware that has one. Outer's third line came after "handle() returned 200".
  7. A layer can answer itself. For ?closed=1, Inner returns a 403 without calling $next, so the controller never runs. Outer still sees the 403 on its way out, because it wraps Inner.

terminate() ran last, after handle() had returned, because the kernel calls it once the response has been sent; the middleware documentation adds that with FastCGI the browser already has the response by then. The probe is the request lifecycle: The path one request takes through an application: the front controller, the kernel, the middleware, the router and the controller, then the response and the work that runs after it. More in “How Laravel handles a request” in miniature. public/index.php loads the application from bootstrap/app.php, and the kernel bootstraps it: configuration, error handling and logging, then every service provider’s register() and, once all are registered, each one’s boot(). Global middleware run next, the router picks the route, and its group and route middleware run around the controller. The response travels back out through the same layers before it is sent and terminated.

Checkpoint

Check: The order middleware run in

  1. Question 1 of 2 The route lists [Outer::class, Inner::class]. Which line did the probe print right after "Controller: builds the response"?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Inner: on the way out

    In the listed order on the way in, in reverse on the way out: Outer, Inner, controller, Inner, Outer.

  2. Question 2 of 2 For GET /report?closed=1, which lines did the probe print?

    Choose every answer that is right.

    Show the answer: Question 2 of 2

    Answer:

    • Outer: on the way out, status 403
    • Outer: on the way in

    A middleware that answers itself stops the request going inward, but every layer outside it still sees the response it returned.

Counting queries: 11 for the lazy loop, 2 with eager loading

Here is the opening’s question, run. The probe makes ten batches with one teacher each, sends the same loop four ways and then switches lazy loading off. The query log: A record of every SQL statement a connection ran during a request, with its text, bindings and time. Laravel's DB::enableQueryLog() collects it, DB::getQueryLog() returns it, DB::listen() watches each one. More in “Databases and the query builder” records every statement, so its length is the count. Predict each number before you read it:

queries.php: one loop, four ways PHP · predict/queries.php
<?php

// Predict the output: how many SQL statements each version of one loop sends for ten batches and
// their teachers. The query log records every statement Eloquent sends, so its length is the count.
// Run:  php queries.php
use Illuminate\Contracts\Console\Kernel;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\LazyLoadingViolationException;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;

require __DIR__.'/vendor/autoload.php';
$app = require __DIR__.'/bootstrap/app.php';
$app->make(Kernel::class)->bootstrap();

class Teacher extends Model
{
    public $timestamps = false;
}

class Batch extends Model
{
    public $timestamps = false;

    public function teacher()
    {
        return $this->belongsTo(Teacher::class);
    }
}

Schema::create('teachers', function ($table) {
    $table->id();
    $table->string('name');
});
Schema::create('batches', function ($table) {
    $table->id();
    $table->string('name');
    $table->foreignId('teacher_id');
});
foreach (range(1, 10) as $i) {
    DB::table('teachers')->insert(['name' => "Teacher {$i}"]);
    DB::table('batches')->insert(['name' => "Batch {$i}", 'teacher_id' => $i]);
}

// Run some work and return how many statements it sent.
function queries(callable $work): int
{
    DB::flushQueryLog();
    DB::enableQueryLog();
    $work();
    DB::disableQueryLog();

    return count(DB::getQueryLog());
}

function show(string $case, string $result): void
{
    printf("%-44s %s\n", $case, $result);
}

$names = fn ($batches) => $batches->map(fn ($batch) => $batch->teacher->name)->all();

show('1. Batch::all(), then each ->teacher', queries(fn () => $names(Batch::all())).' queries');
show('2. Batch::with(\'teacher\')->get()', queries(fn () => $names(Batch::with('teacher')->get())).' queries');
show('3. Batch::all()->load(\'teacher\')', queries(fn () => $names(Batch::all()->load('teacher'))).' queries');

Model::automaticallyEagerLoadRelationships();
show('4. Batch::all(), automatic eager loading on', queries(fn () => $names(Batch::all())).' queries');
Model::automaticallyEagerLoadRelationships(false);

// What AppServiceProvider::boot() does with Model::preventLazyLoading(! app()->isProduction()).
Model::preventLazyLoading();
try {
    $names(Batch::all());
    show('5. lazy loading prevented, ten batches', 'no exception');
} catch (LazyLoadingViolationException $e) {
    show('5. lazy loading prevented, ten batches', class_basename($e));
    echo '   '.$e->getMessage()."\n";
}
show('6. lazy loading prevented, Batch::find(1)', queries(fn () => Batch::find(1)->teacher->name).' queries, no exception');
The statements each version of the loop sent Python · queries_session.py
from session import run

# Counts the SQL statements four versions of one loop send for ten batches and their teachers, then turns
# lazy loading off. The overlay carries the probe; it makes its own two tables in the project's SQLite file.
run(
    """
php queries.php
""",
    app=True,
    overlay=("predict",),
)

Output

$ php queries.php
1. Batch::all(), then each ->teacher         11 queries
2. Batch::with('teacher')->get()             2 queries
3. Batch::all()->load('teacher')             2 queries
4. Batch::all(), automatic eager loading on  2 queries
5. lazy loading prevented, ten batches       LazyLoadingViolationException
   Attempted to lazy load [teacher] on model [Batch] but lazy loading is disabled.
6. lazy loading prevented, Batch::find(1)    2 queries, no exception

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 queries_session.py

The first loop is the N+1 query problem: A page that sends one query for a list and then one more for every row of it, usually by reading a relationship inside a loop, so its query count is 1 + N and grows with the data. More in “Eager loading and the N+1 query problem”: one query for the batches and one for each batch’s teacher, 1 + 10 = 11, a count that grows with the data. eager loading: Loading a relationship for a whole set of models at once, before the code reads it: with('student') sends one extra query with an IN list of keys and matches the rows in memory. More in “Eager loading and the N+1 query problem” with with('teacher') sent 2, the batches and then one query for every teacher whose id is in the list. load('teacher') did the same after the batches were already in memory, and automatic eager loading: Laravel's mode, switched on with Model::automaticallyEagerLoadRelationships(), that loads a relationship for a whole collection the first time one of its models reads it lazily. More in “Eager loading and the N+1 query problem” reached the same 2 without any change to the loop.

Line 5 is the guard. With Model::preventLazyLoading() on, the lazy loop threw LazyLoadingViolationException at its first lazy read. Line 6 is the detail that lifts an answer above the definition: Batch::find(1)->teacher loaded lazily and threw nothing. In Laravel 13.34.0, Eloquent marks models for the check only when a query returns more than one of them, because a single lazy load cannot multiply.

Turning the guard on everywhere, or trusting it alone

The documentation’s own example passes ! $this->app->isProduction(), so development and tests throw while production keeps serving a page that slipped through. The guard also misses the single model of line 6, and a query in a loop that does not go through a relationship at all, such as Teacher::find($id) for each row. Count the queries too.

Checkpoint

Check: Query counts, eager loading and the lazy-loading guard

  1. Question 1 of 2 The lazy loop sent 11 queries for 10 batches. How many would it send for 25 batches, each with its teacher?

    Type a number.

    Show the answer: Question 1 of 2

    Answer: 26 queries

    One query for the batches and one per batch for its teacher: 1 + 25 = 26. With with('teacher') it stays at 2 however many batches there are.

  2. Question 2 of 2 With Model::preventLazyLoading() on, Batch::find(1)->teacher sent 2 queries and threw nothing. Why?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Eloquent marks models for the check only when a query returns more than one, since one lazy load cannot multiply

    The guard catches the pattern that multiplies, a relationship loaded for each model of a list, and leaves a single model alone.

Transactions and queued jobs: what survives a rollback

A fee payment is saved inside DB::transaction(), and a job that sends the receipt is dispatched in the same closure. Then the gateway times out and the closure throws. The probe runs this three ways on the sync queue, which runs a job the moment it is dispatched, the earliest a real worker could take it:

transactions.php: one payment, one receipt job, three ways PHP · predict/transactions.php
<?php

// Predict the output: a fee payment saved inside a database transaction that fails, and a receipt
// job dispatched inside it, first as a plain dispatch and then with afterCommit(). The queue is the
// sync driver here: it runs a job the moment it is dispatched, the earliest a real worker could pick
// it up. Run:  php transactions.php
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Console\Kernel;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;

require __DIR__.'/vendor/autoload.php';
$app = require __DIR__.'/bootstrap/app.php';
$app->make(Kernel::class)->bootstrap();
config(['queue.default' => 'sync']);

class SendReceipt implements ShouldQueue
{
    use Dispatchable, Queueable;

    public static int $sent = 0;

    public function __construct(public int $paymentId) {}

    public function handle(): void
    {
        self::$sent++;
        $row = DB::table('payments')->where('id', $this->paymentId)->exists() ? 'exists' : 'is missing';
        echo "   job ran: receipt for payment {$this->paymentId}, the payment row {$row}\n";
    }
}

Schema::create('payments', function ($table) {
    $table->id();
    $table->unsignedInteger('paise');
});

// Run one payment attempt; $fail makes it throw after the insert and the dispatch, as a gateway timeout would.
function attempt(string $case, bool $afterCommit, bool $fail): void
{
    echo "{$case}\n";
    SendReceipt::$sent = 0;
    try {
        DB::transaction(function () use ($afterCommit, $fail) {
            $id = DB::table('payments')->insertGetId(['paise' => 150000]);
            $afterCommit ? SendReceipt::dispatch($id)->afterCommit() : SendReceipt::dispatch($id);
            if ($fail) {
                throw new RuntimeException('gateway timeout');
            }
        });
    } catch (RuntimeException $e) {
        echo "   transaction rolled back: {$e->getMessage()}\n";
    }
    printf("   payments in the table: %d, receipts sent: %d\n", DB::table('payments')->count(), SendReceipt::$sent);
    DB::table('payments')->delete();
}

attempt('1. dispatch() inside, then the transaction fails', afterCommit: false, fail: true);
attempt('2. dispatch()->afterCommit() inside, then it fails', afterCommit: true, fail: true);
attempt('3. dispatch()->afterCommit() inside, and it commits', afterCommit: true, fail: false);
Payments kept and receipts sent in each case Python · transactions_session.py
from session import run

# Saves a payment inside a transaction that fails and dispatches a receipt job inside it, with and without
# afterCommit(), on the sync queue. The overlay carries the probe; it makes its own table.
run(
    """
php transactions.php
""",
    app=True,
    overlay=("predict",),
)

Output

$ php transactions.php
1. dispatch() inside, then the transaction fails
   job ran: receipt for payment 1, the payment row exists
   transaction rolled back: gateway timeout
   payments in the table: 0, receipts sent: 1
2. dispatch()->afterCommit() inside, then it fails
   transaction rolled back: gateway timeout
   payments in the table: 0, receipts sent: 0
3. dispatch()->afterCommit() inside, and it commits
   job ran: receipt for payment 1, the payment row exists
   payments in the table: 1, receipts sent: 1

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 transactions_session.py

A database transaction: A group of database writes that succeed or fail together: either all of them are committed, or a rollback undoes every one. Work outside the database is not undone. More in “Events, listeners and model observers” undoes database writes, and only those. In case 1 the job ran inside the transaction, found the payment row and sent its receipt; then the rollback removed the row, so the guardian holds a receipt for a payment the table does not have. A Redis or SQS queue reaches the same state another way: the queue documentation warns that a worker may take the job before the transaction commits, so the job can also find no row at all.

afterCommit() changes when the job is queued, not what it does. In case 2 the rollback discarded the job and nothing was sent; in case 3 the job waited for the commit and ran with the row in place. The same after commit: Running a handler only once the surrounding database transaction has committed, so it never acts on writes that a rollback undoes. With no transaction open, the handler runs at once. More in “Events, listeners and model observers” behaviour exists as the after_commit option of a queue connection, and an Eloquent observer gets it by implementing ShouldHandleEventsAfterCommit.

Two more details complete the answer. DB::transaction() throws the closure’s exception again after the rollback, which is why the probe caught it outside, and its attempts argument retries the whole closure when a deadlock happens. A job can also run twice, after a timeout or a crashed worker, and afterCommit() does nothing about that; it needs a different fix, which one of the interview questions below takes up.

Checkpoint

Check: Transactions and jobs dispatched inside them

  1. Question 1 of 2 In case 1 the job printed "the payment row exists" and then the transaction rolled back. What did the guardian end up with?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: A receipt for a payment the table no longer has

    The run printed "payments in the table: 0, receipts sent: 1". A transaction protects the database, not mail, messages or anything else a job touches.

  2. Question 2 of 2 Which statements about dispatch()->afterCommit() match the run?

    Choose every answer that is right.

    Show the answer: Question 2 of 2

    Answer:

    • On a rollback the job is discarded and never runs
    • On a commit the job runs after it, when the row is in place

    afterCommit() ties the moment a job is queued to the transaction's outcome: discarded on rollback, queued after commit.

Security answers with the classes Laravel uses

Security questions punish vague answers most, because “Laravel handles it” names nothing. The probe below shows four defences on one ordinary value each. Predict every line, the two validation messages included:

security.php: four defences, one value each PHP · predict/security.php
<?php

// Predict the output: four of Laravel's defences, each shown on one ordinary value: the
// request-forgery check, Blade escaping, validation and mass-assignment protection. Nothing here
// leaves this project. Run:  php security.php
use Illuminate\Contracts\Http\Kernel;
use Illuminate\Database\Eloquent\MassAssignmentException;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\Facades\Validator;

require __DIR__.'/vendor/autoload.php';
$app = require __DIR__.'/bootstrap/app.php';
$kernel = $app->make(Kernel::class);

function show(string $case, string $result): void
{
    printf("%-46s %s\n", $case, $result);
}

// 1-2. Request forgery: a POST to a web route, without a token, then with the header a browser sets.
$plain = $kernel->handle(Request::create('/notices', 'POST'));
show('1. POST /notices, no token', (string) $plain->getStatusCode());
$sameOrigin = $kernel->handle(Request::create('/notices', 'POST', server: ['HTTP_SEC_FETCH_SITE' => 'same-origin']));
show('2. POST /notices, Sec-Fetch-Site: same-origin', (string) $sameOrigin->getStatusCode());

// 3-4. Output: the same stored name printed with {{ }} and with {!! !!}.
$name = '<b>Asha</b>';
show('3. Blade {{ $name }}', Blade::render('{{ $name }}', ['name' => $name]));
show('4. Blade {!! $name !!}', Blade::render('{!! $name !!}', ['name' => $name]));

// 5-6. Validation: the messages, and what validated() keeps.
$input = ['email' => 'asha@', 'phone' => '98765'];
$validator = Validator::make($input, ['email' => 'required|email', 'phone' => 'required|digits:10']);
foreach ($validator->errors()->all() as $message) {
    show('5. validation message', $message);
}
$valid = Validator::make(['name' => 'Asha', 'is_admin' => '1'], ['name' => 'required|string']);
show('6. validated() keeps', implode(', ', array_keys($valid->validated())));

// 7-8. Mass assignment: a model that allows two fields, given a third.
class Guardian extends Model
{
    protected $fillable = ['name', 'phone'];
}

$guardian = new Guardian(['name' => 'Asha', 'is_admin' => 1]);
show('7. new Guardian([...]) keeps', implode(', ', array_keys($guardian->getAttributes())));
Model::preventSilentlyDiscardingAttributes();
try {
    new Guardian(['name' => 'Asha', 'is_admin' => 1]);
} catch (MassAssignmentException $e) {
    show('8. with discarding prevented', class_basename($e));
    echo '   '.$e->getMessage()."\n";
}
What each defence did Python · security_session.py
from session import run

# Shows four defaults on one ordinary value each: the request-forgery check, Blade escaping, validation and
# mass-assignment protection. The overlay carries the probe and its routes; tokens and cookies are never printed.
run(
    """
php security.php
""",
    app=True,
    overlay=("predict",),
)

Output

$ php security.php
1. POST /notices, no token                     419
2. POST /notices, Sec-Fetch-Site: same-origin  200
3. Blade {{ $name }}                           &lt;b&gt;Asha&lt;/b&gt;
4. Blade {!! $name !!}                         <b>Asha</b>
5. validation message                          The email field must be a valid email address.
5. validation message                          The phone field must be 10 digits.
6. validated() keeps                           name
7. new Guardian([...]) keeps                   name
8. with discarding prevented                   MassAssignmentException
   Add fillable property [is_admin] to allow mass assignment on [Guardian].

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 security_session.py

Lines 1 and 2 are the request-forgery check: a POST with no proof got 419, and one carrying the browser-set Sec-Fetch-Site: A request header the browser sets itself, saying whether a request came from the same origin, the same site or another site. A page cannot forge it, so Laravel 13 accepts same-origin as proof that a form was posted from its own pages. More in “Laravel's security model and hardening” header with same-origin got 200. Lines 3 and 4 are output escaping: Converting characters such as < and > into HTML entities before printing a value, so untrusted text shows as text; Blade's {{ }} does this with htmlspecialchars to prevent cross-site scripting. More in “Your first page: route, controller and view”: {{ $name }} printed the tags as entities, and {!! $name !!} printed them raw. Line 6 shows validated data: The array a validator hands back: only the fields that had rules, holding the values the request carried, with no key the rules did not declare. More in “Validation rules and error messages” keeping only name, and lines 7 and 8 show mass assignment: Creating or updating a model from a whole array of fields in one call, which is why a model declares the columns such an array is allowed to write. More in “Your first CRUD feature with Eloquent” protection dropping is_admin silently and then, with discarding prevented, throwing. Each defence becomes one row of an answer:

Question Laravel’s mechanism How it fails
Request forgery PreventRequestForgery on the web group a route left out of the check, or a GET that changes data
Cross-site scripting Blade’s {{ }} and htmlspecialchars {!! !!} on text a visitor wrote
Mass assignment $fillable, and validated() before create() $guarded = [] with $request->all()
IDOR a policy, or a query scoped to the owner Note::find($id) straight from the URL

The fourth row has no line in this probe, because an insecure direct object reference (IDOR): Access to a record through a real identifier, such as an id in a URL, that the application loaded but failed to authorize for the current user. More in “Authorization: gates and policies” is a missing check rather than a default you can print. The vulnerabilities lab runs it: the scoped query returned 404 for another centre’s note, and the unscoped one returned the note. An answer that names the policy or the scoped query, shows that 404, and says the check belongs on every route that takes an id is the complete one.

Checkpoint

Check: Security answers with Laravel's mechanisms

  1. Question 1 of 2 What did Blade print for {{ $name }} when the stored name was <b>Asha</b>?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: &lt;b&gt;Asha&lt;/b&gt;, so the browser shows the tags as text

    Escaped output is the default, and {!! !!} is the opt-out that a stored cross-site scripting flaw needs.

  2. Question 2 of 2 A controller calls Guardian::create($request->validated()). The form posts name and is_admin, and the rules name only name. What does create() receive?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Only name, because validated() returns just the fields that had rules

    validated() is the first filter and $fillable the second; an answer that names both shows why one forgotten line does not open the column.

Practise: a week of spoken answers

Give each of these one sitting of about thirty minutes in your own copy of Pathshala, a day or two apart.

  1. Answer each section’s question aloud in the five parts, with a two-minute timer.
  2. Change one line of a probe, for example singleton() to scoped() or the order of the two middleware, write down the output you expect, then run it.
  3. Write down how many queries one page of your own app sends, then count them with DB::enableQueryLog().
  4. Score each answer with the table below and spend the next session on the weakest level you did not reach.
Level What the answer covers From this lesson
Fresher the definition and one example bind() builds a new object each time
2 to 5 years the mechanism by name, and a run the 11 and the 2 from the query log
5 to 10 years the trade-off and the edge case the single model the guard skips
10 years and more when not to use it, and how to roll it out singletons that leak under Octane

Practice that adapts to you

Practise this lesson

13 questions about 6 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 13 In what order does the lesson build a strong answer?

    Choose one answer.

    Show the answer: Question 1 of 13

    Answer: Definition, mechanism, trade-off, failure mode, then the proof

    The order follows the questions an interviewer asks next: what is it, how does Laravel do it, what does it cost, how does it break, and how do you know.

  2. Question 2 of 13 Asked "Why inject a dependency instead of calling a facade?", which reply has the strongest trade-off part?

    Choose one answer.

    Show the answer: Question 2 of 13

    Answer: Injection shows each dependency in the constructor, so it is visible and a test passes a fake straight in; a facade is shorter and can be faked too, but hides how many dependencies a class has

    A trade-off names two sides. The probe's "same object" line shows that both reach one CacheManager, so the difference is visibility and size, not behaviour.

  3. Question 3 of 13 Which registration makes the container build a new object every time make() is called?

    Choose one answer.

    Show the answer: Question 3 of 13

    Answer: bind()

    bind() registers a recipe, and the container follows it on every make().

  4. Question 4 of 13 A queue worker runs 1,000 jobs in one process. A service registered with scoped() holds the current job's centre. What does the next job get?

    Choose one answer.

    Show the answer: Question 4 of 13

    Answer: A new object, because the worker calls forgetScopedInstances() before each job

    A worker process lives through many jobs, so per-job state belongs in a scoped binding, never in a singleton.

  5. Question 5 of 13 When did Outer's terminate() method run?

    Choose one answer.

    Show the answer: Question 5 of 13

    Answer: After handle() had returned the response, as the last line of each request

    terminate() is for work the visitor should not wait for, such as writing a log line, and it runs after the response for route middleware as well as global ones.

  6. Question 6 of 13 A middleware must add a header to every response of a route, including a 403 that another middleware of the route returns. Where does it go, and where in handle() does it add the header?

    Choose one answer.

    Show the answer: Question 6 of 13

    Answer: Listed before the middleware that refuses, adding the header after $next returns

    Outer, after $next: the same position from which Outer printed "status 403" for the closed report.

  7. Question 7 of 13 Which code loads the teachers of ten batches with one extra query?

    Choose one answer.

    Show the answer: Question 7 of 13

    Answer: Batch::with('teacher')->get()

    Eager loading asks for every related row in one query with the keys of the whole list.

  8. Question 8 of 13 Why does the documentation pass ! $this->app->isProduction() to Model::preventLazyLoading()?

    Choose one answer.

    Show the answer: Question 8 of 13

    Answer: So development and tests throw on a lazy load, while production keeps serving a page that slipped through

    Strict in development, forgiving in production: the guard finds the mistake before visitors pay for it.

  9. Question 9 of 13 The closure given to DB::transaction() inserts a row and then throws. What happens?

    Choose one answer.

    Show the answer: Question 9 of 13

    Answer: The insert is rolled back and the exception is thrown again to the caller

    All the writes, or none of them, and the caller hears about the failure.

  10. Question 10 of 13 The queue runs on Redis with a separate worker. A job dispatched inside a transaction, without afterCommit(), reads the payment row. What can go wrong even when the transaction commits?

    Choose one answer.

    Show the answer: Question 10 of 13

    Answer: The worker can take the job before the commit, so its query finds no row yet

    With afterCommit(), or after_commit on the connection, the job is queued only once the row is committed.

  11. Question 11 of 13 Where in a Blade view does a stored cross-site scripting flaw appear?

    Choose one answer.

    Show the answer: Question 11 of 13

    Answer: Where a value a visitor stored is printed with {!! !!}

    Look for {!! !!} on anything a visitor could have written, and keep it for HTML you produced yourself.

  12. Question 12 of 13 Which request did the request-forgery check refuse with 419?

    Choose one answer.

    Show the answer: Question 12 of 13

    Answer: A POST with no token and no Sec-Fetch-Site header

    Laravel 13 accepts a same-origin Sec-Fetch-Site or a matching token and refuses a state-changing request with neither.

  13. Question 13 of 13 How do you stop one centre's staff from reading another centre's notes by changing the id in /notes/{id}, in Laravel's terms?

    Choose one answer.

    Show the answer: Question 13 of 13

    Answer: Scope the query to the user's centre, or authorize the note with a policy, so another centre's id gives 404 or 403

    An IDOR is a missing authorization check, so the answer names one: a scoped query, a policy, or both.

Interview questions

Warm-up (fresher): what is the difference between bind() and singleton() in Laravel’s service container?

bind() registers a recipe that the container follows on every make(), so each call gets a new object; singleton() builds the object the first time and returns that same object for as long as the application lives. The lesson’s probe printed “two objects” for bind() asked twice and “same object” for singleton(). The difference matters most in long-lived processes: a queue worker serves many jobs from one booted application, so a singleton that keeps per-job data hands it to the next job, and under Octane, which keeps the application in memory between requests, a singleton can carry per-request data into a later request. For state that belongs to one request or one job, scoped() shares the object within that lifecycle and drops it before the next.

Warm-up (fresher to 2-5 years): in what order do two route middleware run around the controller, and when does terminate() run?

In the order the route lists them on the way in, and in reverse on the way out, because each middleware wraps the ones after it. For a route with Outer and then Inner, the probe printed Outer in, Inner in, the controller, Inner out, Outer out. A middleware that returns its own response without calling $next stops the request there: the controller never runs, but every layer outside it still sees that response on its way out. terminate() runs after the response has been sent, so it suits work the visitor should not wait for, such as a log line.

Two longer questions grow out of this lesson’s probes, one on a page’s query count and one on a receipt job that runs twice. Sketch your first two moves for each before you open it, and check them against the opening of the answer:

Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.

  1. Mid to senior

    Pathshala's batch report shows 25 batches a page, each with its teacher and its number of enrolled students, and it sends 52 queries a page. Bring it to a fixed number, keep it there with a test, and say what changes when an export must cover 5,000 batches.

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Senior to staff

    Pathshala's receipt job waits on a slow mail server, the queue retries it, and a guardian receives two receipts for one payment. Explain the ways a Laravel job can run more than once, make the receipt safe to repeat, and say how a test proves it.

    Model answer, Pro. First step and follow-ups

Flashcards · 8 cards

Recall it, then check

Say the answer to yourself before you look: recalling it is what makes it stick.

  1. Which five parts make a strong answer to a Laravel interview question?

    A one-sentence definition, the mechanism (the class or method that does it, by name), one trade-off, a failure mode and how you would see it, and the proof: a run or a test whose output shows the claim.

  2. Why is "a static class you call with ::" a weak answer to "what is a facade"?

    It describes the syntax, not the thing, and the follow-up about faking it in a test exposes it. A facade is a static-looking front for one object in the container: Cache::getFacadeRoot() and app('cache') were the same CacheManager.

  3. What did the container probe return for bind(), singleton() and scoped(), asked twice and after a new lifecycle?

    bind(): two objects, both times. singleton(): the same object, even after the new lifecycle. scoped(): the same object within a lifecycle and a new one after forgetScopedInstances(), which a queue worker calls before every job.

  4. In which order did two route middleware, Outer then Inner, print their before and after lines?

    The before code ran in the listed order and the after code in reverse, because each wraps the ones after it. When Inner refused with its own 403, the controller never ran, but Outer still printed on its way out; terminate() ran last, after the response was sent.

  5. How many queries did a loop over ten batches printing each teacher send, lazily and with with('teacher')?

    11 lazily, 1 + 10, a count that grows with the data; 2 with with('teacher'), and the same 2 with load() or automatic eager loading. With preventLazyLoading() on, the lazy loop threw at its first lazy read.

  6. Which lazy queries does preventLazyLoading() not catch?

    A single model fetched alone, such as Batch::find(1)->teacher, because Eloquent marks models for the check only when a query returns more than one; and a query in a loop that does not go through a relationship, such as Teacher::find($id) per row. Count the queries too.

  7. A receipt job dispatched inside DB::transaction() ran before the closure threw. What went wrong, and what fixes it?

    The job found the payment row and sent the receipt, then the rollback removed the row: a receipt for a payment that does not exist. afterCommit() queues the job only after the commit and discards it on rollback; it does nothing about a job that runs twice.

  8. Which Laravel mechanism answers each of request forgery, XSS, mass assignment and IDOR?

    PreventRequestForgery on the web group; Blade's double-brace echo through htmlspecialchars; $fillable with validated() before create(); a policy or a query scoped to the owner. Each fails when skipped: a route outside the check, a raw echo of visitor text, $guarded = [] with all(), Note::find($id) from the URL.

Key takeaways

  • A strong answer gives a definition, the mechanism by name, one trade-off and a failure mode, and backs them with a run whose output you can quote.
  • bind() builds a new object on every make(), singleton() shares one for as long as the application lives, and scoped() shares one per request or job, which is what Octane and queue workers need.
  • Middleware run in their listed order on the way in and in reverse on the way out; one that answers itself stops the request, and terminate() runs after the response is sent.
  • A relationship read in a loop costs 1 + N queries, 11 for ten batches, while eager loading costs 2; preventLazyLoading() catches the loop but not a single model.
  • A rollback undoes database writes only, so a job dispatched inside a transaction can act on a row that disappears; afterCommit() queues it only after the commit.
  • Security answers name the mechanism: the request-forgery middleware, Blade’s escaping, $fillable with validated(), and a policy or an owner-scoped query against IDOR.

Terms in this lesson

Laravel Octane
A first-party package that serves a Laravel application from a long-running server such as FrankenPHP, Swoole or RoadRunner. The application boots once and stays in memory between requests, so state kept in a singleton carries over to the next request.

References

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.