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->db property.
  • 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