Skip to main content
Back to ScopeForged

ScopeForged Documentation

Technical documentation, guides, and feature references for the ScopeForged client portal.

Architecture Patterns/Controller Patterns

Controller Patterns Guide

Last Updated: 2026-01-08 Status: Active Audience: Developers

This guide documents controller conventions and patterns for the Client Portal application.


Table of Contents

  1. Overview
  2. Resource Controllers
  3. Controller Organization
  4. Request Handling
  5. Response Patterns
  6. Authorization
  7. API Controllers

Overview

Controllers handle HTTP requests, delegate to services for business logic, and return responses. Keep controllers thin by moving business logic to services.

Controller Responsibilities

DoDon't
Validate input (Form Requests)Complex business logic
Authorize actionsDatabase queries beyond simple CRUD
Call servicesSend emails directly
Return responsesFormat data for views
Handle HTTP concernsAccess external APIs

Resource Controllers

Generating Resource Controllers

php artisan make:controller Admin/ClientController --resource

Standard Resource Methods

MethodURIActionRoute Name
indexGET /clientsList allclients.index
createGET /clients/createShow formclients.create
storePOST /clientsCreate newclients.store
showGET /clients/{client}Show oneclients.show
editGET /clients/{client}/editShow edit formclients.edit
updatePUT /clients/{client}Updateclients.update
destroyDELETE /clients/{client}Deleteclients.destroy

Basic Resource Controller

<?php

namespace App\Http\Controllers\Admin;

use App\Http\Controllers\Controller;
use App\Http\Requests\StoreClientRequest;
use App\Http\Requests\UpdateClientRequest;
use App\Models\Client;
use Illuminate\Http\RedirectResponse;
use Illuminate\View\View;

class ClientController extends Controller
{
    /**
     * Display a listing of clients.
     */
    public function index(): View
    {
        $clients = Client::query()
            ->withCount(['projects', 'invoices'])
            ->orderBy('company_name')
            ->paginate(15);

        return view('admin.clients.index', compact('clients'));
    }

    /**
     * Show the form for creating a new client.
     */
    public function create(): View
    {
        return view('admin.clients.create');
    }

    /**
     * Store a newly created client.
     */
    public function store(StoreClientRequest $request): RedirectResponse
    {
        $client = Client::create($request->validated());

        return redirect()
            ->route('admin.clients.show', $client)
            ->with('success', 'Client created successfully.');
    }

    /**
     * Display the specified client.
     */
    public function show(Client $client): View
    {
        $client->load(['projects', 'invoices' => fn ($q) => $q->latest()->limit(5)]);

        return view('admin.clients.show', compact('client'));
    }

    /**
     * Show the form for editing the client.
     */
    public function edit(Client $client): View
    {
        return view('admin.clients.edit', compact('client'));
    }

    /**
     * Update the specified client.
     */
    public function update(UpdateClientRequest $request, Client $client): RedirectResponse
    {
        $client->update($request->validated());

        return redirect()
            ->route('admin.clients.show', $client)
            ->with('success', 'Client updated successfully.');
    }

    /**
     * Remove the specified client.
     */
    public function destroy(Client $client): RedirectResponse
    {
        $client->delete();

        return redirect()
            ->route('admin.clients.index')
            ->with('success', 'Client deleted successfully.');
    }
}

Route Registration

// routes/web.php
Route::middleware(['auth', 'verified', 'admin'])->prefix('admin')->name('admin.')->group(function () {
    Route::resource('clients', Admin\ClientController::class);
    Route::resource('projects', Admin\ProjectController::class);
    Route::resource('invoices', Admin\InvoiceController::class);
});

Controller Organization

Directory Structure

app/Http/Controllers/
├── Controller.php              # Base controller
├── Admin/                      # Admin controllers
│   ├── ClientController.php
│   ├── ProjectController.php
│   ├── InvoiceController.php
│   └── DashboardController.php
├── Portal/                     # Client portal controllers
│   ├── DashboardController.php
│   ├── ProjectController.php
│   └── InvoiceController.php
└── Api/                        # API controllers
    └── V1/
        ├── ClientController.php
        └── ProjectController.php

Single Action Controllers

For actions that don't fit resource conventions:

<?php

namespace App\Http\Controllers\Admin;

use App\Http\Controllers\Controller;
use App\Models\Invoice;
use App\Services\InvoiceService;
use Illuminate\Http\RedirectResponse;

class SendInvoiceController extends Controller
{
    /**
     * Send the invoice to the client.
     */
    public function __invoke(Invoice $invoice, InvoiceService $service): RedirectResponse
    {
        $this->authorize('send', $invoice);

        try {
            $service->sendInvoice($invoice);

            return redirect()
                ->back()
                ->with('success', 'Invoice sent successfully.');
        } catch (\Exception $e) {
            return redirect()
                ->back()
                ->with('error', $e->getMessage());
        }
    }
}
// Route registration
Route::post('invoices/{invoice}/send', SendInvoiceController::class)
    ->name('admin.invoices.send');

Nested Resource Controllers

// routes/web.php
Route::resource('clients.projects', Admin\ClientProjectController::class)
    ->shallow();  // Uses project ID directly for show/edit/update/destroy

// Generates:
// GET    /clients/{client}/projects          -> index
// GET    /clients/{client}/projects/create   -> create
// POST   /clients/{client}/projects          -> store
// GET    /projects/{project}                 -> show (shallow)
// GET    /projects/{project}/edit            -> edit (shallow)
// PUT    /projects/{project}                 -> update (shallow)
// DELETE /projects/{project}                 -> destroy (shallow)

Request Handling

Form Request Validation

Always use Form Request classes:

// Good - Form Request handles validation
public function store(StoreClientRequest $request): RedirectResponse
{
    $client = Client::create($request->validated());
    return redirect()->route('admin.clients.show', $client);
}

// Bad - Validation in controller
public function store(Request $request): RedirectResponse
{
    $validated = $request->validate([
        'company_name' => 'required|string|max:255',
        // ...
    ]);
    // ...
}

Accessing Validated Data

public function store(StoreClientRequest $request): RedirectResponse
{
    // All validated fields
    $data = $request->validated();

    // Specific field
    $name = $request->validated('company_name');

    // Safe access (returns null if not validated)
    $optional = $request->safe()->only(['company_name', 'email']);
}

Route Model Binding

// Automatic binding - Laravel resolves {client} to Client model
public function show(Client $client): View
{
    return view('admin.clients.show', compact('client'));
}

// Custom key
// Route: /clients/{client:slug}
public function show(Client $client): View
{
    // Resolved by slug instead of id
}

// Scoped binding
// Route: /clients/{client}/projects/{project}
public function show(Client $client, Project $project): View
{
    // Project is automatically scoped to client
}

Response Patterns

View Responses

// Simple view
return view('admin.clients.index');

// With data
return view('admin.clients.index', [
    'clients' => $clients,
    'stats' => $stats,
]);

// Using compact
return view('admin.clients.show', compact('client', 'projects'));

Redirect Responses

// To named route
return redirect()->route('admin.clients.index');

// With route parameters
return redirect()->route('admin.clients.show', $client);
// or
return redirect()->route('admin.clients.show', ['client' => $client->id]);

// Back to previous page
return redirect()->back();

// With flash message
return redirect()
    ->route('admin.clients.index')
    ->with('success', 'Client created successfully.');

// With errors
return redirect()
    ->back()
    ->withErrors(['email' => 'This email is already taken.'])
    ->withInput();

Flash Messages

// Success message
return redirect()->route('admin.clients.index')
    ->with('success', 'Client created successfully.');

// Error message
return redirect()->back()
    ->with('error', 'Unable to delete client with active projects.');

// Multiple messages
return redirect()->route('admin.clients.index')
    ->with([
        'success' => 'Client created.',
        'info' => 'Welcome email sent.',
    ]);

Displaying Flash Messages (Blade)

@if (session('success'))
    <div class="bg-green-100 text-green-800 p-4 rounded">
        {{ session('success') }}
    </div>
@endif

@if (session('error'))
    <div class="bg-red-100 text-red-800 p-4 rounded">
        {{ session('error') }}
    </div>
@endif

Authorization

Using Policies in Controllers

class ProjectController extends Controller
{
    public function __construct()
    {
        // Authorize all resource actions
        $this->authorizeResource(Project::class, 'project');
    }

    // Or authorize individually
    public function show(Project $project): View
    {
        $this->authorize('view', $project);

        return view('portal.projects.show', compact('project'));
    }

    public function update(UpdateProjectRequest $request, Project $project): RedirectResponse
    {
        $this->authorize('update', $project);

        $project->update($request->validated());

        return redirect()->route('admin.projects.show', $project);
    }
}

Policy Method Mapping

Controller MethodPolicy Method
indexviewAny
showview
createcreate
storecreate
editupdate
updateupdate
destroydelete

Manual Authorization

public function archive(Project $project): RedirectResponse
{
    // Custom policy method
    $this->authorize('archive', $project);

    $project->update(['status' => 'archived']);

    return redirect()->back();
}

// Check without exception
public function show(Project $project): View
{
    $canEdit = auth()->user()->can('update', $project);

    return view('projects.show', compact('project', 'canEdit'));
}

API Controllers

API Resource Controller

<?php

namespace App\Http\Controllers\Api\V1;

use App\Http\Controllers\Controller;
use App\Http\Requests\Api\StoreClientRequest;
use App\Http\Resources\ClientResource;
use App\Http\Resources\ClientCollection;
use App\Models\Client;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Response;

class ClientController extends Controller
{
    /**
     * Display a listing of clients.
     */
    public function index(): ClientCollection
    {
        $clients = Client::query()
            ->withCount(['projects', 'invoices'])
            ->paginate(15);

        return new ClientCollection($clients);
    }

    /**
     * Store a newly created client.
     */
    public function store(StoreClientRequest $request): JsonResponse
    {
        $client = Client::create($request->validated());

        return (new ClientResource($client))
            ->response()
            ->setStatusCode(Response::HTTP_CREATED);
    }

    /**
     * Display the specified client.
     */
    public function show(Client $client): ClientResource
    {
        return new ClientResource($client->load(['projects', 'invoices']));
    }

    /**
     * Update the specified client.
     */
    public function update(UpdateClientRequest $request, Client $client): ClientResource
    {
        $client->update($request->validated());

        return new ClientResource($client);
    }

    /**
     * Remove the specified client.
     */
    public function destroy(Client $client): Response
    {
        $client->delete();

        return response()->noContent();
    }
}

API Response Consistency

// Success with data
return response()->json([
    'success' => true,
    'data' => $client,
    'message' => 'Client created successfully.',
], 201);

// Success without data
return response()->json([
    'success' => true,
    'message' => 'Client deleted successfully.',
]);

// Error response
return response()->json([
    'success' => false,
    'message' => 'Client not found.',
    'errors' => [],
], 404);

Best Practices

Do

  • Use Form Requests for validation
  • Use Route Model Binding
  • Keep controllers thin (delegate to services)
  • Use resource controllers for CRUD
  • Return appropriate HTTP status codes
  • Use policies for authorization
  • Type hint return types

Don't

  • Put business logic in controllers
  • Use Request instead of Form Requests
  • Mix web and API responses
  • Skip authorization checks
  • Hard-code strings (use translations)
  • Return views from API controllers

Controller Method Order

class ClientController extends Controller
{
    // 1. Constructor (if needed)
    public function __construct() {}

    // 2. Resource methods in order
    public function index() {}
    public function create() {}
    public function store() {}
    public function show() {}
    public function edit() {}
    public function update() {}
    public function destroy() {}

    // 3. Additional methods
    public function archive() {}
    public function restore() {}
}