Routing
Introduction
Doppar’s routing system, available through the Phaseolies\Support\Facades\Route namespace and using Phaseolies\Utilities\Attributes\Route attributes, provides a clean, expressive way to define your application’s URL structure and map it to the appropriate controller actions or closures.
It now supports two routing approaches: defining routes traditionally within files like web.php, or using attribute-based route declarations placed directly above controller methods. This flexibility allows developers to choose the style that best fits their project’s structure and development workflow.
Doppar’s routing engine offers features such as route prefix grouping, named routes, throttling, middleware assignment, and RESTful resource routing. Whether you’re building a lightweight API or a complex web application, Doppar’s routing system ensures your code remains consistent, maintainable, and scalable.
Supported HTTP Methods
Doppar supports the following HTTP methods for defining routes:
| Method | Description |
|---|---|
| GET | Retrieves data from the server. Commonly used for fetching resources. |
| POST | Submits new data to the server. Typically used for creating new records. |
| PUT | Replaces existing data with new data. Used for full updates. |
| PATCH | Partially updates existing data. Ideal for minor changes to a resource. |
| DELETE | Removes data from the server. |
| HEAD | Same as GET but returns only headers. Useful for checking existence or metadata. |
| OPTIONS | Describes available communication options. Often used for CORS preflight checks. |
Route Caching
Doppar supports route caching for improved performance in production environments. Route caching is controlled by the APP_ROUTE_CACHE environment variable: Set to true to enable route caching (recommended for production). Set to false to disable (default for development) Route cache files are stored in: storage/framework/cache/.
Cache Routes
The route:cache command is used to compile your application's routes into a single, cached file. This improves performance by significantly speeding up route registration, especially in production environments.
When the route cache is enabled, the framework loads the routes from the generated cache file instead of parsing all route definitions on each request. This reduces overhead and improves response times.
To create the route cache, run:
php pool route:cacheIt's important to ensure that all of your routes are working correctly before caching them. If there are any issues or syntax errors in your route definitions, the command will fail and output the relevant error.
Clear Route Cache
The route:clear command is used to remove the route cache file. This is useful when you have made changes to your application's routes and want to ensure the framework uses the updated definitions.
In a production environment, route caching improves performance by loading a precompiled route list. However, if the route cache becomes outdated or corrupted, it can lead to unexpected behavior. Running this command will delete the cached route file, forcing the application to load routes directly from the source files on the next request.
You should run this command after deploying any updates that modify your routes:
php pool route:clearUse this when making route changes in production or if experiencing route-related issues.
Attribute Based Routing
Doppar also supports attribute-based routing, allowing you to define routes directly above your controller methods using PHP 8 attributes. This approach offers a cleaner, more localized way to declare routes, keeping route definitions close to the logic they handle. It reduces the need to manage separate route files for simple or self-contained controllers, improving code readability and maintainability
Each attribute-based route can specify its path, name, HTTP methods, and other configurations, just like traditional route definitions. The Doppar routing engine automatically detects and registers these routes when your application boots.
When defining a route using the Route attribute, only the first parameter — the route path — is mandatory.
The path specifies the URI that the route should respond to, while all other parameters such as name, methods, middleware, are optional and can be included as needed. This makes it simple to define quick routes with minimal configuration when defaults are sufficient.
Example:
<?php
namespace App\Http\Controllers;
use Phaseolies\Utilities\Attributes\Route;
class UserController extends Controller
{
#[Route(uri: 'user')]
public function index()
{
// Handles GET /user
}
}Now you can view the details of your registered routes by running the following pool command:
php pool route:listIn this example, only the route uri ('user') is provided. Doppar will automatically assume default values for other parameters — such as using the GET method and no assigned name or middleware.
See the below example with handling incoming HTTP methods
<?php
namespace App\Http\Controllers;
use Phaseolies\Utilities\Attributes\Route;
class PostController extends Controller
{
#[Route(uri: 'posts', methods: ['GET'])]
public function index()
{
//
}
}In this example, the index method is mapped to the /posts endpoint, responding to GET requests.
In addition to GET requests, attribute-based routing in Doppar supports all common HTTP methods such as POST, PUT, PATCH, DELETE etc. This allows developers to define full RESTful endpoints directly within controllers, without relying solely on external route files.
See the below example with methods and name
<?php
namespace App\Http\Controllers;
use Phaseolies\Utilities\Attributes\Route;
class PostController extends Controller
{
#[Route(uri: 'posts', methods: ['GET'], name: 'post.index')]
public function index()
{
//
}
}In this example, the index method is mapped to the /posts endpoint, responding to GET requests pointing to route name post.list.
Each route can declare one or multiple HTTP methods using the methods parameter, giving fine-grained control over how requests are handled.
#[Route(uri: 'post/store', methods: ['POST', 'PATCH'])]
public function store(Request $request)
{
// Handle creating or updating a post
}In this example, the store method will respond to both POST and PATCH requests sent to the /post/store endpoint. This flexibility makes it easy to manage different request types for the same route, supporting both resource creation and partial updates within a single controller action.
Attribute Routing with Middleware
Doppar’s attribute-based routing also supports middleware assignment directly within the route definition. This allows you to apply one or more middleware layers to a specific controller method without configuring them separately in a route file.
By specifying the middleware parameter inside the Route attribute, you can easily protect routes, apply request filters, or run any preprocessing logic before the controller action executes. Middleware are executed in the order they are listed, ensuring full control over the request lifecycle.
#[Route(
uri: '/post/store',
methods: ['POST', 'PATCH'],
name: 'post.store',
middleware: ['auth', 'admin']
)]
public function store(Request $request): Response
{
// Handle creating or updating a post
}In this example, the /post/store route is protected by the auth and admin middleware. The request must first pass both middleware checks before the controller’s store method is executed, ensuring secure and validated access to the route.
Note: When using the Route attribute, you must pass registered middleware names, not class references.
For example, passing middleware: ['auth'] will work correctly if auth is a middleware alias registered in your application’s middleware configuration.
However, passing middleware: [Authenticate::class] will not work, as attribute-based routing expects middleware names rather than class references.
If you need to use class-based middleware, apply them through the dedicated #[Middleware(...)] attribute instead.
Passing Parameters to Middleware in Attribute Routing
When using attribute-based routing in Doppar, you can enhance your routes by passing parameters directly to middleware. This feature allows attributes and middleware to work seamlessly together, giving you expressive, method-level control over your route behavior.
By defining middleware and their parameters right within the route attribute, your controller logic stays clean, self-contained, and easy to understand — with all route configurations centralized in one place.
Example:
#[Route(
uri: '/user',
name: 'user.index',
methods: ['GET', 'POST'],
middleware: ['auth', 'response.break:admin']
)]
public function __invoke()
{
//
}In this example, the response.break middleware receives the parameter admin, demonstrating how you can pass dynamic configuration directly through attributes.
💡 Learn more about passing parameters to middleware middleware-parameters
Routing with Rate Limit
Though rate limiting can be implemented using middleware, it can now be defined directly within the route attributes.
See the example of rate limiting using middleware
#[Route(
uri: 'home',
methods: ['GET'],
middleware: ['throttle:10,1']
)]
public function home(): Response
{
//
}In this example, the throttle middleware restricts access to 10 requests per minute.
Now we can also implement this by passing rateLimit and rateLimitDecay naming params like this way
#[Route(
uri: 'home',
methods: ['GET'],
rateLimit: 3, // Allow up to 3 requests
rateLimitDecay: 1, // Within 1 minute
)]
public function home(): Response
{
//
}This approach eliminates the need to manually specify middleware for simple throttling needs.
Routing with Route Facades
The most basic Doppar routes accept a URI and a closure, providing a very simple and expressive method of defining routes and behavior without complicated routing configuration files:
<?php
use Phaseolies\Support\Facades\Route;
Route::get('/', fn() => "Welcome to Doppar");The Default Route Files
All Doppar routes are defined in your route files, located within the routes directory. These files are automatically loaded by the framework. The routes/web.php file is dedicated to defining routes for your web interface and is assigned the web middleware group, which enables essential features like session management and CSRF protection.
For most Doppar applications, you will start by defining routes inside the routes/web.php file. Any route declared in this file can be accessed through its corresponding URL in the browser. For example, the following route can be accessed by visiting http://example.com/user in your browser:
use App\Http\Controllers\UserController;
Route::get('user', [UserController::class, 'index']);API Routes
Doppar provides separete api routes file localted in routes/api.php.
You can use Doppar flarion, which provides a robust, yet simple API token authentication system which can be used to authenticate third-party API consumers, or mobile applications.
use Phaseolies\Support\Facades\Route;
use Phaseolies\Http\Request;
Route::get('user', function (Request $request) {
return $request->user();
})->middleware('auth-api');
// Endpoint
// http://example.com/api/userDependency Injection
You may type-hint any dependencies required by your route directly within the route’s callback signature. Doppar’s service container will automatically resolve and inject the appropriate instances for you. For example, you can type-hint the Phaseolies\Http\Request class to have the current HTTP request automatically injected into your route callback:
use Phaseolies\Http\Request;
Route::post('payment', function (Request $request) {
//
});CSRF Protection
Keep in mind that any HTML forms targeting routes using the POST, PUT, PATCH, or DELETE methods—defined in the web.php routes file—must include a CSRF token field. Without this token, Doppar will reject the request for security reasons. To learn more, refer to the CSRF protection documentation.
<form method="POST" action="{{ route('profile') }}">
@csrf
...
</form>Handling Modified Request Route
In Doppar, when you need to handle PUT, PATCH, or DELETE requests (typically for updating or deleting data), you must follow a few conventions to make it work properly with HTML forms.
The HTTP methods PUT, PATCH, and DELETE define the intended action on a resource in RESTful APIs or web applications. Here's what each one does when a request is made:
| Method | Action | Body Required | Idempotent | Common Use Case |
|---|---|---|---|---|
| PUT | Full replace of resource | ✅ Yes | ✅ Yes | Update full user record |
| PATCH | Partial update of resource | ✅ Yes | ✅ Usually | Change a single field |
| DELETE | Remove resource | ❌ Usually no | ✅ Yes | Delete an item or record |
HTTP Verb Spoofing in Forms
Since HTML forms only support GET and POST methods directly, Doppar provides Blade directives to spoof other HTTP methods like PUT, PATCH, and DELETE.
Here’s how you do it in your form:
<form method="POST" action="{{ route('update-profile') }}">
@csrf
@method('PUT') {{-- For PUT Request --}}
{{-- @method('PATCH') For PATCH Request --}}
{{-- @method('DELETE') For DELETE Request --}}
<button type="submit">Submit</button>
</form>WARNING
Always include @csrf to protect against CSRF attacks. The @method directive tells Doppar to treat the request as the specified HTTP verb.
Any Route
The Route::any() method is used to register a route that responds to any HTTP method (GET, POST, PUT, DELETE, etc.). This is particularly useful for routes where the HTTP method doesn’t matter, such as catch-all pages, testing endpoints, or webhook receivers.
Route::any('welcome*', fn() => 'welcome');The * acts as a wildcard, so this route matches any URL starting with welcome (e.g., /welcome, /welcome-home, /welcome123, welcome/hello. This handle multiple methods without defining them separately.
Defining Modified Request Routes
Once your form is set up to spoof PUT, PATCH, or DELETE methods, you need to define the corresponding routes in your routes/web.php file. These routes will map the specific HTTP methods to the appropriate controller actions.
In Doppar, this is typically done using the Route::put, Route::patch, and Route::delete methods provided by the routing system.
use App\Http\Controllers\ProfileController;
// PUT Route
Route::put('update-profile', [ProfileController::class, 'update']);
// PATCH Route
Route::patch('update-profile', [ProfileController::class, 'update']);
// DELETE Route
Route::delete('user/{id}', [ProfileController::class, 'delete']);Route Parameters
Sometimes you will need to capture segments of the URI within your route. For example, you may need to capture a user's ID from the URL. You may do so by defining route parameters:
Route::get('user/{id}', function (string $id) {
return 'User '.$id;
});You may define as many route parameters as required by your route:
Route::get('posts/{post}/comments/{comment}', function (
string $postId, string $commentId
) {
echo $postId;
echo $commentId;
});Route parameters in Doppar are defined by wrapping the parameter name in curly braces {} and should consist of alphabetic characters. You may also use underscores (_) in the parameter names. These parameters are automatically passed into your route callbacks or controller methods based on their position in the route — the actual variable names in the callback or method signature do not need to match the parameter names in the route.
Named Routes
Doppar support convenient naming route structure. Named routes allow the convenient generation of URLs or redirects for specific routes. You may specify a name for a route by chaining the name method onto the route definition:
use Phaseolies\Support\Facades\Route;
use App\Http\Controllers\UserController;
Route::get('user/{id}/{name}', [UserController::class, 'profile'])
->name('profile');Now use this naming route any where using route() global method.
<form action="{{ route('profile', ['id' => 2, 'name' => 'abc']) }}"
method="post">
@csrf
<button type="submit" class="btn btn-primary">Submit</button>
</form>If there is single param in your route, just use
Route::get('user/{id}', [UserController::class, 'profile'])
->name('profile');Now call the route
{{ route('profile', $user->id) }}Route names should always be unique.
Generating URLs to Named Routes
Once you have assigned a name to a given route, you may use the route's name when generating URLs or redirects via Doppar's route and redirect helper functions:
// Generating URLs...
$url = route('profile');
// Generating Redirects...
return redirect()->route('profile');Defining Bundle Routes
The bundle method allows you to register a complete set of CRUD routes for a controller. You can customize which routes are created and the names assigned to them using the only, except, and names options.
Example
use Phaseolies\Support\Router\Route;
use App\Http\Controllers\ProductController;
Route::bundle('products', ProductController::class);Generated route for this products bundle
| Method | URI | Action | Name |
| ------ | -------------------------- | -------------------------- | ----------------- |
| GET | `/products` | `ProductController@index` | `products.index` |
| GET | `/products/create` | `ProductController@create` | `products.create` |
| POST | `/products` | `ProductController@store` | `products.store` |
| GET | `/products/{id}/show` | `ProductController@show` | `products.show` |
| GET | `/products/{id}/edit` | `ProductController@edit` | `products.edit` |
| PUT | `/products/{id}/update` | `ProductController@update` | `products.update` |
| DELETE | `/products/{id}/delete` | `ProductController@delete` | `products.delete` |By default, bundle routes use the primaryKey field as the route key for model binding along with fallback to id.
If your model does not have an id column, or you want to bind routes using a different column (for example, slug), you can override the getRouteKeyName() method in your Entity model.
/**
* Get the route key name for model binding.
*
* @return string
*/
#[\Override]
public function getRouteKeyName(): string
{
return 'slug';
}Now, instead of resolving routes by default primaryKey, Doppar will automatically resolve models using the slug field.
Bundle Model & Controller Naming Convention
If you use custom route key binding, then you must follow this model and controller naming convention. It is important to follow a consistent naming convention for models and controllers. This ensures that automatic model resolution and route binding work correctly.
Controller → Model Mapping
| Controller | Model |
|----------------------------------|---------------------|
| `ProductController` | `Product` |
| `ProductDetailsController` | `ProductDetails` |You must follow this convention before using bundle routes
If you use custom route key binding. This ensures that Doppar can correctly infer the corresponding controller from the model and vice versa.
Bundle Routes with Options
The bundle method registers a full set of CRUD routes for a given controller. You can customize which routes are generated and what names they use by passing the only, except, names and method options.
Example
use App\Http\Controllers\ProductController;
use Phaseolies\Support\Router\Route;
Route::bundle('products', ProductController::class, [
'only' => ['index', 'show', 'update'],
'names' => [
'index' => 'products.all',
'show' => 'products.view'
],
'methods' => [
'update' => 'POST'
]
]);Explanation:
- The only option ensures that only the
index,showandupdateroutes are registered. - The names option
overridesthedefault route nameswith custom ones. - Now the
updateendpoint usesPOSTrather thanPUT
Generated Routes:
| Method | URI | Action | Name |
| ------ | --------------------- | ------------------------- | --------------- |
| GET | `/products` | `ProductController@index` | `products.all` |
| GET | `/products/{id}/show` | `ProductController@show` | `products.view` |API Bundle Routes
The apiBundle method works like bundle, but it excludes the create and edit routes. This is ideal for API endpoints, where HTML form views are not needed, and you only want routes for data operations.
Example:
use App\Http\Controllers\InviteController;
use Phaseolies\Support\Router\Route;
Route::apiBundle('posts', InviteController::class);Example with Options:
Route::apiBundle('posts', InviteController::class, [
'only' => ['index', 'show']
]);Generated Routes:
| Method | URI | Action | Name |
| ------ | ---------------------- | ------------------------ | ------------ |
| GET | `/api/posts` | `InviteController@index` | `posts.index`|
| GET | `/api/posts/{id}/show` | `InviteController@show` | `posts.show` |Reminder on API Usage
When you are building APIs that accept file uploads, this note is important:
📌 Reminder on API Usage
If you are uploading files using form-data rather than x-www-form-urlencoded, Doppar does not support file handling for PUT and PATCH requests. Always use POST request when your API endpoint accepts files. Using PUT/PATCH with form-data uploads may cause unexpected issues.
By default, Doppar’s Route::apiBundle will generate a PUT request for the update action. Since PUT does not support file uploads for form-data, you can override it to POST:
Route::apiBundle('file', FileController::class, [
'methods' => [
'update' => 'POST' // Now the update endpoint uses POST
]
]);This ensures your update endpoint can handle file uploads without issues.
Nested Bundle Routes
The nestedBundle method registers a full CRUD route set for a child resource that is nested under a parent resource. It automatically includes the parent resource parameter in the URI, making it easy to work with relationships such as “posts → comments” or “categories → products”.
Example of Nested Bundle
use App\Http\Controllers\CommentController;
use Phaseolies\Support\Router\Route;
Route::nestedBundle('posts', 'comments', CommentController::class);Example Controller
<?php
namespace App\Http\Controllers;
use Phaseolies\Http\Request;
use App\Http\Controllers\Controller;
class CommentController extends Controller
{
public function index($postId) {}
public function create($postId) {}
public function store(Request $request, $postId) {}
public function show($postId, $id) {}
public function edit($postId, $id) {}
public function update(Request $request, $postId, $id) {}
public function delete($postId, $id) {}
}Generated Routes
| Method | URI | Action | Name |
| ------ | ----------------------------------- | -------------------------- | ----------------------- |
| GET | `/posts/{post}/comments` | `CommentController@index` | `posts.comments.index` |
| GET | `/posts/{post}/comments/create` | `CommentController@create` | `posts.comments.create` |
| POST | `/posts/{post}/comments` | `CommentController@store` | `posts.comments.store` |
| GET | `/posts/{post}/comments/{id}/show` | `CommentController@show` | `posts.comments.show` |
| GET | `/posts/{post}/comments/{id}/edit` | `CommentController@edit` | `posts.comments.edit` |
| PUT | `/posts/{post}/comments/{id}/update`| `CommentController@update` | `posts.comments.update` |
| DELETE | `/posts/{post}/comments/{id}/delete`| `CommentController@delete` | `posts.comments.delete` |You can also pass options in nested bundle
Route::nestedBundle('posts', 'comments', CommentController::class, [
'except' => ['create', 'edit']
]);
Note:Bundle routes do not support method chaining like->middleware('auth'). To apply middleware, use attribute-based middleware calling directly in your bundle controller.
Route Group
Route::group is used to group multiple routes under a shared configuration like URL prefix. This helps in organizing routes cleanly and applying common logic to them.
Route::group([
'prefix' => 'your-prefix'
], function () {
// Routes go here
});Example
Look at the below example, we are using prefix as a group route.
Route::group(['prefix' => 'login'], function () {
Route::get('/action', function () {
// Matches The "/login/action" URL
});
});'prefix' => 'login' This means all routes inside this group will be prefixed with /login.
Route Middleware
Middleware in Doppar provides a convenient mechanism for filtering or modifying HTTP requests as they enter your application. Route middleware is typically used to perform tasks such as authentication, logging, CORS handling, input sanitization, and more — before the request reaches the controller or route logic.
You can assign middleware to routes in two primary ways:
Assigning Middleware to Individual Routes
You can attach middleware directly to a specific route using the middleware method. This allows you to apply custom logic, such as authentication or request throttling, to just that route.
Route::get('dashboard', function () {
// Only accessible to authenticated users
})->middleware('auth');In this example, the auth middleware ensures that only authenticated users can access the dashboard route. If a user is not authenticated, they will be redirected or denied access based on your application's configuration.
You may also chain multiple middleware by passing them as an array:
Route::post('settings', function () {
// Protected by multiple middleware
})->middleware(['auth', 'verified']);This approach gives you fine-grained control over access and behavior at the route level.
You can assign middleware directly to a specific route using the middleware method. This lets you apply route-level behavior such as authentication, authorization, or request handling as multiple string arguments like this way.
Route::post('settings', function () {
// Protected by multiple middleware
})->middleware('auth', 'verified');Both approaches are fully supported in Doppar and function the same. Use whichever style best fits your project's conventions or coding preferences.
Route Redirection
The Route::redirect() method allows you to define quick and clean route redirections in your application. Whether you're migrating old URLs, setting up aliases, or redirecting to external destinations, this method provides a declarative and consistent way to do so.
Syntax
Route::redirect($from, $to, $status = 302);$from— The original URI pattern to match.$to— The target location: can be a URL, a named route, or an external link.$status— Optional HTTP status code (default: 302 Found). Use 301 for permanent redirects.
Redirecting to a New URL
Route::redirect('old', 'new', 301);Redirecting to a Named Route
Route::redirect('legacy', 'user.profile');Redirecting to an External URL
Route::redirect('blog', 'https://doppar.com');Route Helper Method
The Route Facades provide convenient ways to interact with the current request and route information in your application. They allow you to easily retrieve route names, check if a route exists, determine the current route or middleware, and inspect the controller or action handling the request.
These helpers are designed to make it easier to write clean and expressive logic inside controllers, middleware, or services without manually parsing routes.
Get All Route Names
The getRouteNames() method returns an array of all the named routes registered in your application. This is useful when you want to inspect or debug available route names, or programmatically loop through them.
<?php
namespace App\Http\Controllers;
use Phaseolies\Support\Facades\Route;
use App\Http\Controllers\Controller;
class ProductController extends Controller
{
public function index()
{
return Route::getRouteNames();
}
}If you have routes defined like this:
Route::get('/products', [ProductController::class, 'index'])->name('products.index');
Route::get('/products/{id}', [ProductController::class, 'show'])->name('products.show');
Route::post('/products', [ProductController::class, 'store'])->name('products.store');Then calling /products (with the controller above) will return something like:
{
"products.index": "/products",
"products.show": "/products/{id}",
"products.store": "/products"
}Get Route Middlewares
The getCurrentMiddlewareNames() method returns an array of all middleware names that are applied to the current request’s route. This is helpful when you want to check which middlewares are active for the request being processed.
Route::getCurrentMiddlewareNames();If the current route does not have any middleware applied, the method will return null.
Checking Route Name Existance
The has() method checks if a named route exists in your application. It returns a boolean (true or false) depending on whether the given route name has been registered.
This is useful when you need to confirm the existence of a route before generating URLs or performing route-specific logic.
if (Route::has('products.index')) {
return "Route 'products.index' exists!";
}
return "Route 'products.index' does not exist.";Check Current Route Matching
The is() method checks if the current request’s route matches the given route name. It compares the request path against the named route definition.
if (Route::is('products.index')) {
return "You are on the products index page.";
}
return "This is not the products index page.";Get Current Route Name
The currentRouteName() method returns the name of the current route being executed. If no matching named route is found, it will return null.
This is especially useful when you need to check or display the current route name (e.g., for navigation highlighting or conditional logic).
Route::currentRouteName();Get Current Route Action
The currentRouteAction() method returns the action associated with the current route. The action can be returned in one of the following forms:
String:ControllerClass@methodClosure:ClosureArray:[ControllerClass, 'method'] (converted to stringControllerClass@method)Null:if no callback is found
This is useful for debugging, logging, or conditionally checking which controller or action is handling the current request.
Example
Route::currentRouteAction();You will get the response like this
App\Http\Controllers\ProductController@indexCheck Current Route Uses Specific Controller
The currentRouteUsesController() method checks if the current route is handled by a specific controller class. It returns true if the current route’s action belongs to the given controller, and false otherwise.
Check if the current route uses a specific controller
<?php
namespace App\Http\Controllers;
use Phaseolies\Support\Facades\Route;
use App\Http\Controllers\Controller;
class ProductController extends Controller
{
public function index()
{
if (Route::currentRouteUsesController(ProductController::class)) {
return "This route is handled by ProductController.";
}
return "This route is handled by another controller.";
}
}This is useful for conditional logic that depends on the controller handling the current request, such as middleware checks, logging, or dynamic UI adjustments.
