Skip to content

re #224 feat(module): MiddlewareProviderInterface — modules contribute PSR-15 middleware - #225

Merged
tonydspaniard merged 3 commits into
masterfrom
feat/224-middleware-provider
Jun 6, 2026
Merged

re #224 feat(module): MiddlewareProviderInterface — modules contribute PSR-15 middleware#225
tonydspaniard merged 3 commits into
masterfrom
feat/224-middleware-provider

Conversation

@tonydspaniard

Copy link
Copy Markdown
Member

Closes #224.

Goal

Let a module self-register PSR-15 middleware into the HTTP pipeline the same way it already contributes routes, entities, and migrations. Previously the Relay pipeline was hand-assembled in the host's public/index.php, so any feature needing a guard (auth, rate-limit, tenant resolution, an action-aware idempotency check) forced the host to hand-edit the front controller — defeating the self-registering-module value proposition.

What changed

New contractAltair\Module\Contracts\MiddlewareProviderInterface (mirrors RoutesProviderInterface):

/** @return list<array{middleware: class-string<MiddlewareInterface>|MiddlewareInterface, priority: int}> */
public function middleware(): array;

A module opts in by also implementing it. univeros/module gains a psr/http-server-middleware dependency (a stable PSR interface package).

Ordering — integer priority with documented bands. Altair\Http\Support\MiddlewarePriority exposes the framework's own stages as anchors so module middleware can slot relative to them:

Anchor Value Stage
EXCEPTION_HANDLER 0 outermost; throwable → problem+json response
DISPATCHER 500 matches the route, records the action on the request
ACTION 1000 innermost; resolves and runs the matched action

Pre-routing guards slot below DISPATCHER; action-aware guards between DISPATCHER and ACTION. Boundaries are documented: ACTION is terminal on a matched route (priority >= ACTION never runs), and a priority below EXCEPTION_HANDLER runs outside the exception handler (reserved for a deliberate outermost wrapper).

Pickup at bootstrapAltair\Http\Support\ModuleMiddleware mirrors ModuleRoutes: it collects altair.module-tagged modules' middleware(), stable-sorts the merged queue by priority (equal priorities keep input order — base entries first, then registration order, so the result is deterministic), and returns the flat list. entries() exposes the sorted entries so a host can bind a MiddlewareCollection that bin/altair middleware:list reads. The skeleton public/index.php now assembles its base pipeline as prioritised entries, merges module middleware, and hands the queue to Relay with a ContainerResolver (class-string middleware are autowired).

Acceptance criteria

  • MiddlewareProviderInterface under Altair\Module\Contracts, same doc/style as RoutesProviderInterface.
  • A module's middleware is merged into the Relay pipeline at bootstrap, ordered deterministically by priority, resolved via the container.
  • Framework stages (exception handler / dispatcher / action) have documented priorities so module middleware can position relative to them.
  • bin/altair middleware listing (Introspection commands — container, routes, events, middleware, manifests, config #71) shows module-contributed middleware in resolved order (covered by a PipelineInspector test over the merged collection).
  • Docs: extending.md gains a "Middleware — automatic" section; a module can register an action-aware guard without the host editing public/index.php.
  • Tests: a fixture module contributes middleware; order + real Relay execution + determinism asserted.

Test plan

  • composer cs — clean
  • composer stan (PHPStan level 8, no baseline) — no errors
  • composer rector (whole-tree dry-run) — no changes
  • vendor/bin/phpunit tests/Http/Support/ModuleMiddlewareTest.php — 9/9 pass (merge order, sorting, equal-priority determinism, negative-priority outermost boundary, container resolution, real Relay execution order, introspection listing, service-only no-op, entries() ordering)
  • tests/Http tests/Bootstrap tests/Introspection tests/Mcp — 396 pass

Note: 5 MongoSessionHandlerTest errors in the full suite are pre-existing (missing local ext-mongodb) and reproduce on clean master — unrelated to this change.

Notes

  • The module skeleton (module:new) is intentionally left unchanged to avoid shipping a sample middleware; the capability is fully documented in extending.md.
  • bin/altair middleware:list reflects module middleware when the host binds the merged collection as its MiddlewareCollection; introspection is not wired into the default skeleton, so this is documented as the host-side hook.

Antonio Ramirez added 3 commits June 6, 2026 10:44
…e PSR-15 middleware

Let a module self-register PSR-15 middleware into the HTTP pipeline the same
way it already contributes routes, entities, and migrations — closing the one
gap that forced a self-contained auth/rate-limit/idempotency module to make the
host hand-edit public/index.php.

- New contract Altair\Module\Contracts\MiddlewareProviderInterface: each entry
  pairs a middleware (class-string resolved via the container, or an instance)
  with an integer priority. Adds psr/http-server-middleware to univeros/module.
- Altair\Http\Support\MiddlewarePriority: documented anchors for the framework's
  own stages — EXCEPTION_HANDLER (0), DISPATCHER (500), ACTION (1000) — with
  bands so module middleware slot deterministically (pre-routing < DISPATCHER;
  action-aware between DISPATCHER and ACTION). Documents the bounds: ACTION is
  terminal on a matched route (priority >= ACTION never runs); a priority below
  EXCEPTION_HANDLER runs outside the exception handler (outermost wrapper only).
- Altair\Http\Support\ModuleMiddleware mirrors ModuleRoutes: collects tagged
  modules' middleware(), stable-sorts the merged queue by priority (equal
  priorities keep input order — base first, then registration order), and
  returns the flat list. entries() exposes the sorted entries for a host to bind
  a MiddlewareCollection the introspection (middleware:list) reads.
- Skeleton public/index.php assembles its base pipeline as prioritised entries,
  merges module middleware via ModuleMiddleware::collect, and hands the queue to
  Relay with a ContainerResolver so class-string middleware are autowired.
- Tests: merge order, determinism, container resolution, real Relay execution
  order, introspection listing, and the negative-priority outermost boundary.
- Docs: extending.md gains a "Middleware — automatic" section; module.md adds the
  contract to the capability table and pickup notes.
The new MiddlewarePriority/ModuleMiddleware support classes and the updated
univeros/module description change the generated manifests; regenerate so the
Determinism gate (#74) stays byte-stable.
…h CI install

Adding psr/http-server-middleware to univeros/module changed the CI composer
cache key, busting the warm cache and forcing a clean dependency resolution.
That unmasked two pre-existing static-analysis errors the warm cache had been
hiding (neither reproduces on the local macOS dependency tree; both are real on
CI's slim Linux install):

- Scaffold\Emitter\HandlerTestEmitter::emit() computed shortNameOf($messageFqcn)
  and discarded the result (method.resultUnused) — the message short name is
  never used in the generated test body. Remove the dead statement; the emitted
  output is byte-identical.
- Messaging\Configuration\MessengerConfiguration probes for optional Symfony
  Messenger transport-bridge factories via class_exists(); a slim CI install
  resolves some bridges as absent, so PHPStan proves the check false-at-analysis
  (function.impossibleType) even though the runtime check is intentional. Add a
  scoped ignoreErrors entry with a reason, per the project's stan policy.
@tonydspaniard
tonydspaniard merged commit 1c6d4a9 into master Jun 6, 2026
4 checks passed
@tonydspaniard
tonydspaniard deleted the feat/224-middleware-provider branch June 6, 2026 09:05
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.

module: MiddlewareProviderInterface — let modules contribute PSR-15 middleware

1 participant