CRUD

Seven routes, generated, and none of it hidden

A CRUD resource in ADR is seven Actions, because ADR has one Action per route. That is a lot of obvious code, so the framework writes it for you.

php tether make:resource Post --uri=/posts

--uri sets the base URL that routes and redirects use, and defaults to the kebab-cased name. There is no pluraliser and there is not going to be one — guessing that Post becomes posts and Category becomes categories is a table of English irregulars that will be wrong about the one word your application cares about.

What it writes

app/Actions/Post/      Index Create Store Show Edit Update Destroy
app/Domains/Post/      Index Create Store Show Edit Update Destroy
app/Domains/Post/Results/   Collection  Record  Written
app/Responders/Post/   Index Create Store Show Edit Update Destroy
app/Views/pages/post/  index.php  create.php  show.php  edit.php

Twenty-eight files. Four views rather than seven, because a write answers with a redirect and has no page to render.

The routes are printed, not written

$router->get('/posts', Actions\Post\Index::class);
$router->get('/posts/create', Actions\Post\Create::class);
$router->post('/posts', Actions\Post\Store::class);
$router->get('/posts/{id}', Actions\Post\Show::class);
$router->get('/posts/{id}/edit', Actions\Post\Edit::class);
$router->put('/posts/{id}', Actions\Post\Update::class);
$router->delete('/posts/{id}', Actions\Post\Destroy::class);

The command prints these for you to paste into routes/web.php. It does not write them itself: the route table is the one file you must be able to trust, and a generator that had been editing it would take that away. For the same reason there is no $router->resource() registering seven routes from one line.

The PUT and DELETE routes need OverridesMethod in routes/middleware.php, or a browser form cannot reach them.

Three answers, three Results

Actions, Domains and Responders are named for the operation. A Result is named for its shape, and shared by every operation that answers the same way — there are only three answers a CRUD domain gives.

Collection — many records. Used by Index. Its Responder is also handed $request->query, which is where paging and filtering read their input.

Record — one record, and its identity. Used by Create, Show and Edit. The id is kept separate from the attributes because a form posts back to a URL built from it — and because a create form has attributes with no id yet, so an empty id is what tells the template it is making rather than editing.

Written — something changed, and which one. Used by Store, Update and Destroy. It carries the identifier rather than the record, because the Responder answers with a redirect and the browser asks for the record again.

These are starting shapes, not a fixed vocabulary. Rename them and their properties for what your domain actually deals in — that is the whole point of a Result being yours.

A write answers with a redirect

public function __invoke(Written $result): Response
{
    return Response::redirect('/posts/' . $result->id, 303);
}

Post/Redirect/Get: the browser is sent to a URL it can safely ask for again, so refreshing after saving does not submit the form a second time. 303 rather than the default 302, because only 303 is defined to make the next request a GET whatever this one was — which is not guaranteed after a PUT or a DELETE.

A feature grows into a resource

make:feature and make:resource write the same layout — a directory per feature — and share the same templates. A resource is a feature with seven operations instead of one, so nothing has to move when a page turns into a resource:

php tether make:feature Blog     # app/Actions/Blog/Index.php
php tether make:action Blog Show # app/Actions/Blog/Show.php, beside it