Controllers & Routing
Define routes with PHP attributes and create RESTful controllers
Creating a Controller
        Controllers in Larafony extend the Controller base class and use the #[Route] attribute
        to define routes directly on methods.
    
<?php
declare(strict_types=1);
namespace App\Controllers;
use Larafony\Framework\Routing\Advanced\Attributes\Route;
use Larafony\Framework\Web\Controller;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
class HomeController extends Controller
{
    #[Route('/', 'GET')]
    public function index(ServerRequestInterface $request): ResponseInterface
    {
        return $this->render('home', [
            'title' => 'Welcome to Larafony'
        ]);
    }
}src/Controllers directory.
        No need to manually register routes!
    Route Attributes
        The #[Route] attribute accepts a path and HTTP method:
    
#[Route('/notes', 'GET')]
public function index(): ResponseInterface
{
    // GET /notes
}
#[Route('/notes', 'POST')]
public function store(): ResponseInterface
{
    // POST /notes
}
#[Route('/notes/{id}', 'GET')]
public function show(int $id): ResponseInterface
{
    // GET /notes/123
}
#[Route('/notes/{id}', 'PUT')]
public function update(int $id): ResponseInterface
{
    // PUT /notes/123
}
#[Route('/notes/{id}', 'DELETE')]
public function destroy(int $id): ResponseInterface
{
    // DELETE /notes/123
}Route Parameters
Capture route parameters using curly braces in the path:
#[Route('/users/{id}', 'GET')]
public function show(int $id): ResponseInterface
{
    $user = User::query()->find($id);
    return $this->render('users.show', ['user' => $user]);
}
#[Route('/posts/{slug}', 'GET')]
public function showBySlug(string $slug): ResponseInterface
{
    $post = Post::query()
        ->where('slug', '=', $slug)
        ->first();
    return $this->render('posts.show', ['post' => $post]);
}Model Binding (Auto-Binding)
        One of Larafony's most powerful features is automatic model binding. Using the #[RouteParam] attribute,
        you can automatically resolve route parameters into model instances. The framework will fetch the model from the database
        and inject it directly into your controller method.
    
Basic Model Binding
        Use #[RouteParam] to configure model binding:
    
use App\Models\Note;
use Larafony\Framework\Routing\Advanced\Attributes\{Route, RouteParam};
#[Route('/notes/<note>', 'GET')]
#[RouteParam(name: 'note', pattern: '\d+', bind: Note::class)]
public function show(ServerRequestInterface $request, Note $note): ResponseInterface
{
    // $note is automatically loaded from database
    // using the route parameter value
    return $this->render('notes.show', ['note' => $note]);
}/notes/123, Larafony:
        - Validates the parameter matches the pattern (\d+)
- Calls Note::findForRoute(123)to load the model
- Injects the loaded model into your controller method
- Returns 404 automatically if the model is not found!
Model findForRoute Method
        Your model must implement the findForRoute() method for binding to work:
    
use Larafony\Framework\Database\ORM\Model;
class Note extends Model
{
    public string $table { get => 'notes'; }
    public static function findForRoute(int|string $id): ?static
    {
        return static::query()->find($id);
    }
}Before and After Comparison
See how model binding simplifies your code:
// ❌ Without Model Binding (manual approach)
#[Route('/notes/<id:\d+>', 'GET')]
public function show(ServerRequestInterface $request): ResponseInterface
{
    $params = $request->getAttribute('routeParams');
    $note = Note::query()->find($params['id']);
    if (!$note) {
        // Handle 404
        return new Response(404, [], 'Note not found');
    }
    return $this->render('notes.show', ['note' => $note]);
}
// ✅ With Model Binding (automatic approach)
#[Route('/notes/<note>', 'GET')]
#[RouteParam(name: 'note', pattern: '\d+', bind: Note::class)]
public function show(ServerRequestInterface $request, Note $note): ResponseInterface
{
    // $note is already loaded, 404 handled automatically!
    return $this->render('notes.show', ['note' => $note]);
}Custom Resolution Methods
        Use findMethod parameter to specify a custom resolution method (e.g., find by slug instead of ID):
    
// In your model
class Post extends Model
{
    public static function findBySlug(string $slug): ?static
    {
        return static::query()->where('slug', '=', $slug)->first();
    }
}
// In your controller
#[Route('/posts/<slug:[a-z0-9-]+>', 'GET')]
#[RouteParam(name: 'slug', bind: Post::class, findMethod: 'findBySlug')]
public function show(ServerRequestInterface $request, Post $post): ResponseInterface
{
    // $post resolved via findBySlug() instead of default findForRoute()
    // Pattern validation ([a-z0-9-]+) and binding in the same attribute!
    return $this->render('posts.show', ['post' => $post]);
}Multiple Model Bindings
You can bind multiple models in a single route:
use App\Models\User;
use App\Models\Note;
#[Route('/users/<user>/notes/<note>', 'GET')]
#[RouteParam(name: 'user', pattern: '\d+', bind: User::class)]
#[RouteParam(name: 'note', pattern: '\d+', bind: Note::class)]
public function showUserNote(
    ServerRequestInterface $request,
    User $user,
    Note $note
): ResponseInterface {
    // Both models are automatically loaded!
    // $user is loaded from <user> parameter
    // $note is loaded from <note> parameter
    return $this->render('users.notes.show', [
        'user' => $user,
        'note' => $note
    ]);
}Combining Model Binding with DTOs
Mix model binding with DTO injection for powerful update operations:
use App\Models\Note;
use App\DTOs\UpdateNoteDto;
#[Route('/notes/<note>', 'PUT')]
#[RouteParam(name: 'note', pattern: '\d+', bind: Note::class)]
public function update(
    ServerRequestInterface $request,
    Note $note,
    UpdateNoteDto $dto
): ResponseInterface {
    // $note is auto-bound from route parameter
    // $dto is auto-created and validated from request body
    $note->title = $dto->title;
    $note->content = $dto->content;
    $note->save();
    return $this->redirect("/notes/{$note->id}");
}Working with Relationships
Auto-bound models work seamlessly with relationships:
#[Route('/notes/<note>', 'GET')]
#[RouteParam(name: 'note', pattern: '\d+', bind: Note::class)]
public function show(ServerRequestInterface $request, Note $note): ResponseInterface
{
    // Access relationships directly
    $author = $note->user;
    $tags = $note->tags;
    $comments = $note->comments;
    return $this->render('notes.show', [
        'note' => $note,
        'author' => $author,
        'tags' => $tags,
        'comments' => $comments
    ]);
}Complete CRUD with Model Binding
Here's a complete CRUD controller using model binding:
use App\Models\Note;
use App\DTOs\{CreateNoteDto, UpdateNoteDto};
use Larafony\Framework\Routing\Advanced\Attributes\{Route, RouteParam};
class NoteController extends Controller
{
    // List all notes (no binding needed)
    #[Route('/notes', 'GET')]
    public function index(ServerRequestInterface $request): ResponseInterface
    {
        $notes = Note::query()->get();
        return $this->render('notes.index', ['notes' => $notes]);
    }
    // Show single note (model binding)
    #[Route('/notes/<note>', 'GET')]
    #[RouteParam(name: 'note', pattern: '\d+', bind: Note::class)]
    public function show(ServerRequestInterface $request, Note $note): ResponseInterface
    {
        return $this->render('notes.show', ['note' => $note]);
    }
    // Show edit form (model binding)
    #[Route('/notes/<note>/edit', 'GET')]
    #[RouteParam(name: 'note', pattern: '\d+', bind: Note::class)]
    public function edit(ServerRequestInterface $request, Note $note): ResponseInterface
    {
        return $this->render('notes.edit', ['note' => $note]);
    }
    // Update note (model binding + DTO)
    #[Route('/notes/<note>', 'PUT')]
    #[RouteParam(name: 'note', pattern: '\d+', bind: Note::class)]
    public function update(
        ServerRequestInterface $request,
        Note $note,
        UpdateNoteDto $dto
    ): ResponseInterface {
        $note->title = $dto->title;
        $note->content = $dto->content;
        $note->save();
        return $this->redirect("/notes/{$note->id}");
    }
    // Delete note (model binding)
    #[Route('/notes/<note>', 'DELETE')]
    #[RouteParam(name: 'note', pattern: '\d+', bind: Note::class)]
    public function destroy(ServerRequestInterface $request, Note $note): ResponseInterface
    {
        $note->delete();
        return $this->redirect('/notes');
    }
}<param> or <param:pattern>
        syntax for route parameters, not {param}. This allows inline regex patterns like
        <id:\d+> or <slug:[a-z0-9-]+>.
    DTO Injection
Type-hint a DTO class to automatically validate and hydrate request data:
use App\DTOs\CreateNoteDto;
#[Route('/notes', 'POST')]
public function store(CreateNoteDto $dto): ResponseInterface
{
    // $dto is automatically created from request
    // and validated based on attributes
    $note = new Note()->fill([
        'title' => $dto->title,
        'content' => $dto->content,
    ]);
    $note->save();
    return $this->redirect('/notes');
}See the DTO Validation guide for more details.
Response Helpers
        The Controller base class provides helpful methods for creating responses:
    
Rendering Views
// Render a Blade template
return $this->render('notes.index', [
    'notes' => $notes
]);JSON Responses
// Return JSON
return $this->json([
    'success' => true,
    'data' => $notes
]);
// With status code
return $this->json(['error' => 'Not found'], 404);Redirects
// Redirect to a URL
return $this->redirect('/notes');
// Redirect with status code
return $this->redirect('/login', 302);Complete CRUD Example
Here's a complete RESTful controller for managing notes:
<?php
declare(strict_types=1);
namespace App\Controllers;
use App\Models\Note;
use App\DTOs\CreateNoteDto;
use App\DTOs\UpdateNoteDto;
use Larafony\Framework\Routing\Advanced\Attributes\Route;
use Larafony\Framework\Web\Controller;
use Psr\Http\Message\ResponseInterface;
class NoteController extends Controller
{
    #[Route('/notes', 'GET')]
    public function index(): ResponseInterface
    {
        $notes = Note::query()->get();
        return $this->render('notes.index', ['notes' => $notes]);
    }
    #[Route('/notes/create', 'GET')]
    public function create(): ResponseInterface
    {
        return $this->render('notes.create');
    }
    #[Route('/notes', 'POST')]
    public function store(CreateNoteDto $dto): ResponseInterface
    {
        $note = new Note()->fill([
            'title' => $dto->title,
            'content' => $dto->content,
            'user_id' => 1, // Get from auth
        ]);
        $note->save();
        return $this->redirect('/notes');
    }
    #[Route('/notes/{id}', 'GET')]
    public function show(Note $note): ResponseInterface
    {
        return $this->render('notes.show', ['note' => $note]);
    }
    #[Route('/notes/{id}/edit', 'GET')]
    public function edit(Note $note): ResponseInterface
    {
        return $this->render('notes.edit', ['note' => $note]);
    }
    #[Route('/notes/{id}', 'PUT')]
    public function update(Note $note, UpdateNoteDto $dto): ResponseInterface
    {
        $note->title = $dto->title;
        $note->content = $dto->content;
        $note->save();
        return $this->redirect("/notes/{$note->id}");
    }
    #[Route('/notes/{id}', 'DELETE')]
    public function destroy(Note $note): ResponseInterface
    {
        $note->delete();
        return $this->redirect('/notes');
    }
}API Controllers
Create JSON APIs by returning JSON responses:
<?php
namespace App\Controllers;
use App\Models\Note;
use Larafony\Framework\Routing\Advanced\Attributes\Route;
use Larafony\Framework\Web\Controller;
use Psr\Http\Message\ResponseInterface;
class ApiNoteController extends Controller
{
    #[Route('/api/notes', 'GET')]
    public function index(): ResponseInterface
    {
        $notes = Note::query()->get();
        return $this->json([
            'success' => true,
            'data' => $notes
        ]);
    }
    #[Route('/api/notes/{id}', 'GET')]
    public function show(Note $note): ResponseInterface
    {
        return $this->json([
            'success' => true,
            'data' => $note
        ]);
    }
}