The Router has been able to register five verbs since v0.4.0, and the
framework called routing done. But a resource is not routes — it is
routes plus the input they carry, and three of the four letters were
missing something. A put() route matched, ran its Action, and the Action
found an empty payload, because PHP populates $_POST for a POST body and
nothing else. No error, no log line, just a field that was not there.
Request describes all three sources of input now rather than one and a
half: params from the path, query from the query string, payload from the
body. The query string had been split off REQUEST_URI since v0.3.4 so
that /posts?page=2 could match /posts, and then dropped on the floor —
the roadmap listed it as something no application could read. All three
are plain public arrays read with ??; there is deliberately no input()
accessor over them, which would be a second way to read one value and
would hide which of the three it came from.
Routing:
- match() compares case-insensitively rather than Request lowercasing the
URI, so a captured parameter keeps the case it was sent with. Making
routing case-insensitive by destroying the URI meant every slug and
half of every UUID was corrupted on the way in
- static routes still win over dynamic ones of the same shape, so
/posts/create is the create form and not a post whose id is "create"
Request:
- the body is parsed for every verb — JSON, then $_POST where PHP has
filled it (the only thing that can read a multipart body, so uploads
survive), then a form-encoded body read from the stream
- it is built before middleware runs rather than during dispatch, where
reading payload early was a fatal rather than an empty array
- Kernel::body() is protected because php://input is always empty under
the CLI, and tests/Fixtures/KernelWithBody.php is the only thing that
should ever override it
Middleware:
- OverridesMethod reads _method from the body of a POST, which is the
only way a browser form can reach PUT, PATCH or DELETE. It is opt-in
precisely because it is the one behaviour in the framework triggered by
a magic field name: composed in, it is a line in routes/middleware.php
and shows up in `tether routes` and `tether explain`; left out,
_method is an ordinary form field with no meaning
- VerifyCsrfToken reads the token off the request instead of $_POST, so
the hidden field authorises every verb a form can ask for, and the last
superglobal leaves a framework class
Generators:
- make:resource writes the seven ADR triples of a resource and prints the
seven route lines for routes/web.php. It does not write them: a
generator that edited the route table would make the one file a reader
must be able to trust the one file a tool had been at. There is no
$router->resource() and no pluraliser, for the same reason
- every feature is a directory. make:feature wrote five flat files while
make:resource nested each resource, which was two layouts for one
concept — and the flat one was a dead end, since the second route a
feature ever needed cost four moved files and four rewritten
namespaces. A feature is a resource with one operation; both commands
share one set of stubs through Traits\GeneratesTriples
- make:action, make:domain and make:responder take the operation as a
second argument, defaulting to Index
- a Result is named for its shape and shared by every operation that
answers the same way: Collection for many, Record for one, Written for
a write that answers with a redirect, Page for a page with neither
behind it. Naming one per operation produced seven classes of which
four differed from another by their class name and nothing else
Introspection:
- triple() reads the Result off the return type Domain::handle() declares.
It has to, now that Show and Edit both return Results\Record and there
is no Results\Show to predict — but it is also the standard the rest of
that output is held to. Reflection reads a signature and constructs
nothing, so the rule that these commands never instantiate an Action,
Domain or Responder still holds
- every part of a triple carries a declared flag; inspect marks which the
code stated and which the command guessed
- the conventional fallback preferred the old top-level Domains\Results\
bucket whenever the nested class was absent, so for a Result that
existed in neither it named the one place nothing else lives
- autoload-dev gains a Domains\ mapping onto tests/Fixtures/app/Domains,
which backs the fixture the reflection is tested against
The generated resource was run end to end against a linked skeleton
checkout — all seven routes, a mixed-case id, and a form-encoded PUT
arriving through OverridesMethod. Core's own suite cannot cover that: it
has no ADR base classes, which belong to the skeleton.
BREAKING: Request no longer lowercases $uri, and Router::match() expects
the URI as sent. Generated code is emitted into a namespace per feature,
and the flat Action, Domain, Responder, Result and View stubs are gone.
Results are named for their shape rather than per operation.