Forum guided project

Forum Routes

Step 4 / 13 Beginner to intermediate SQLite to MySQL path

Forum Routes

In 0.4.1, the folder/filename defines each URL and method. The forum's web handlers belong under src/Views/forum-demo; its JSON handlers belong under src/Views/api/forum-demo.

Remove the temporary src/Views/forum-demo.get.php from setup before creating forum-demo/index.get.php.

Route Map

Paths below are relative to src/Views/forum-demo/.

FileURL and methodForumActions method
index.get.phpGET /forum-demoindex
topics/index.get.phpGET /forum-demo/topicstopics
topics/index.post.phpPOST /forum-demo/topicsstoreTopic
topics/[id]/index.get.phpGET /forum-demo/topics/1showTopic
topics/[id]/replies.post.phpPOST /forum-demo/topics/1/repliesstoreReply
topics/[id]/replies.get.phpGET reply URLRedirect to topic
login.get.phpGET /forum-demo/loginlogin
login.post.phpPOST /forum-demo/loginauthenticate
logout.post.phpPOST /forum-demo/logoutlogout
admin/index.get.phpGET /forum-demo/adminadmin
admin/users.get.phpGET /forum-demo/admin/usersadminUsers
admin/users.post.phpPOST /forum-demo/admin/usersupdateUserRole
admin/topics.post.phpPOST /forum-demo/admin/topicsmoderateTopic
admin/replies.post.phpPOST /forum-demo/admin/repliesmoderateReply

Guests can read. Members can submit topics/replies. Admins can manage roles and moderation. The write service checks each ability; filenames do not provide authorization.

Connect Read Handlers

Create src/Views/forum-demo/index.get.php:

<?php
use App\Actions\ForumActions;
use Psr\Http\Message\ServerRequestInterface;

return static fn (ServerRequestInterface $request) =>
    (new ForumActions())->index($request);

Create topics/index.get.php with the same imports and:

return static fn (ServerRequestInterface $request) =>
    (new ForumActions())->topics();

Create topics/[id]/index.get.php with the same imports and:

return static fn (ServerRequestInterface $request) =>
    (new ForumActions())->showTopic((string) $request->getAttribute('id'));

These become usable after the handler chapter defines the class. Always return the response from the action.

You are here in the flow:

1

Request

GET /forum-demo/topics/1 selects topics/[id]/index.get.php.

2

Middleware

Validates the matched id before the handler.

3

Action

ForumActions::showTopic() asks the repository for the topic and replies.

4

Response

Renders src/Views/forum-demo/topic.php with prepared data.

Validate The Dynamic Id

[id] matches any segment. The old regex route constraint is not available. Add src/Views/forum-demo/topics/[id]/_middleware.php:

<?php
use CorianderCore\Core\Http\Responses;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;

return [new class implements MiddlewareInterface {
    public function process(
        ServerRequestInterface $request,
        RequestHandlerInterface $handler
    ): ResponseInterface {
        $id = (string) $request->getAttribute('id');
        return ctype_digit($id) && (int) $id > 0
            ? $handler->handle($request)
            : Responses::html('Topic not found.', 404);
    }
}];

All detail/reply handlers in that directory inherit it. Reusable middleware may instead live in App\Middleware.

Connect Form Writes

Create topics/index.post.php with the ForumActions/request imports:

return static fn (ServerRequestInterface $request) =>
    (new ForumActions())->storeTopic($request);

Create topics/[id]/replies.post.php:

return static fn (ServerRequestInterface $request) =>
    (new ForumActions())->storeReply($request, (string) $request->getAttribute('id'));

Each action validates through a service, stores a flash, and redirects to a GET page. It must not leave the browser on a submission URL.

Create topics/[id]/replies.get.php:

<?php
use CorianderCore\Core\Http\Responses;
use Psr\Http\Message\ServerRequestInterface;

return static fn (ServerRequestInterface $request) =>
    Responses::redirect('/forum-demo/topics/' . $request->getAttribute('id'), 302);

This fallback handles bookmarks/back navigation to the reply URL without resubmitting a form.

Authentication And Admin Files

For login.get.php, login.post.php, and logout.post.php, use the same action/request imports and return these calls respectively:

(new ForumActions())->login();
(new ForumActions())->authenticate($request);
(new ForumActions())->logout();

Each call is the returned expression inside its own callable, not three statements in one file.

Apply the same pattern to admin files using the methods in the route-map table. Add admin/_middleware.php returning [new ForumDemoAdminMiddleware()] once the admin chapter defines it. It protects every method below that directory, including write routes, while inheriting root CSRF.

The API chapter adds the three separate JSON method files.

Checkpoint

Run php coriander routes:list. Check paths/methods and inherited middleware before testing action behavior. GET supplies HEAD fallback. Unknown paths return 404; unsupported methods return 405/Allow.

After implementing handlers/auth, verify guest reads, member writes, admin denial, and redirect-after-write. A 404 is not fixed by including a route file in public/routes.php; that mechanism was removed.

Continue with Request Handlers.

Live version

Open the safe demo

Use the demo when a guide step asks you to verify behavior. Local projects write to SQLite; this public demo validates writes without persisting visitor content.

Open forum demo

Project files

Download project files

This download does not include the CorianderPHP framework. Start from a CorianderPHP project, then use the completed app files as a reference.

Download completed app