Course outline

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.

About 5 min readContent reviewed
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 Local controller invocation with a known 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;
Expected output
{"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 A response object without network emission
<?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;
Expected output
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.

Official references