Laravel / Basic: request and data foundations / Lesson 3
Controllers, HTTP input, and response ownership
Keep controllers focused on translating a request into an authorized use case and a coherent response. Make domain dependencies testable without loading real routes or services.
On this page
Lesson goals
- Separate transport parsing from domain calculation.
- Use dependency injection and response objects.
- State what local controller invocation does not test.
Coordinate a use case rather than owning every rule
A controller receives framework input, invokes validation and authorization at the appropriate boundaries, coordinates application behavior, and returns a response. It should not become the only place where pricing, persistence, rendering, and retries are defined. Small services can express domain rules with explicit parameters and outcomes. Constructor or method injection makes dependencies visible, while request helpers remain transport conveniences. Avoid reading global request state inside a reusable calculation. A controller with few lines can still be unsafe if it mass assigns unvalidated fields or assumes a route-bound object is authorized.
Request convenience does not replace a schema
Laravel Request exposes query parameters, form input, JSON input, headers, files, and content negotiation through documented methods. A merged input bag can be convenient, but the source and allowed shape still matter. Do not assume every field is a scalar string or that a hidden price is authoritative. Validate required keys, types, ranges, and ownership before effects. The first example deliberately reads known synthetic query data and passes an integer into a pure rule. It is a narrow invocation demonstration, not a recommended cast-based parser for arbitrary request values or a substitute for a Form Request.
<?php
declare(strict_types=1);
require __DIR__ . '/vendor/autoload.php';
final class PriceRule { public function total(int $count): int { return 125 * $count; } }
final class QuoteController {
public function show(Illuminate\Http\Request $request, PriceRule $rule): Illuminate\Http\JsonResponse {
return new Illuminate\Http\JsonResponse(['total_cents' => $rule->total((int) $request->query('count'))]);
}
}
$container = new Illuminate\Container\Container();
$request = Illuminate\Http\Request::create('/quote?count=4', 'GET');
$response = $container->call([new QuoteController(), 'show'], ['request' => $request]);
echo $response->getContent(), PHP_EOL;
{"total_cents":500}
The container resolves PriceRule, but no web middleware, validation pipeline, or server listener runs.
Return a response instead of printing fragments
Response objects carry status, headers, and body together. This allows an adapter and tests to inspect a coherent decision before bytes are sent. Avoid echo in services, which can corrupt JSON or commit output before cookies and redirects are decided. Choose status according to the public contract rather than returning 200 for every serialized error. Do not expose raw exceptions or private storage fields simply because they can be converted to JSON. Explicit field selection and API resources can define a public representation, while exception rendering maps known failures safely at the application boundary.
A redirect is a response decision, not automatic control flow
A redirect response describes where a client should navigate and which status to use. Returning it from the controller ends that action path, while merely constructing or sending a Location value does not justify continuing a write workflow. Validate destinations rather than redirecting to arbitrary input. The local object below exposes its status and destination header without contacting a browser or the destination. Flash messages, cookies, session persistence, and browser navigation are separate integration behaviors. Test those where the application actually uses them, and preserve the distinction between a local response object and an emitted HTTP exchange.
<?php
declare(strict_types=1);
require __DIR__ . '/vendor/autoload.php';
$response = new Illuminate\Http\RedirectResponse('/courses', 303);
echo $response->getStatusCode(), PHP_EOL;
echo $response->headers->get('Location'), PHP_EOL;
303
/courses
The object holds a 303 destination. It does not establish that a browser navigated or session flash data was saved.
Keep actions reviewable under success and failure
Use a consistent action flow: identify the route context, validate input, authorize the operation, call a use case, and translate its outcome. Keep transaction ownership in a service that can protect related writes, and do not let a response formatter unexpectedly commit state. A safe error response must not hide a partial reservation or duplicate payment. Test the intended failure category and final state, not just the response text. At integration level, include wrong methods, missing input, unauthorized actors, and content-negotiation differences. Local method tests are useful but cannot establish the actual route and middleware composition.
Common errors and fixes
A service prints into an API response
- Why it happens
- Data calculation and response emission share one hidden contract.
- How to fix it
- Return domain data and emit only at the HTTP boundary.
- Prevent it next time
- Test helper output and full body parsing.
A cast silently accepts malformed quantity input
- Why it happens
- Transport conversion is mistaken for validation.
- How to fix it
- Validate shape and range before invoking domain rules.
- Prevent it next time
- Include arrays, empty text, and malformed values in request tests.
A local action test is used to claim middleware coverage
- Why it happens
- Direct invocation omits the actual HTTP pipeline.
- How to fix it
- Add an isolated feature test through the intended route.
- Prevent it next time
- Name the tested layer and missing integration assumptions.
Check your understanding
1. Where should an authoritative course price come from?
Show worked solution
Resolve it from trusted server-side domain data after validating the course identity and access context. A hidden request field can be a display hint but cannot set the charge. Pass the authoritative cents to the use case and test that a client-supplied conflicting price cannot change the effect.
2. What extra evidence is needed after the local controller example passes?
Show worked solution
Exercise an isolated route with its validation, authorization, middleware, and exception rendering, then inspect status, headers, body, and effects. The local result proves dependency resolution and known-input calculation only. Do not infer CSRF or session-cookie behavior from that method call.
Key takeaways
- Controllers translate boundaries and coordinate use cases.
- A coherent response object and final-state assertions provide different evidence.