Skip to content

Resolve route controllers through the application container - #20

Merged
ahamed merged 3 commits into
mainfrom
feature/route-controller-container-resolution
Nov 12, 2026
Merged

ahamed merged 3 commits into
mainfrom
feature/route-controller-container-resolution

Conversation

@ahamed

@ahamed ahamed commented Nov 12, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

Route::make() had its own reflection-based injector that ignored the application container, so controller constructors did not follow container bindings:

  • A container singleton injected into a controller's constructor was a different instance from the same singleton injected into its action method.
  • An interface bound in the container failed with Class "..." does not exist.
  • A service built by a container closure (for example one whose constructor takes an array) failed with "must be a class type".
  • A scalar constructor parameter that has a default (int $limit = 10) failed in the same way.
  • A static process-wide cache (Route::$instances) shared unbound services between unrelated controllers. It cached only classes that have a constructor.

Change

  • Route::make() keeps its signature and now delegates to app()->make(). Controllers and their dependencies follow the same bindings, singletons and defaults as everything else the container resolves.
  • Each dispatch builds the controller fresh. Bind it with app()->singleton(SomeController::class) to share one instance.
  • The protected Route::$instances, cache(), is_cached() and get_cached() are removed. The cache gained little in WordPress, where each request is a new PHP process, and it caused the instance mismatch above.

Compatibility

  • Who is affected: subclasses of Route that used the removed protected cache members. Nothing in the public API changes.
  • Unbound services: a service injected into both a controller's constructor and its action is a separate instance in each. That was already the case before this change, and it matches how the container resolves unbound classes everywhere else. Register the service with singleton() when one shared instance is needed.

Tests

tests/Unit/RouteDiTest.php gains one regression test for each case above:

  • the constructor and the action share a container singleton (through a real REST dispatch)
  • a bound interface resolves
  • a service built by a closure resolves
  • a scalar parameter takes its default
  • a controller bound as a singleton is reused
  • unbound services are not shared between controllers

Each test failed before the change. The circular-dependency test is unchanged and still passes.

  • composer test:unit: 953 tests, 2252 assertions, all passing.
  • composer phpcs: 0 errors. It shows only the 3 line-length warnings already present in TemplateEngine.php and ValidationRule.php.

Docs

  • New "Controller constructor injection" subsection in docs/routing-examples.md.
  • CHANGELOG.md entry under 3.3.2 / Fixed.
  • OpenSpec change route-controller-container-resolution.

🤖 Generated with Claude Code

ahamed and others added 3 commits November 12, 2026 17:03
Route::make() had its own reflection-based injector that ignored the
container, so controller constructors did not follow container bindings:

- A container singleton injected into a controller's constructor was a
  different instance from the same singleton injected into its action.
- Interfaces bound in the container failed with `Class "..." does not exist`.
- Services built by container closures, and scalar parameters that have
  defaults, failed with "must be a class type".
- A static process-wide cache shared unbound services between unrelated
  controllers. It cached only classes that have a constructor.

Route::make() keeps its signature and now delegates to app()->make(). Each
dispatch builds the controller fresh. Bind it with app()->singleton() to
share one instance. The protected Route::$instances, cache(), is_cached()
and get_cached() are removed.

Adds regression tests for each case, a "Controller constructor injection"
section in docs/routing-examples.md, a 3.3.2 changelog entry, and the
OpenSpec change route-controller-container-resolution.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Moves the route-controller-container-resolution change into
openspec/changes/archive/2026-11-12-route-controller-container-resolution/
and adds its requirements as the new main spec in
openspec/specs/route-controller-resolution/spec.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ahamed
ahamed merged commit d80feb7 into main Nov 12, 2026
1 check passed
@ahamed
ahamed deleted the feature/route-controller-container-resolution branch November 12, 2026 11:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant