Framework Core
What Laravel and Symfony actually do for you — MVC, the service container and autowiring, middleware and events, routing and controllers, Blade and Twig templating, validation, authorization policies and voters, and API authentication.
23 questions
JuniorTheoryVery commonWhat is the difference between authentication and authorization in a web application?
What is the difference between authentication and authorization in a web application?
Authentication answers who are you: it verifies a credential and establishes the current user. Authorization answers may you do this: it checks that the already-identified user is permitted to perform this action on this resource. Authentication comes first — being logged in does not make you allowed.
Common mistakes
- ✗Swapping the two terms, calling the login step authorization
- ✗Treating a logged-in user as permitted, and checking only that a session exists
- ✗Enforcing permissions in the UI by hiding buttons, with no check on the server
Follow-up questions
- →Where in the request pipeline would you enforce each of the two checks, and in what order?
- →Why is an ownership check — is this the user's own record — an authorization concern?
JuniorTheoryVery commonWhat is HTTP middleware, and where does it sit in the framework's request pipeline?
What is HTTP middleware, and where does it sit in the framework's request pipeline?
Middleware is a layer that wraps the request-to-response cycle. Each one receives the request, may act on it, calls $next($request) to pass it further down the chain, and can then inspect or alter the response on the way back. It sits between the kernel and the controller — auth, CORS and throttling live there.
Common mistakes
- ✗Thinking middleware only post-processes the response and never sees the request
- ✗Forgetting to call
$next($request), which silently swallows the request - ✗Not realising a middleware can short-circuit the chain by returning a response itself
Follow-up questions
- →How does a middleware reject a request early without ever reaching the controller?
- →Why does the registration order of two middleware change the behaviour of the response?
JuniorTheoryVery commonHow do Laravel and Symfony realise the architectural pattern MVC on a request?
How do Laravel and Symfony realise the architectural pattern MVC on a request?
The router maps the URL to a controller action. The controller coordinates: it pulls data through models — Eloquent models or Doctrine entities — and hands the result to a view (Blade or Twig) to render. The controller should stay thin; business rules belong in a model or a service, not in the action.
Common mistakes
- ✗Letting the view query models directly instead of receiving data the controller prepared
- ✗Putting business rules in the controller action, so it grows into a god object
- ✗Thinking
MVCdescribes framework internals rather than how you organise your own code
Follow-up questions
- →Where do you put logic that is neither persistence nor presentation — a service, or the model?
- →What does a fat controller cost you the moment the same action must run from a queue job?
JuniorTheoryVery commonHow does a resource controller map HTTP verbs onto the CRUD (create-read-update-delete) actions?
How does a resource controller map HTTP verbs onto the CRUD (create-read-update-delete) actions?
One declaration generates a fixed set of named routes against one controller: GET /posts calls index, POST /posts calls store, GET /posts/{id} calls show, PUT or PATCH /posts/{id} calls update, and DELETE /posts/{id} calls destroy. The verb plus the path selects the action.
Common mistakes
- ✗Encoding the action in the path (
/posts/delete/5) instead of using the HTTP verb - ✗Not knowing which method name each verb maps to, so routes silently 404
- ✗Exposing all seven routes when an API needs only five —
createandeditare form pages
Follow-up questions
- →Why does an API resource route omit
createandedit, and how do you exclude them? - →How does route-model binding turn
{id}into an already-loaded model in the action?
JuniorTheoryVery commonWhat does a framework's service container do when a controller type-hints a dependency?
What does a framework's service container do when a controller type-hints a dependency?
It resolves and builds it for you. The container reads the constructor's type hints, recursively instantiates each dependency, and injects them, so you never new a collaborator by hand. Bindings tell it which concrete class to supply for an interface, and a singleton binding reuses one instance.
Common mistakes
- ✗Thinking the container needs a string key when the type hint alone is enough to resolve
- ✗Not binding an interface to a concrete class and then wondering why resolution fails
- ✗Confusing a singleton binding with a PHP static — the container scopes it per request
Follow-up questions
- →Why does a singleton in the container not survive to the next request under php-fpm?
- →How would you inject a scalar such as an API key, which the container cannot infer by type?
JuniorTheoryVery commonHow does session authentication differ from a signed token JWT (JSON Web Token) for an API?
How does session authentication differ from a signed token JWT (JSON Web Token) for an API?
A session keeps state on the server: the cookie carries only an id and the server looks the session up, so revoking access is instant, but it needs shared storage and CSRF protection. A JWT is self-contained and stateless — the server just verifies a signature, which scales, but you cannot revoke it before it expires.
Common mistakes
- ✗Thinking a
JWTpayload is encrypted — it is only signed, and anyone can decode it - ✗Assuming a
JWTcan be revoked, when nothing but expiry or a deny-list stops it - ✗Forgetting that a cookie-based session is what makes CSRF protection necessary
Follow-up questions
- →How do you approximate revocation for a
JWTwithout giving up statelessness entirely? - →Why does storing a token in
localStoragetrade a CSRF risk for an XSS one?
JuniorTheoryCommonHow do the template engines Blade and Twig differ in what they let a view do?
How do the template engines Blade and Twig differ in what they let a view do?
Blade compiles down to plain PHP, so a template may call any PHP it likes — convenient and easy to abuse. Twig has its own sandboxed syntax with no raw PHP at all and escapes output by default. Blade escapes with {{ }} but {!! !!} opts out. Twig enforces the discipline; Blade trusts you.
Common mistakes
- ✗Believing
Bladesandboxes the template — a compiledBladeview is plain PHP - ✗Reaching for
{!! !!}to render user content, which reopens an XSS hole - ✗Assuming neither engine escapes by default, so double-escaping every value
Follow-up questions
- →Why does a designer-facing project often argue for the sandboxed template engine
Twig? - →What kind of logic should never appear in a template, whichever engine you use?
JuniorTheoryCommonHow does Symfony bind a URL to a controller action through the #[Route] attribute?
How does Symfony bind a URL to a controller action through the #[Route] attribute?
You annotate the action: #[Route('/posts/{id}', name: 'post_show', methods: ['GET'])]. Symfony reads those attributes by reflection while warming the cache and compiles them into a route table, so matching at runtime is a lookup rather than a scan. Placeholders like {id} arrive as action arguments.
Common mistakes
- ✗Thinking attributes are parsed per request rather than compiled into a cached route table
- ✗Forgetting
methods:, so one action answers every verb includingDELETE - ✗Not naming the route, then hardcoding URLs instead of generating them from the name
Follow-up questions
- →Why must you clear the route cache after adding an attribute, and what does the warm-up build?
- →How do you constrain
{id}to digits, and what happens to a request that fails the constraint?
JuniorTheoryCommonHow does the Symfony validator check an object declared with constraint attributes?
How does the Symfony validator check an object declared with constraint attributes?
You declare the rules on the properties — #[Assert\NotBlank], #[Assert\Email] — and call $validator->validate($dto). It walks that metadata by reflection, runs each constraint, and returns a ConstraintViolationList. The object stays a plain DTO; the rules travel with it rather than with the controller.
Common mistakes
- ✗Expecting constraints to fire on assignment rather than on an explicit
validate()call - ✗Assuming the first violation aborts —
validate()collects every violation into a list - ✗Validating the entity instead of a request DTO, so bad input reaches the domain object
Follow-up questions
- →How do validation groups let one DTO carry different rules for create and update?
- →Where would you enforce a rule that needs a database lookup, such as email uniqueness?
MiddleTheoryCommonWhy does autowiring fail when one interface has two implementations, and how do you fix it?
Why does autowiring fail when one interface has two implementations, and how do you fix it?
The container resolves by type. With two classes behind PaymentGatewayInterface it cannot choose, so it throws at compile time — loudly, never silently picking one. The fix is an explicit binding: a Symfony alias or Laravel's contextual binding.
Common mistakes
- ✗Believing the container silently picks one implementation instead of throwing
- ✗Type-hinting the concrete class everywhere instead of binding the interface once
- ✗Forgetting that two consumers may legitimately need different implementations
Follow-up questions
- →Two controllers need different gateways — how does Laravel's contextual binding express that?
- →When would you inject an iterable of every implementation instead of binding just one?
MiddleTheoryCommonHow do Laravel events and listeners decouple side effects from the triggering action?
How do Laravel events and listeners decouple side effects from the triggering action?
The action dispatches a domain event such as OrderPlaced and knows nothing about who reacts. Each side effect is its own listener, testable alone, and can implement ShouldQueue to run outside the request. The cost: control flow becomes implicit.
Common mistakes
- ✗Keeping side effects inline in the controller, so every new one edits the action again
- ✗Assuming a listener is asynchronous by default — only
ShouldQueuemoves it off the request - ✗Expecting a queued listener's failure to roll back the action that dispatched the event
Follow-up questions
- →What does the queue-marker interface
ShouldQueuechange about a listener's failure handling? - →How do you keep an event's listeners discoverable once the controller no longer names them?
MiddleTheoryCommonWhat does a Laravel form request do before the controller action body runs?
What does a Laravel form request do before the controller action body runs?
Type-hint a FormRequest and the container resolves it: before the body runs it calls authorize(), then rules(). Failure throws ValidationException — 422 for an API, a redirect back for a web form. The body sees only $request->validated().
Common mistakes
- ✗Reading
$request->all()in the action instead ofvalidated(), so unvalidated keys slip through - ✗Treating
authorize()as an authentication check rather than a permission check on this request - ✗Expecting
rules()to hand the action an error list, when a failure throwsValidationException
Follow-up questions
- →How does the failure response differ between an API client and a browser form, and why?
- →How would you keep one
FormRequestusable for create and update, whose rules differ?
MiddleTheoryCommonWhat is a Laravel facade under the hood, and what is the honest criticism of it?
What is a Laravel facade under the hood, and what is the honest criticism of it?
Not a static class: Cache::get() hits __callStatic, which pulls the real object from the container and forwards the call. The criticism is that it hides dependencies: the constructor no longer declares what the class needs, so tests static-mock.
Common mistakes
- ✗Thinking a facade is a real static class rather than a
__callStaticproxy over the container - ✗Reading a constructor to learn a class's collaborators when facades keep them hidden
- ✗Calling facades testable while the test still static-mocks instead of injecting a double
Follow-up questions
- →When is a facade acceptable, and when does the hidden coupling justify constructor injection?
- →How does
Cache::shouldReceive()differ from injecting a test double of the contract?
MiddleTheoryCommonHow does a Laravel policy decide whether a user may act on a specific model instance?
How does a Laravel policy decide whether a user may act on a specific model instance?
A policy is bound to one model; its methods are abilities of signature (User $user, Post $post): bool. Call $this->authorize('update', $post) or the can: middleware; denial throws AuthorizationException → 403. The instance proves ownership.
Common mistakes
- ✗Checking only the role, when the policy method also receives the model instance to test ownership
- ✗Forgetting that
authorize()throws rather than returning false — on denial the action never runs - ✗Assuming registering a policy guards routes by itself, with no
authorize()call orcan:middleware
Follow-up questions
- →What does the policy short-circuit hook
before()change, and how can it over-grant? - →How would you check an ability that has no model instance yet, such as creating a post?
MiddleTheoryCommonWhat is a Laravel service provider, and how does register() differ from boot()?
What is a Laravel service provider, and how does register() differ from boot()?
A provider is the bootstrap unit. register() may only bind into the container — never resolve a service there, as other providers may not have registered yet. boot() runs after all have, so it adds routes and listeners. Deferred ones load lazily.
Common mistakes
- ✗Resolving another service inside
register(), before its own provider has registered - ✗Registering routes or event listeners in
register()instead of inboot() - ✗Thinking a deferred provider loads on every request rather than on first resolution
Follow-up questions
- →Why must a deferred provider declare
provides(), and what breaks when that list is wrong? - →How do you order two providers when one's
boot()depends on the other's binding?
MiddleTheoryCommonHow does Laravel middleware differ from a Symfony kernel event listener?
How does Laravel middleware differ from a Symfony kernel event listener?
Middleware is a decorator pipeline: each layer gets the request plus $next, runs before and after it, and short-circuits by not calling $next, so order is semantic. A kernel listener instead subscribes to lifecycle events and sets a response.
Common mistakes
- ✗Treating middleware order as cosmetic when its position in the stack is semantic
- ✗Expecting a kernel listener to wrap the next layer rather than be notified at a point
- ✗Not knowing a listener short-circuits by setting a response on the event object
Follow-up questions
- →Which kernel events fire —
kernel.request,kernel.response,kernel.exception— and in what order? - →How does a terminable middleware do its work after the response has already been sent?
MiddleTheoryCommonWhen would you choose the Laravel API-auth package Sanctum over its sibling Passport?
When would you choose the Laravel API-auth package Sanctum over its sibling Passport?
Sanctum is lightweight: opaque tokens in the database, carrying abilities checked at authorization time, plus a cookie mode for a same-domain SPA. Passport is a full OAuth2 server. Your own SPA or app takes Sanctum; third-party clients take Passport.
Common mistakes
- ✗Reaching for Passport's OAuth2 machinery when only a first-party SPA needs to sign in
- ✗Thinking Sanctum's tokens are
JWTs — they are opaque strings looked up in the database - ✗Issuing every token with full access and never checking its abilities on the action
Follow-up questions
- →What do a token's abilities give you that a role check on the user alone cannot?
- →Why does Sanctum's cookie mode for a same-domain SPA still need CSRF protection?
MiddleTheoryCommonHow does the Symfony service container autowire a controller's constructor?
How does the Symfony service container autowire a controller's constructor?
It resolves each constructor parameter by its type-hint using reflection, at container compile time — autowire: true in services.yaml. That yields a compiled PHP container class, not a per-request lookup, so wiring errors appear at cache warm-up.
Common mistakes
- ✗Thinking the container re-reflects on every request instead of being compiled once
- ✗Expecting a wiring mistake to show up on live traffic rather than at cache warm-up
- ✗Believing the parameter name, not its type-hint, is what the container matches on
Follow-up questions
- →How do you inject a scalar such as an API key that autowiring cannot infer by type?
- →What can the PHP attribute
#[Autowire]override on one parameter thatservices.yamlcannot?
MiddleTheoryCommonHow does a Symfony voter differ from a Laravel policy in an authorization check?
How does a Symfony voter differ from a Laravel policy in an authorization check?
A policy is bound to one model class: its methods are abilities and exactly one answers. A voter is not — it declares supports($attribute, $subject), and the access decision manager polls every voter, combining their votes under a strategy.
Common mistakes
- ✗Expecting one voter to be selected the way a policy is — the manager polls every registered voter
- ✗Ignoring the decision strategy: under the default
affirmative, one granting voter is enough - ✗Assuming a voter cannot see the subject and so deciding from the user's roles alone
Follow-up questions
- →When does the decision strategy
unanimousmake sense, and what breaks if you adopt it late? - →How would you compose two voters that disagree about the same attribute on one subject?
SeniorDesignCommonA large Laravel/Symfony application answers errors in every shape imaginable. Some endpoints return HTTP 200 with an {"error": ...} body, some leak a full HTML stack trace, validation failures arrive in one JSON shape and authentication failures in another, and every client has grown its own brittle parser for each. Design a single error contract for the whole application. It must cover validation failures (422), authentication and authorization failures (401/403), missing resources (404) and unexpected internal errors (500); it must never expose internals such as stack traces, SQL or file paths in production; and it must be enforced in exactly one place rather than by asking every developer to remember a convention. Say where that place is, what the payload looks like, and how a new domain exception gets its status code without any controller learning about it.
A large Laravel/Symfony application answers errors in every shape imaginable. Some endpoints return HTTP 200 with an {"error": ...} body, some leak a full HTML stack trace, validation failures arrive in one JSON shape and authentication failures in another, and every client has grown its own brittle parser for each. Design a single error contract for the whole application. It must cover validation failures (422), authentication and authorization failures (401/403), missing resources (404) and unexpected internal errors (500); it must never expose internals such as stack traces, SQL or file paths in production; and it must be enforced in exactly one place rather than by asking every developer to remember a convention. Say where that place is, what the payload looks like, and how a new domain exception gets its status code without any controller learning about it.
Render errors in ONE place — the exception handler — mapping each domain exception to a status code. Emit one envelope (application/problem+json): validation and auth differ in fields, not shape. The status carries the outcome, not a 200. Stack traces go to the log.
Common mistakes
- ✗Returning HTTP
200with an error object, forcing every client to parse the body to learn it failed - ✗Formatting errors inside each controller, so the shape drifts and nothing can enforce it
- ✗Letting a stack trace reach the production payload instead of the log
Follow-up questions
- →How does a domain exception written today get a
409without any controller learning about it? - →What breaks for clients when you add a field to the envelope, and what breaks when you rename one?
SeniorDesignCommonYou maintain a public REST API used by third-party integrators whose code you cannot see and cannot deploy. A breaking change is due on the order resource: total stops being a plain number and becomes a money object, and two fields disappear. Three options are on the table: URI versioning (/v1/orders, /v2/orders), media-type versioning through the Accept header (application/vnd.acme.v2+json), and a ?version=2 query parameter. Choose one and defend it. Address how you avoid duplicating every controller and route across versions, how long v1 stays supported and on what evidence that is decided, how you find out which integrators still call v1 at all, and how you stop the two versions from forking the whole application into two codebases you have to patch twice.
You maintain a public REST API used by third-party integrators whose code you cannot see and cannot deploy. A breaking change is due on the order resource: total stops being a plain number and becomes a money object, and two fields disappear. Three options are on the table: URI versioning (/v1/orders, /v2/orders), media-type versioning through the Accept header (application/vnd.acme.v2+json), and a ?version=2 query parameter. Choose one and defend it. Address how you avoid duplicating every controller and route across versions, how long v1 stays supported and on what evidence that is decided, how you find out which integrators still call v1 at all, and how you stop the two versions from forking the whole application into two codebases you have to patch twice.
URI versioning is the pragmatic default: visible, cacheable, routable. Media-type versioning is purer but hostile to clients and caches. Version the contract, not the codebase: one business layer, only the serializer varies. Per-client tokens show who still calls v1.
Common mistakes
- ✗Forking the whole application into
v1andv2trees instead of varying only the serializer - ✗Picking media-type versioning for a public API whose clients, caches and browsers cannot use it
- ✗Announcing a sunset date with no per-client evidence of who still calls
v1
Follow-up questions
- →How do you keep one controller serving both versions without
if ($version === 2)branches inside it? - →What does a version number tell a client that a purely additive change would never have needed?
SeniorDesignOccasionalA heavily-used public endpoint has to be retired. Dozens of third-party integrations still call it, you cannot see their code, you cannot deploy on their behalf, and support has no list of who they even are. The constraints are firm: no integration may break without warning; you need evidence of who is still calling the endpoint before you are allowed to remove it; and the team wants a hard removal date rather than carrying the endpoint forever. Design the rollout end to end. Cover what you send back to callers while the endpoint still works, how you discover who the remaining callers actually are, how you get their attention when a changelog entry is plainly not enough, and what you do in the final weeks to flush out integrations that never read anything you publish.
A heavily-used public endpoint has to be retired. Dozens of third-party integrations still call it, you cannot see their code, you cannot deploy on their behalf, and support has no list of who they even are. The constraints are firm: no integration may break without warning; you need evidence of who is still calling the endpoint before you are allowed to remove it; and the team wants a hard removal date rather than carrying the endpoint forever. Design the rollout end to end. Cover what you send back to callers while the endpoint still works, how you discover who the remaining callers actually are, how you get their attention when a changelog entry is plainly not enough, and what you do in the final weeks to flush out integrations that never read anything you publish.
Announce in band, measure, then remove. Emit Deprecation and Sunset response headers so the deadline travels with every response. Middleware logs the calling client id on each hit, giving a per-client usage curve. Before the cut, run a short brownout of 410s.
Common mistakes
- ✗Changing the old endpoint's behaviour in place, which breaks callers with no signal at all
- ✗Announcing a removal date with no per-client usage data on who would actually break
- ✗Trusting a changelog to reach integrations that never read it, and skipping the brownout
Follow-up questions
- →What does a
Sunsetresponse header give a client that the same date in a changelog does not? - →How long should a brownout run, and how do you tell a broken integration from an abandoned one?
SeniorDesignOccasionalTwo internal services must integrate, and the team is split between REST, the query language GraphQL and an asynchronous message queue such as RabbitMQ. There are two distinct paths. On the first, a service renders a page and needs the customer's current balance right now, with a fixed and well-known set of fields, before it can answer the browser. On the second, roughly 50,000 user profile changed notifications a day must reach a downstream indexing service: the caller has no use for a reply, the consumer is regularly taken down for maintenance windows, and not a single event may be lost. Choose a mechanism for each path and defend the choice on coupling and failure semantics rather than on preference or on what the team enjoys writing. State explicitly what happens on each path when the callee is unavailable.
Two internal services must integrate, and the team is split between REST, the query language GraphQL and an asynchronous message queue such as RabbitMQ. There are two distinct paths. On the first, a service renders a page and needs the customer's current balance right now, with a fixed and well-known set of fields, before it can answer the browser. On the second, roughly 50,000 user profile changed notifications a day must reach a downstream indexing service: the caller has no use for a reply, the consumer is regularly taken down for maintenance windows, and not a single event may be lost. Choose a mechanism for each path and defend the choice on coupling and failure semantics rather than on preference or on what the team enjoys writing. State explicitly what happens on each path when the callee is unavailable.
A synchronous read on a fixed contract is REST. The 50k no-reply notifications go on a queue: the producer must not care the consumer is down; only a broker gives durability and retries. GraphQL pays only for many shapes of one graph, costing caching and field auth.
Common mistakes
- ✗Choosing one mechanism for the whole integration when the two paths have different failure semantics
- ✗Believing a caller-side retry loop gives the same durability as a broker when the consumer is down
- ✗Reaching for
GraphQLby default and paying for it with caching and per-field authorization
Follow-up questions
- →Who is responsible for a duplicate notification if the broker only guarantees at-least-once delivery?
- →What would have to become true about the page-render path before a queue made sense there?