Routing

The router is the heart of the Rhapsody framework. It maps incoming browser requests to specific controller methods, allowing you to create clean, human-readable URLs. All routes for your web application are defined in the /routes/web.php and /routes/api.php files.


Basic Routing

The most basic route accepts a URI and a callback. The callback is typically an array containing the controller class and the method name to execute. You can define routes for different HTTP methods like GET and POST.

// routes/web.php
use App\Controllers\PageController;
use Rhapsody\Core\Routing\Router;

// A GET route that shows the about page
Router::get('/about', [PageController::class, 'about']);

// A POST route that handles a form submission
Router::post('/contact', [PageController::class, 'handleContact']);

Dynamic Routes with Parameters

Often, you'll need to capture segments of the URI, such as a user ID or a blog post slug. You can do this by defining route parameters enclosed in curly braces {}.

These captured parameters are automatically passed as arguments to your controller method, in the order they appear.

// Route definition
Router::get('/users/{user_id}', [PageController::class, 'viewUser']);

// Controller method
public function viewUser(Request $request, string $user_id)
{
    $user = (new User())->getUserById($user_id);
    // ...
}

Important: The router always passes the Rhapsody\Core\Request object as the first argument to your controller method, followed by any dynamic route parameters.

Named Routes & URL Generation

Rhapsody supports named routes, allowing you to generate URLs dynamically without hardcoding paths. This is especially useful when your application is installed in a subdirectory.

To assign a name to a route, chain the name() method:

Router::get('/profile/{id}', [ProfileController::class, 'show'])->name('profile.show');

You can then generate URLs using the route() Twig function in your views:

<a href="{{ route('profile.show', {id: user.id}) }}">View Profile</a>

The route() function accepts the route name as the first argument and an optional associative array of parameters to replace the placeholders.

If you need the base URL (including subdirectory) prepended, the route() function automatically uses $_ENV['APP_URL'] . $_ENV['APP_BASE_URL'].

You can also generate URLs from PHP using the static method Router::generateUrl($name, $params).

Middleware

You can attach middleware to routes to filter requests (e.g., authentication, CSRF protection). Middleware are resolved from the container and executed before the controller.

Router::get('/dashboard', [DashboardController::class, 'index'])
    ->middleware('auth'); // only authenticated users

Middleware can be defined in the config.php file under the middleware.map key.

Container Integration

The router uses the service container to resolve controllers and their dependencies. Your controller classes can type‑hint any service, and the container will inject them automatically. The router also accepts a ContainerInterface instance, allowing you to swap the container implementation if needed.

For performance, the container can be configured to lazy‑load services (see the Performance documentation).

AJAX-Aware Routes

A single route can serve both a full page load and a JSON response for the same data — check Request::wantsJson() (true when the client sends Accept: application/json) inside the controller action and branch to $this->json(...) instead of $this->view(...). No separate API route needed. See AJAX Pagination for a full example.

Route Caching

For optimal performance, you can cache your routes by running:

php rhapsody route:cache

This will cache your routes to a flat file in /storage/cache/routes/routes.php; saving you the overhead of processing your routing definitions on every request. There is only one thing to be aware of - any time you add a new route, you must clear the cache by either deleting the file manually, or running php rhapsody route:clear, and then recompiling the cache with php rhapsody route:cache again. If you do not regenerate the cache after adding a new route, you will hit a 404 error when you attempt to visit the new page.

Additional notes on routing:

  • It is recommended to leave route caching off in your dev environment.
  • The route cache will be cleared automatically when you update the framework, or when you clear the app cache: php rhapsody cache:clear

404 Handling & the RouteNotFound Event

When no registered route matches the incoming request, the router doesn't immediately show the 404 page. First, it dispatches a Rhapsody\Core\Events\RouteNotFound event, giving other code one last chance to resolve the URL to something real — most commonly, redirecting a URL that's since moved to a new one.

If nothing is listening for this event, or every listener that is declines to act, behavior is unchanged from a normal 404: an HttpException is thrown and handled the usual way. This event only changes anything if something actually listens for it.

namespace App\Listeners;

use Rhapsody\Core\Events\RouteNotFound;
use Rhapsody\Core\Response;

class RedirectLegacyUrls
{
    public function handle(RouteNotFound $event): void
    {
        $newPath = /* look up $event->uri in your own redirects table */ null;

        if ($newPath !== null) {
            $response = (new Response())
                ->setStatusCode(301)
                ->setHeader('Location', $newPath);

            $event->setResponse($response); // tells the router to use this instead of 404ing
        }
    }
}

Important: RouteNotFound is what the docs call a stoppable event — once a listener calls $event->setResponse(), no further RouteNotFound listeners run, and the router returns that response directly instead of throwing the 404. This is the exception, not the rule, for how events behave in Rhapsody — see Events & Listeners for the full explanation of stoppable events and why almost no other event works this way.

A module that wants to listen for this event must declare Rhapsody\Core\Events\RouteNotFound under events.listen.listen in its module.json manifest before it's allowed to — this follows the same permission model as any other module event listener.

Debugging Routes

The debug toolbar includes a Services and Middleware panel that show which routes were matched, which middleware were executed, and which services were resolved during the request. This is invaluable for profiling and debugging.

Social Login (OAuth)

Rhapsody includes built‑in support for social authentication via Google and Facebook. See the Social Authentication documentation for setup instructions and route examples.