Application Recipes#
Routing guidance for an AI coding agent: compact routes for tasks that recur across Kinetis applications. Each recipe names the pages that hold the actual contract and routes to them rather than restating it — read those before writing code. Start at Agent Workflow if a task does not match one of these. A human developer wants the linked guides directly — Routing & Validation, Sessions & CSRF, Query Builder, and the rest each recipe below routes through.
JSON HTTP endpoint with request DTO validation#
Guides: Routing & Validation, Appendix: Routing & Validation.
Lifecycle/I-O: the request DTO and its controller resolve fresh from the request scope on every request; nothing about one request’s input persists past it.
Security/integrity: validation attributes run before the controller does, so a malformed request never reaches application code — put every constraint on the DTO, not in checks scattered through the controller.
Verification: a Testing
TestClientrequest for a valid payload and for each violation, asserting the422and its violation list.Non-goal: hand-written validation duplicating what a constraint attribute already expresses.
Constrained or catch-all route#
Guides: Routing & Validation’s “Route constraints”, then the complete rules in Appendix: Routing & Validation’s “Route constraints”.
Lifecycle/I-O: a route’s
wheremap is fixed at registration and compiled once, like the rest of the route; nothing about it varies per request.Security/integrity: a
wherefragment decides whether the route admits a path — a mismatch is a404and no controller runs — while the parameter’s type and validation attributes still decide the value, whose failure is a422. Registration rejects a fragment that is not self-contained or uses(*ACCEPT), so a fragment cannot detach the rest of the route. A constraint never picks between two routes: same method and path shape is a duplicate whatever the fragments say. A catch-all such as.*captures raw text that may hold//or..; validate and canonicalise it before any filesystem use.Verification: a
TestClientrequest the constraint admits, one it rejects asserting the404, and, where the value is also validated, an admitted request asserting the422. The generated document carries the fragment asx-kinetis-route-constrainton the path parameter; assert it there rather than expecting a JSON Schemapattern.Non-goal: using constraints to route one path shape to different handlers, or relying on
/files/{path}to answer/filesitself.
Browser form with session, CSRF, and validation errors#
Guides: Sessions & CSRF, Routing & Validation, Middleware, Views.
Lifecycle/I-O: the session is request-scoped through
SessionMiddleware; see Appendix: Container Lifecycle when a value needs to outlive one request some other way.Security/integrity:
CsrfMiddlewareruns afterSessionMiddlewarein the pipeline; every state-changing form carriescsrfToken(), and a missing or mismatched token is refused before the controller runs. A valid submission with an invalid field gets the default422application/problem+jsonresponse; an application that wants an HTML redirect or a re-rendered form instead must implement and bind its ownValidationExceptionRendererInterface, per Middleware’s “Rendering validation failures”.Verification: a
TestClientsubmission with a missing or wrong token asserting the refusal, one with a valid token and an invalid field asserting the default422problem document, and — only when the application installs a custom renderer — a test of that renderer’s own response instead of assuming automatic form rendering.Non-goal: a CSRF token as the only protection on a state change that also needs its own authorization check.
Database-backed bounded list with pagination#
Guides: Query Builder, Database.
Lifecycle/I-O: the link resolves from the shared connection
kinetis/database-bridgeregisters onAppScope— under a persistent worker, its connection pool — not a request-scoped object; a query still suspends the calling Fiber rather than blocking the worker.Security/integrity: page size and any filter come through the same validated DTO as the rest of the request, and every value binds as a parameter rather than being interpolated into SQL.
Verification: an integration test against a real database asserting the page boundary — the right rows, in order, with a stable cursor across an intervening write.
Non-goal: an unbounded
SELECT, or anOFFSETwalk over a table the application does not control the size of.
Transactional mutation with explicit failure behavior#
Guides: Database — “Transactions”, “When a write’s outcome is unknown”, “Unique violations”.
Lifecycle/I-O: one
TransactionGuardper unit of work; every statement runs on the transaction it hands the callback, never the outer link.Security/integrity: a unique key settles a race instead of a read-then-write check, and a
COMMITthat throws leaves the outcome unknown — never retried as though it were a rollback.Verification: a test that fails a statement mid-transaction and asserts nothing committed, plus one that exercises the unique-violation path.
Non-goal: several related writes left as separate, individually committed statements outside one
transaction()call.
Application with a second database#
Guides: Database — “Options and more connections”; Appendix: Databases — “Registering connections in
bootstrap.php”; ORM — “Entities on other connections”; Query Builder for a named link queried without the ORM.Lifecycle/I-O: the bridge builds, injects and closes the default connection. With the ORM, it also wires every connection an entity names with
#[Entity(connection: ...)]and opens and closes one request-scoped manager per connection throughEntityManagerRegistry; ORM’s “Entities on other connections” states who closes each link. A named link queried without the ORM is explicit application wiring, down to who closes it.Security/integrity: a named connection reads every key under its own name — for
reporting,DB_REPORTING_DRIVERrather thanDB_DRIVER— so nothing it needs comes from the default connection’s settings. Only the default is injected by type: code on the named connection asksEntityManagerRegistryfor its manager, or is passed the named link explicitly. No relationship, flush or transaction spans the two databases.Verification: an integration test against each real backend, including one in which a unit of work on the named connection throws and the next asserts that nothing from it committed or stayed open.
Non-goal: one transaction across both databases. Each connection commits on its own.
Application with more than one search engine or connection#
Guides: Search engines — “More than one engine or connection”; Appendix: Search contracts — “Named connections”, “Why the client is short-lived”, and “OpenSearch client internals”; Bootstrapping; Appendix: Container Lifecycle.
Lifecycle/I-O: construct each named connection with its engine factory and bind it under an application-owned identity in
AppScope. Follow the engine-specific client lifetime in Appendix: Search contracts: the two engines do not have the same sharing contract. Dropping those application bindings whenAppScopeis disposed releases the transports they own.Security/integrity: inject the exact application identity into each repository. Keep its connection name, endpoint, credentials, timeout and index together; an index name is not a connection identity. When both engine packages are installed, do not let their competing
SearchClientbindings or package-bootstrap order select a repository’s engine.Verification: run the package bootstraps in both orders and assert that only the unused default
SearchClientchanges. Against the two real engines, write distinct markers through each repository and assert each marker and index is absent from the other engine. Rejecting an invalid timeout under each named prefix proves the two configuration scopes do not collapse.Non-goal: a connection registry, request-selected endpoint, cross-engine transaction, failover policy or translated query language.
Queue job with retry/idempotency considerations#
Guides: Queue, the installed backend’s own page (Queue (Redis), Queue (SQL), Queue (SQS), Queue (RabbitMQ)), Appendix: Queue Contracts, and Appendix: Redis when Redis owns the effect.
Lifecycle/I-O: a job’s constructor arguments are its whole state — nothing request-scoped from the code that pushed it survives to the worker that runs it.
Security/integrity: delivery is at least once, so a handler that is not safe to run twice records its idempotency key and effect in one atomic operation. In a database, use a unique key in the same transaction. When one Redis node owns both values, use one Lua script that writes the marker and effect together. On the pushing side,
QueueInterface::push()is not enlisted in a transaction the caller has open: the job is enqueued even if that transaction rolls back.kinetis/queue-sql’sSqlQueue::pushOn()is the one backend API that places the row on a transaction the caller supplies, so the enqueue commits with the work — see Queue (SQL).Verification: run the handler twice with the same identifier and assert one effect, and exercise the permanent-failure path at
maxAttempts. If the work must eventually complete, verify the recovery, dead-letter or reconciliation path as well; retry exhaustion alone loses the queue’s ability to make further progress.Non-goal: treating delivery as exactly-once. Leaving
maxAttemptsunset defers to the worker’sQUEUE_MAX_ATTEMPTS, which defaults to0and gives up after the first failed attempt — no setting retries a job forever. A released job is held by the backend for the exponentially-backed-off delayQUEUE_RETRY_BASE_DELAY_SECONDSproduces, so a retry lands later rather than at once.
Scheduled console command#
Guides: CLI, “Running commands on a schedule”, and Database for the unique key that claims a run.
Lifecycle/I-O: the scheduler lives outside Kinetis and starts
vendor/bin/kinetis <command>in its own process, which ends with the command. The exit code is what the scheduler sees.Security/integrity: every scheduler can run a command twice or let two runs overlap, so the command is idempotent or claims its run under a unique key. A host-local lock does not cover several replicas; run the schedule in one place.
Verification: run the command twice for the same slot and assert one effect, and assert a non-zero exit code on failure.
Non-goal: a built-in scheduler or an in-process cron loop inside an HTTP worker.
Outbound HTTP integration with deadline/cancellation/uncertain-write semantics#
Guides: HTTP Client, Appendix: HTTP client contracts.
Lifecycle/I-O:
withTimeout()bounds the whole call, below whatever deadline is waiting on it, andwithMaxResponseBytes()bounds the reply.Security/integrity: a
TransportorTimeoutfailure on a non-idempotent write means the request may already have taken effect — look up the result, or use the API’s own idempotency key, rather than resending it blind.Verification: a test against a mock HTTP client for the timeout and transport-failure branches, asserting the lookup path runs instead of a blind repeat.
Non-goal:
withRetries()on an endpoint whosePUT/DELETEis not actually idempotent.
Broadcast update with truthful delivery semantics#
Guides: Broadcasting.
Lifecycle/I-O: triggering an event suspends the calling Fiber while the request to the broker is in flight; nothing about the broadcast is request-scoped state.
Security/integrity: authorize every
private-/presence-channel with#[BroadcastChannel]; an unauthorized subscription is refused before it reaches the broker.Verification: a test of the authorizer method’s own logic — accept and reject cases — rather than a live broker.
Non-goal: reading the trigger request’s success as proof a subscribed client received the event. This package reports whether the broker accepted the event, not whether, or when, any client saw it.
Application-owned MCP resource or tool#
Guides: Model Context Protocol (MCP), Appendix: MCP Reference.
Lifecycle/I-O: each message resolves and disposes its own request scope, the same way an HTTP request does, per Model Context Protocol (MCP)’s “Each message is its own request”.
Security/integrity: a tool’s arguments validate exactly like a request DTO’s fields, and nothing about the caller’s identity persists past its own message.
Verification: a unit test of the tool or resource method’s own domain logic, plus a separate test that drives the registered definition through
McpServer::handle()(see Appendix: MCP Reference’s “Malformed requests”) to exerciseMcpDispatcherargument hydration, validation, and the MCP result envelope — a direct method call bypasses both.Non-goal: this documentation.
mcp-docsserves only its own fixed page catalogue, whether reached directly or through Orbitron; an application’s own tools and resources arekinetis/mcp’s job.
Application on a non-default runtime adapter#
Guides: Runtime Adapters — “Choose a runtime”, “Choose an adapter explicitly”; Appendix: Container Lifecycle; Database — “Driver selection”; Testing — “Conformance-testing a runtime adapter”; Appendix: Testing Reference — “What each run proves”.
Lifecycle/I-O:
RequestScoperesolves fresh on every invocation under every adapter, persistent ones (FrankenPHP worker mode, RoadRunner, Lambda) included — request identity, request data and callbacks belong there and nowhere else.AppScopebindings, and any other state a process keeps outsideRequestScope(a static, a global, a resident Fiber), can outlive one request on a persistent adapter and must never carry a request’s identity, callbacks or data — see Appendix: Container Lifecycle’sAppScope/RequestScopesplit.Security/integrity:
DB_DRIVER=autoselects the driver the chosen runtime supports — see Database’s “Driver selection”. Do not infer or force a driver value observed under one runtime onto another; let the runtime choose it, or set it deliberately per that section’s two documented exceptions.Verification: Conformance-testing a runtime adapter’s “What each run proves” states each adapter’s own proof limit — Lambda’s shared conformance run is in-process and proves the event conversion alone, RoadRunner’s runs against a real spawned
rr serveprocess, and FrankenPHP’s and PHP-FPM’s run under their real SAPI in CI. When the open question is process lifecycle itself — whetherRequestScopeactually resets between invocations on the chosen adapter — drive one bounded real invocation or request sequence through that adapter’s own runtime rather than the in-process driver call; the conformance suite alone does not establish that for every adapter.Non-goal: treating a local or single-process run as proof of a managed platform’s own process lifecycle guarantees.
See also#
Agent Workflow — establishing installed versions and routing a task here in the first place.
Agent Correctness Review — the review checklist once a change is made.