Skip to content

v0.10.0

@Dillonsmart Dillonsmart tagged this 10 Sep 07:07
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.
Assets 2
Loading