Phase 5 · Professional ApplicationsModule 28~52 min read

Routing, Controllers & MVC

Build a small framework-style architecture with routing, middleware, controllers, services, views, and dependency injection.

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.

ConceptWhat it protectsDecision rule
RouterRequest-to-handler selectionMatch method and normalized path, then extract parameters.
ControllerHTTP translationMap request to use case and result to response.
MiddlewareCross-cutting request policyUse 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.

  1. Describe the HTTP application boundary boundary: its inputs, outputs, invariants, and expected failures.
  2. Implement the smallest happy path behind an explicit contract.
  3. Add validation and translate low-level failures into language the caller understands.
  4. Exercise the boundary with realistic data, then inspect output, logs, and resource cleanup.
  5. Refactor only after behavior is protected by a repeatable check.

Make the boundary visible

Name inputs, outputs, side effects, and failure cases before adding framework or infrastructure code. That habit keeps advanced PHP understandable as the application grows.

Guided code lab

Register explicit routes

Routes are data, so dispatch stays independent of the controller implementations.

routes.php
<?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.

CourseController.php
<?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

A controller that validates business rules, runs SQL, renders HTML, and sends email cannot be tested or reused without the entire web stack.

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

Run the happy path and at least two failure paths, explain one design tradeoff in a short README, and leave the code formatted and ready for review.

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.