Controllers
Controllers are the central traffic cops of your application. Their primary responsibility is to handle incoming requests, interact with the necessary models to retrieve data, and then pass that data to the appropriate view to be rendered. This keeps your application logic separate from your presentation logic.
All controllers should be placed in the app/Controllers directory and should extend the Rhapsody\Core\BaseController.
BaseController Features
The BaseController provides a rich set of helper methods and automatically injects core services
so you can focus on your application logic. When you extend it, your controller automatically gains:
- A configured Twig environment with theme support and custom extensions.
- Access to the database via the
$this->dbproperty. - Access to the cache via
$this->cache. - Built-in SEO helpers (Schema.org structured data).
- Twig extensions for React islands, named routes, and CSRF protection.
Creating a Controller
Here is a basic example of a controller class.
view('home/index.twig', [
'title' => 'Welcome',
]);
}
public function showUsers(Request $request): Response
{
$users = (new User())->findAll();
return $this->view('home/users.twig', [
'users' => $users,
]);
}
}
Returning Responses
Returning a View
The view() helper renders a Twig template and returns a Response object with status 200.
public function dashboard(): Response
{
return $this->view('dashboard/index.twig', [
'stats' => $this->getStats(),
], [
'title' => 'Dashboard',
'description' => 'Your personal dashboard',
]);
}
The third parameter allows you to pass SEO metadata (title, description) that will be
automatically merged into the view's meta variable.
Returning JSON
The json() helper creates a JSON response with the correct Content-Type header.
public function getUser(string $id): Response
{
$user = (new User())->findById($id);
if (!$user) {
return $this->json(['error' => 'User not found'], 404);
}
return $this->json($user);
}
Returning a React Component
Rhapsody includes a first‑class integration with React via the react() method.
This mounts a React component as a full‑page HTML response, with props serialised safely and Vite assets
injected automatically.
public function dashboard(): Response
{
return $this->react('Dashboard', [
'user' => auth()->user()->toArray(),
'stats' => $this->getDashboardStats(),
], [
'title' => 'Dashboard',
]);
}
The react() method handles:
- Serialising props as JSON (XSS‑safe).
- Injecting the CSRF token as a
<meta>tag. - Resolving Vite assets (development HMR or production manifest).
- Rendering a complete HTML document with the component mounted at
#rhapsody-root.
For more details, see the React Integration documentation.
Accessing the Request
The router automatically injects an instance of the Rhapsody\Core\Request object as the first parameter
to every controller action. This object provides a clean API for accessing request data.
public function handleContact(Request $request): Response
{
// Get the request method (e.g., 'post')
$method = $request->getMethod();
// Get all input (POST, JSON, PUT, etc.)
$data = $request->getBody();
// Get a specific query parameter from the URL
$page = $request->getQueryParam('page', 1);
// Get a specific POST value
$email = $request->post('email');
// Access uploaded files
$files = $request->getFiles();
// ...
}
Generating URLs with Named Routes
The BaseController automatically registers the RoutingExtension with Twig,
making the route() function available in all templates.
<a href="{{ route('profile.show', {id: user.id}) }}">View Profile</a>
In PHP (e.g., inside a controller), you can use Router::generateUrl():
use Rhapsody\Core\Routing\Router;
public function redirectToProfile(Request $request): Response
{
$url = Router::generateUrl('profile.show', ['id' => $request->get('id')]);
return redirect($url);
}
Dependency Injection & Lazy Loading
The container resolves your controllers and their dependencies automatically. If you type‑hint a service in your controller's constructor, it will be injected.
namespace App\Controllers;
use App\Services\PaymentService;
use Rhapsody\Core\BaseController;
class CheckoutController extends BaseController
{
public function __construct(
private PaymentService $paymentService
) {}
public function process(): Response
{
$this->paymentService->charge(...);
// ...
}
}
For performance, controllers are not lazy‑loaded (they are resolved eagerly).
However, all other services injected into controllers can be lazy‑loaded if you enable
the LAZY_LOADING_ENABLED setting in your .env file. This means that
heavy services (like the ORM or mailer) are only instantiated when they are actually used.
The Services panel in the debug toolbar shows which services were resolved and whether they were lazy‑loaded – perfect for performance tuning.
Debugging Controllers
The debug toolbar includes a Services panel that shows:
- Which services were resolved during the request.
- How long each service took to instantiate.
- Whether the service was lazy‑loaded (green "Yes") or eager (red "No").
This is invaluable for identifying bottlenecks in your controller's dependency graph.
The Middleware panel shows execution times for all middleware, helping you pinpoint slow components.
Next Steps
- Learn about Routing to map URLs to controllers.
- Explore the Request and Response objects.
- Read about React Integration for building modern SPAs.
- Check the Performance guide for lazy loading and caching.