Routing & Controllers
Minimum Version Requirement
This feature requires WhatsMarkSaaS v2.0.0 or higher. Ensure your installation meets this requirement before proceeding.
Overview
This guide covers route definition and controller implementation for custom modules. You will learn to create web routes, API endpoints, apply middleware, and handle requests with tenant-aware controllers.
Web Routes
Web routes are defined in Routes/web.php and handle browser-based requests.
Basic Route Definition
<?php
use Illuminate\Support\Facades\Route;
use Modules\OrderTracker\Http\Controllers\OrderTrackerController;
Route::middleware('web')->group(function () {
Route::prefix('order-tracker')->group(function () {
Route::get('/', [OrderTrackerController::class, 'index'])
->name('order-tracker.index');
Route::get('/create', [OrderTrackerController::class, 'create'])
->name('order-tracker.create');
Route::post('/store', [OrderTrackerController::class, 'store'])
->name('order-tracker.store');
Route::get('/{id}', [OrderTrackerController::class, 'show'])
->name('order-tracker.show');
Route::get('/{id}/edit', [OrderTrackerController::class, 'edit'])
->name('order-tracker.edit');
Route::put('/{id}', [OrderTrackerController::class, 'update'])
->name('order-tracker.update');
Route::delete('/{id}', [OrderTrackerController::class, 'destroy'])
->name('order-tracker.destroy');
});
});Route Naming Conventions
Routes must follow consistent naming patterns:
| Pattern | Example | Description |
|---|---|---|
{module}.index | order-tracker.index | List view |
{module}.create | order-tracker.create | Create form |
{module}.store | order-tracker.store | Store new record |
{module}.show | order-tracker.show | Detail view |
{module}.edit | order-tracker.edit | Edit form |
{module}.update | order-tracker.update | Update record |
{module}.destroy | order-tracker.destroy | Delete record |
Route prefix convention:
Use kebab-case matching the module alias defined in module.json:
// module.json: "alias": "order-tracker"
Route::prefix('order-tracker')->group(function () {
// Routes here
});API Routes
API routes are defined in Routes/api.php and handle JSON-based API requests.
Basic API Route Definition
<?php
use Illuminate\Support\Facades\Route;
use Modules\OrderTracker\Http\Controllers\Api\OrderApiController;
Route::middleware('api')->prefix('api')->group(function () {
Route::prefix('order-tracker')->group(function () {
// Public API endpoints (no authentication)
Route::get('/status', [OrderApiController::class, 'status']);
// Protected API endpoints (require authentication)
Route::middleware('auth:sanctum')->group(function () {
Route::get('/orders', [OrderApiController::class, 'index']);
Route::get('/orders/{id}', [OrderApiController::class, 'show']);
Route::post('/orders', [OrderApiController::class, 'store']);
Route::put('/orders/{id}', [OrderApiController::class, 'update']);
Route::delete('/orders/{id}', [OrderApiController::class, 'destroy']);
});
});
});API Versioning
Implement API versioning using route prefixes:
Route::middleware('api')->prefix('api')->group(function () {
// v1 routes
Route::prefix('v1/order-tracker')->group(function () {
Route::get('/orders', [OrderApiV1Controller::class, 'index']);
});
// v2 routes
Route::prefix('v2/order-tracker')->group(function () {
Route::get('/orders', [OrderApiV2Controller::class, 'index']);
});
});Middleware Application
Common Middleware
| Middleware | Purpose | Usage |
|---|---|---|
web | Browser requests, CSRF, sessions | Required for all web routes |
api | API requests, JSON responses | Required for all API routes |
auth | User authentication | Protect authenticated routes |
auth:sanctum | API token authentication | Protect API endpoints |
tenant | Tenant context verification | Ensure tenant isolation |
Authentication Middleware
Web authentication:
Route::middleware(['web', 'auth'])->group(function () {
Route::prefix('order-tracker')->group(function () {
Route::get('/', [OrderTrackerController::class, 'index'])
->name('order-tracker.index');
});
});API authentication:
Route::middleware(['api', 'auth:sanctum'])->prefix('api')->group(function () {
Route::prefix('order-tracker')->group(function () {
Route::get('/orders', [OrderApiController::class, 'index']);
});
});Tenant Middleware
Apply tenant middleware to ensure tenant-scoped operations:
Route::middleware(['web', 'auth', 'tenant'])->group(function () {
Route::prefix('order-tracker')->group(function () {
Route::get('/', [OrderTrackerController::class, 'index'])
->name('order-tracker.index');
});
});Custom Middleware
Create custom middleware in Http/Middleware/:
<?php
namespace Modules\OrderTracker\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class CheckOrderPermission
{
/**
* Handle an incoming request.
*
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
*/
public function handle(Request $request, Closure $next): Response
{
if (! $request->user()->can('manage-orders')) {
abort(403, 'Unauthorized action.');
}
return $next($request);
}
}Register custom middleware:
Route::middleware(['web', 'auth', CheckOrderPermission::class])->group(function () {
Route::prefix('order-tracker')->group(function () {
// Protected routes
});
});Controllers
Basic Controller Structure
<?php
namespace Modules\OrderTracker\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Routing\Controller;
use Modules\OrderTracker\Models\Order;
class OrderTrackerController extends Controller
{
/**
* Display a listing of orders.
*/
public function index()
{
$orders = Order::where('tenant_id', tenant_id())
->latest()
->paginate(20);
return view('ordertracker::index', compact('orders'));
}
/**
* Show the form for creating a new order.
*/
public function create()
{
return view('ordertracker::create');
}
/**
* Store a newly created order.
*/
public function store(Request $request)
{
$validated = $request->validate([
'order_number' => 'required|string|unique:orders',
'total' => 'required|numeric|min:0',
'status' => 'required|string|in:pending,processing,completed',
]);
$order = Order::create([
'tenant_id' => tenant_id(),
'order_number' => $validated['order_number'],
'total' => $validated['total'],
'status' => $validated['status'],
]);
return redirect()
->route('order-tracker.show', $order->id)
->with('success', 'Order created successfully.');
}
/**
* Display the specified order.
*/
public function show(int $id)
{
$order = Order::where('tenant_id', tenant_id())
->findOrFail($id);
return view('ordertracker::show', compact('order'));
}
/**
* Show the form for editing the specified order.
*/
public function edit(int $id)
{
$order = Order::where('tenant_id', tenant_id())
->findOrFail($id);
return view('ordertracker::edit', compact('order'));
}
/**
* Update the specified order.
*/
public function update(Request $request, int $id)
{
$order = Order::where('tenant_id', tenant_id())
->findOrFail($id);
$validated = $request->validate([
'total' => 'required|numeric|min:0',
'status' => 'required|string|in:pending,processing,completed',
]);
$order->update($validated);
return redirect()
->route('order-tracker.show', $order->id)
->with('success', 'Order updated successfully.');
}
/**
* Remove the specified order.
*/
public function destroy(int $id)
{
$order = Order::where('tenant_id', tenant_id())
->findOrFail($id);
$order->delete();
return redirect()
->route('order-tracker.index')
->with('success', 'Order deleted successfully.');
}
}API Controller Structure
<?php
namespace Modules\OrderTracker\Http\Controllers\Api;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
use Illuminate\Routing\Controller;
use Modules\OrderTracker\Models\Order;
class OrderApiController extends Controller
{
/**
* Display a listing of orders.
*/
public function index(): JsonResponse
{
$orders = Order::where('tenant_id', tenant_id())
->latest()
->paginate(20);
return response()->json($orders);
}
/**
* Store a newly created order.
*/
public function store(Request $request): JsonResponse
{
$validated = $request->validate([
'order_number' => 'required|string|unique:orders',
'total' => 'required|numeric|min:0',
'status' => 'required|string|in:pending,processing,completed',
]);
$order = Order::create([
'tenant_id' => tenant_id(),
'order_number' => $validated['order_number'],
'total' => $validated['total'],
'status' => $validated['status'],
]);
return response()->json($order, 201);
}
/**
* Display the specified order.
*/
public function show(int $id): JsonResponse
{
$order = Order::where('tenant_id', tenant_id())
->findOrFail($id);
return response()->json($order);
}
/**
* Update the specified order.
*/
public function update(Request $request, int $id): JsonResponse
{
$order = Order::where('tenant_id', tenant_id())
->findOrFail($id);
$validated = $request->validate([
'total' => 'numeric|min:0',
'status' => 'string|in:pending,processing,completed',
]);
$order->update($validated);
return response()->json($order);
}
/**
* Remove the specified order.
*/
public function destroy(int $id): JsonResponse
{
$order = Order::where('tenant_id', tenant_id())
->findOrFail($id);
$order->delete();
return response()->json(null, 204);
}
}Request Validation
Form Request Classes
Create dedicated request validation classes for complex validation logic:
<?php
namespace Modules\OrderTracker\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class StoreOrderRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return $this->user()->can('create-orders');
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'order_number' => 'required|string|unique:orders',
'total' => 'required|numeric|min:0',
'status' => 'required|string|in:pending,processing,completed',
'items' => 'required|array|min:1',
'items.*.product_id' => 'required|exists:products,id',
'items.*.quantity' => 'required|integer|min:1',
'items.*.price' => 'required|numeric|min:0',
];
}
/**
* Get custom messages for validator errors.
*/
public function messages(): array
{
return [
'order_number.required' => 'Order number is required.',
'items.required' => 'Order must contain at least one item.',
];
}
}Usage in controller:
use Modules\OrderTracker\Http\Requests\StoreOrderRequest;
public function store(StoreOrderRequest $request)
{
$validated = $request->validated();
$order = Order::create([
'tenant_id' => tenant_id(),
...$validated
]);
return redirect()
->route('order-tracker.show', $order->id)
->with('success', 'Order created successfully.');
}Response Patterns
Web Responses
Redirect with flash message:
return redirect()
->route('order-tracker.index')
->with('success', 'Order created successfully.');Redirect with errors:
return redirect()
->back()
->withErrors(['error' => 'Unable to create order.'])
->withInput();View response:
return view('ordertracker::index', [
'orders' => $orders,
'statistics' => $statistics,
]);API Responses
Success response (200):
return response()->json([
'message' => 'Order retrieved successfully.',
'data' => $order,
], 200);Created response (201):
return response()->json([
'message' => 'Order created successfully.',
'data' => $order,
], 201);No content response (204):
return response()->json(null, 204);Error response (4xx):
return response()->json([
'message' => 'Order not found.',
'errors' => [],
], 404);Validation error response (422):
return response()->json([
'message' => 'Validation failed.',
'errors' => $validator->errors(),
], 422);Tenant-Aware Operations
Critical Tenant Scoping
All database queries must be scoped to the current tenant:
// Correct - tenant-scoped query
$orders = Order::where('tenant_id', tenant_id())
->get();
// Incorrect - exposes all tenant data
$orders = Order::all();Using Tenant Helper
use function App\Helpers\tenant_id;
public function index()
{
$tenantId = tenant_id();
$orders = Order::where('tenant_id', $tenantId)
->latest()
->paginate(20);
return view('ordertracker::index', compact('orders'));
}Route Caching
Generate Route Cache
php artisan route:cacheClear Route Cache
php artisan route:clearImportant: Clear route cache after adding or modifying routes.
Testing Routes
List All Routes
php artisan route:listFilter Module Routes
php artisan route:list --path=order-trackerCheck Specific Route
php artisan route:list --name=order-tracker.indexNext Steps
- Views & Translations - Create Blade templates and translations
- Hooks & Events - Integrate with hook system
- Helper Functions - Use module utilities
This guide is for WhatsMarkSaaS v2.0.0+ custom module development.