Insights into Company Bills & Payments
← PHP & Laravel

Laravel 5-Year Interview (Q26–50): Routing, Validation & Security

Understand the idea. Compare it. Explain it. Test yourself.

Question 21 of 25

Q46. What is Laravel's session system?

Solution and explanation

  • HTTP is stateless: each request arrives with no memory of the previous one.
  • A session stores data on the server and links it to a browser through a cookie containing only a random session id.
Browser                         Server
  cookie: laravel_session=Abc   ->  look up session Abc in storage  ->  ['user_id' => 42, 'cart_id' => 9]
  • API:
session(['cart_id' => 123]);                 // write
$cartId = session('cart_id');                // read
$request->session()->put('step', 2);
$request->session()->forget('step');
$request->session()->flash('status', 'Order placed!');   // available on the next request only
session()->regenerate();                     // new id after login (prevents session fixation)
session()->invalidate();                     // destroy on logout
  • E-commerce example: a guest builds a cart (stored in the session), then logs in.
  • You merge the session cart into the database cart, then regenerate the session id.
  • Banking security settings (config/session.php):
'lifetime' => 10,            // minutes of inactivity before logout
'expire_on_close' => true,
'secure' => true,            // cookie only over HTTPS
'http_only' => true,         // JavaScript cannot read it
'same_site' => 'strict',     // never sent on cross-site requests
'encrypt' => true,           // session payload encrypted at rest
  • What not to store: large objects (sessions are read on every request), passwords, card numbers.
  • Store ids and look data up.
  • Sessions vs tokens: browser apps use sessions; mobile apps and third-party API clients use tokens (Sanctum/Passport/JWT) because they do not manage cookies the same way.
  • Interview answer: "Sessions keep per-user state across stateless HTTP using a session id cookie.
  • I regenerate on login, invalidate on logout, keep payloads small and lock cookies down with secure, HttpOnly and SameSite."
Practice explaining this
Question 22 of 25

Q47. What are Laravel session drivers and which should you use?

Solution and explanation

The driver decides where session data lives (SESSION_DRIVER in .env):

file        storage/framework/sessions; default, fine for one server
cookie      whole payload in an encrypted cookie; no server storage, 4 KB limit
database    sessions table; shared across servers, easy to inspect
redis       in-memory, very fast, shared; the usual production choice at scale
memcached   in-memory, shared
array       tests only, not persisted

Why it matters when you scale out:

Load balancer -> Server 1 (file session for Abc lives here)
              -> Server 2 (no file for Abc)  => user appears logged out

Fixes: a shared store (Redis or database) or sticky sessions on the load balancer. Shared storage is preferred because sticky sessions break when a server is removed.

Setup examples:

php artisan session:table && php artisan migrate     # database driver
SESSION_DRIVER=redis                                  # with REDIS_HOST configured

Choosing for real systems:

  • Small site on one server or shared hosting: file.
  • Several app servers: redis (fast) or database (simple).
  • Banking: Redis or database with short lifetimes, encryption on, and the ability to delete a user's sessions on password change or suspicious activity. With the database driver you can do DB::table('sessions')->where('user_id', $id)->delete().

Also note: queue workers and scheduled commands have no session; never rely on session() inside jobs.

Interview answer: "file for a single server, redis or database when load-balanced so every server sees the same session, plus short lifetimes and the ability to revoke sessions for sensitive apps."

Practice explaining this
Question 23 of 25

Q48. How does exception handling work in Laravel?

Solution and explanation

  • Every uncaught exception goes to one central handler (app/Exceptions/Handler.php in Laravel 9/10, withExceptions() in bootstrap/app.php in 11+).
  • It decides what to log (report) and what to return (render), so controllers do not need try/catch everywhere.
  • Custom domain exceptions make failures explicit:
class InsufficientBalanceException extends RuntimeException
{
    public function __construct(public readonly Account $account, public readonly Money $requested)
    {
        parent::__construct('Insufficient balance for this transfer.');
    }

    public function render(Request $request)     // Laravel calls this automatically
    {
        return response()->json(['success' => false, 'message' => $this->getMessage(), 'code' => 'INSUFFICIENT_BALANCE'], 422);
    }

    public function report(): bool               // expected business case: do not alert on-call
    {
        return false;
    }
}

// Service
if ($from->balance->lt($amount)) throw new InsufficientBalanceException($from, $amount);
  • Central rules (Laravel 9/10 Handler::register):
$this->renderable(function (ModelNotFoundException $e, Request $request) {
    if ($request->expectsJson()) return response()->json(['success' => false, 'message' => 'Not found.'], 404);
});

$this->reportable(function (PaymentGatewayException $e) {
    Log::channel('payments')->error($e->getMessage(), ['order' => $e->orderId]);
});
  • When to catch locally: only when you can do something useful, such as retrying, falling back, or converting a third-party error into a domain exception.
try {
    $charge = $gateway->charge($order);
} catch (ConnectionException $e) {
    throw new PaymentGatewayUnavailableException(previous: $e);   // keep the original as context
}
  • Production rules: APP_DEBUG=false so users never see stack traces, SQL, file paths or secrets; log with context (user id, order id, request id); send errors to a tracker (Sentry, Bugsnag); return a reference id the customer can quote to support.
  • Interview answer: "One central handler reports and renders exceptions.
  • I use domain exceptions with render/report methods, catch locally only to recover or translate, keep debug off in production and log with context."
Practice explaining this
Question 24 of 25

Q49. How do you return API errors consistently?

Solution and explanation

  • Clients (mobile apps, partners) should be able to handle every endpoint the same way.
  • Pick one envelope and one set of status codes.
Success                                  Failure
{                                        {
  "success": true,                         "success": false,
  "message": "Order created",              "message": "Validation failed",
  "data": { "id": 187, "status": "placed" }, "code": "VALIDATION_FAILED",
  "meta": { "request_id": "c1f..." }       "errors": { "email": ["The email field is required."] }
}                                        }
  • Status codes that must match the body:
200 OK            read or update succeeded
201 Created       POST created a resource (with Location header)
204 No Content    delete succeeded, no body
400 Bad Request   malformed request
401 Unauthorized  not authenticated
403 Forbidden     authenticated, not allowed
404 Not Found     resource missing or hidden
409 Conflict      duplicate or state conflict (order already paid)
422 Unprocessable validation failed
429 Too Many      rate limited (with Retry-After)
500 Server Error  unexpected failure; generic message only
503 Unavailable   maintenance or upstream gateway down
  • Implementation: one place builds responses, and the exception handler converts all exceptions to the same shape.
trait ApiResponses
{
    protected function ok($data, string $message = 'OK', int $status = 200)
    {
        return response()->json(['success' => true, 'message' => $message, 'data' => $data], $status);
    }
}

// Handler: validation, auth, not found, throttle, everything else
$this->renderable(fn (ValidationException $e, $r) => $r->expectsJson()
    ? response()->json(['success' => false, 'message' => 'Validation failed', 'code' => 'VALIDATION_FAILED', 'errors' => $e->errors()], 422) : null);
$this->renderable(fn (Throwable $e, $r) => $r->is('api/*') && ! config('app.debug')
    ? response()->json(['success' => false, 'message' => 'Something went wrong', 'code' => 'SERVER_ERROR'], 500) : null);
  • Use API Resources (php artisan make:resource OrderResource) so field names and formats are stable even when the database changes.
  • Payment API tip: include a machine-readable code (CARD_DECLINED, INSUFFICIENT_FUNDS, GATEWAY_TIMEOUT).
  • Apps branch on codes, not on English messages that may be translated or reworded.
  • Interview answer: "One JSON envelope, correct status codes, stable error codes, API Resources for data, and the exception handler converting every error so no endpoint invents its own format."
Practice explaining this
Question 25 of 25

Q50. Design a secure Laravel login API.

Solution and explanation

Goal: POST /api/login returns a token for a mobile or SPA client, resists brute force and does not leak which emails exist.

Step 1, validation and rate limiting:

// RouteServiceProvider / bootstrap: 5 attempts per minute per email+IP
RateLimiter::for('login', fn (Request $r) => Limit::perMinute(5)->by(Str::lower($r->input('email')) . '|' . $r->ip()));

Route::post('/login', LoginController::class)->middleware('throttle:login');

$data = $request->validate(['email' => ['required', 'email'], 'password' => ['required', 'string'], 'device_name' => ['required', 'string', 'max:100']]);

Step 2, verify the password with the hash (bcrypt/argon2), and give the same error for a wrong email or a wrong password:

$user = User::where('email', Str::lower($data['email']))->first();

if (! $user || ! Hash::check($data['password'], $user->password)) {
    throw ValidationException::withMessages(['email' => 'These credentials do not match our records.']);
}

if (! $user->is_active) abort(403, 'This account is disabled.');

Step 3, issue a token with limited abilities and an expiry (Sanctum):

$token = $user->createToken($data['device_name'], ['orders:read', 'orders:write'], now()->addDays(30))->plainTextToken;

return response()->json(['success' => true, 'token' => $token, 'token_type' => 'Bearer',
    'user' => new UserResource($user)]);

Sanctum stores only a hash of the token, so a database leak does not expose usable tokens.

Step 4, protect routes and check abilities:

Route::middleware('auth:sanctum')->group(function () {
    Route::get('/profile', ProfileController::class);
    Route::post('/orders', StoreOrderController::class)->middleware('ability:orders:write');
    Route::post('/logout', fn (Request $r) => tap(response()->noContent(), fn () => $r->user()->currentAccessToken()->delete()));
});

Step 5, production hardening:

  • HTTPS only; never log passwords or tokens (redact them in logging).
  • Rehash passwords when the hashing cost increases (Hash::needsRehash).
  • Two-factor or OTP for sensitive apps; for banking, require step-up OTP again for transfers even with a valid token.
  • Lock or slow down accounts after repeated failures and notify the user of new device logins.
  • Revoke all tokens on password change: $user->tokens()->delete().
  • Store tokens on mobile in secure storage (Keychain/Keystore), not plain preferences.

Flow:

Client -> POST /api/login (throttled) -> validate -> Hash::check -> createToken(abilities, expiry)
Client -> Authorization: Bearer <token> -> auth:sanctum -> ability check -> policy -> controller

Interview answer: "Validate and throttle, verify hashed passwords with a generic error, issue scoped expiring Sanctum tokens, protect routes with auth:sanctum and abilities, revoke tokens on logout and password change, and add OTP for high-risk actions."

Practice explaining this

Showing questions 21–25 of 25