What you'll learn
Build the essential architecture beneath a framework: a front controller, router, middleware pipeline, thin controllers, use-case services, and views. Each layer translates one concern instead of accumulating every concern in index.php.
By the end of this lesson, you'll be able to:
- Route requests by method and path
- Keep controllers focused on HTTP translation
- Compose middleware and dependencies at the application edge
Core mental model
Professional PHP is less about memorizing APIs and more about choosing a clear boundary for each responsibility. Use this table as a decision guide while reading the examples.
| Concept | What it protects | Decision rule |
|---|---|---|
| Router | Request-to-handler selection | Match method and normalized path, then extract parameters. |
| Controller | HTTP translation | Map request to use case and result to response. |
| Middleware | Cross-cutting request policy | Use for concerns that wrap many handlers. |
Professional workflow
Build the feature in small, verifiable steps. Each step leaves the system in a state you can test.
- Describe the HTTP application boundary boundary: its inputs, outputs, invariants, and expected failures.
- Implement the smallest happy path behind an explicit contract.
- Add validation and translate low-level failures into language the caller understands.
- Exercise the boundary with realistic data, then inspect output, logs, and resource cleanup.
- Refactor only after behavior is protected by a repeatable check.
Make the boundary visible
Guided code lab
Register explicit routes
Routes are data, so dispatch stays independent of the controller implementations.
<?php
$router->get('/courses', [CourseController::class, 'index']);
$router->get('/courses/{id}', [CourseController::class, 'show']);
$router->post('/courses', [CourseController::class, 'store']);
$response = $router->dispatch(
$_SERVER['REQUEST_METHOD'],
parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH),
);Keep the controller thin
The controller reads transport input, invokes a use case, and constructs a response—business rules remain elsewhere.
<?php
final readonly class CourseController
{
public function __construct(private ListCourses $listCourses) {}
public function index(Request $request): Response
{
$query = new CourseQuery(
search: $request->query('search', ''),
page: max(1, $request->integer('page', 1)),
);
return Response::view('courses/index', [
'page' => $this->listCourses->handle($query),
]);
}
}Production practice
Contract
Requests and responses belong to delivery code; use cases receive transport-neutral input objects.
Verification
Unit-test route matching and use cases, then feature-test the complete request pipeline.
Operations
Assign request IDs at the first middleware and log status, duration, and route name at the last.
Common failure mode
Independent workshop
Build a tiny framework kernel supporting route parameters, dependency injection, middleware, controllers, and template responses.
Your finished workshop must include:
- 404 and 405 behavior
- Authentication and timing middleware
- Three feature-tested course routes
Definition of done
Recap & quick check
Key takeaways
- The front controller creates one application entry point.
- Routers select handlers; controllers translate HTTP.
- Use cases hold application behavior.
- Middleware wraps cross-cutting request concerns.
Quick check
1. What is a controller's primary job?
2. Where does authentication fit across many routes?
3. What does 405 mean?
Keep the workshop: later phases deliberately build on these boundaries, so today's small example can become part of your portfolio architecture.