Complete action map
HTML forms directly understand GET and POST. We therefore use explicit paths for update and delete without adding a hidden method-override mechanism.
The routes
src/routes/WebApp.php
$this->get('/books', [BookController::class, 'index']);
$this->get('/books/create', [BookController::class, 'create']);
$this->post('/books', [BookController::class, 'store']);
$this->get('/books/{id}/edit', [BookController::class, 'edit']);
$this->post('/books/{id}/edit', [BookController::class, 'update']);
$this->post('/books/{id}/delete', [BookController::class, 'destroy']);Read a dynamic identifier
public function edit(Request $request): Response
{
$id = (int) $request->attribute('id');
$books = $this->session->get('books', []);
$book = $this->findBook($books, $id);
if ($book === null) {
return Response::html('<h1>404</h1><p>Book not found.</p>', 404);
}
return $this->view('books/edit.php', [
'book' => $book,
'errors' => [],
]);
}The router extracts {id} from the address and places it in Request attributes. The int cast prevents treating this identifier as arbitrary text.
Search without duplicating the loop
/** @param list<array{id:int,title:string,author:string}> $books */
private function findBook(array $books, int $id): ?array
{
foreach ($books as $book) {
if ($book['id'] === $id) {
return $book;
}
}
return null;
}This private method does not answer the browser. It simply helps controller actions find a book. In the next chapter, a database and repository will replace this session search.
Books here belong to one browser and may disappear when the session expires. This is enough to learn HTTP; it is not production storage.