Skip to content

🧭 Routing in PageBlocks ​

Routes define which code should execute when a specific URL is accessed on your site.

Example: if a user visits /about, you can set up a route to display the "About Us" page.

🔰 Basic Example ​

PageBlocks uses the Route facade to define routes:

php
use Boshnik\PageBlocks\Facades\Route;

Route::get('/about', function () {
    return 'This is the About page';
});

📥 When someone opens https://example.com/about, this function runs, and the user sees its output.

You can return the following from a route:

TypeResult
stringPlain text
arrayJSON
objectJSON or HTML (depending on content and headers)
ResponseExplicit response object using the Response class

Everything returned from a route automatically passes through Response::dispatchResponse, which determines how to properly send the result to the client.

📂 Where Are Routes Located? ​

All routes are stored in the core/App/routes folder of your project:

core/
└── App/
    └── routes/
        ├── web.php
        ├── api.php
        └── ...

🔄 All .php files in this folder are automatically loaded, and their routes are registered when the application starts.

📝 You can split routes into multiple files:

  • web.php — Main website routes
  • api.php — API routes
  • admin.php, auth.php, blog.php — Any additional route groups

The filename doesn’t matter as long as it’s in core/App/routes and has a .php extension. One exception: a file whose name starts with _ is skipped, which is a handy way to park a set of routes without deleting it.

⚙️ Who handles the front end — pageblocks_routing ​

The system setting decides how much of the site the component's router owns. It ships as full:

ValueIn the comboWhat happens
fullFull APIThe router sees every front-end request. The shipped core/App/routes/web.php ends with Route::fallback('ResourceController@index'), so a URI that matches no route of yours is still rendered by PageBlocks. Remove that fallback and an unmatched URI becomes a 404.
routesRoute OnlyThe router sees the request, but a URI that matches nothing (and has no fallback) is handed back to MODX, which resolves it as an ordinary resource.
offDisabledThe front end is left to MODX entirely.

off does not switch the component off — only its front end

Requests under mgr/ keep going through the router even with off. They have to: the manager's own pages, the moderation menu item and the dashboard widget call /mgr/pb/* on every manager page, and handing those to the MODX resource resolver returns an HTML 404 where ExtJS expects JSON — which takes the manager down, not just one grid.

Two more settings, both off by default:

SettingDefaultWhat it does
pageblocks_route_cachefalseCaches the compiled FastRoute dispatcher. The cache is dropped by itself when a route file is newer than it.
pageblocks_strict_routesfalseMakes a URL that route() cannot build throw instead of only logging — for development.

⚙️ Route Methods ​

Each route is associated with an HTTP method—the way a browser or client sends a request to the server. Here are the main methods:

📄 Route::get($uri, $callback) ​

Responds to GET requests—when a user simply opens a page.

php
Route::get('/contact', function () {
    return 'Contact information';
});

📩 Route::post($uri, $callback) ​

Handles POST requests—commonly used when submitting forms.

php
Route::post('/feedback', function () {
    return 'Thank you for your feedback!';
});

📝 Route::put($uri, $callback) ​

Used for full resource updates.

php
Route::put('/profile', function () {
    return 'Profile updated';
});

🔧 Route::patch($uri, $callback) ​

Partial resource updates—for example, changing a single field.

php
Route::patch('/profile/email', function () {
    return 'Email updated';
});

🗑 Route::delete($uri, $callback) ​

Deletes a resource.

php
Route::delete('/account', function () {
    return 'Account deleted';
});

⚙️ Route::options($uri, $callback) ​

Handles OPTIONS requests—useful for CORS and APIs.

php
Route::options('/api/data', function () {
    return response('', 204)
        ->header('Allow', 'GET, POST, OPTIONS')
        ->header('Access-Control-Allow-Origin', '*')
        ->header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS')
        ->header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
});

🔀 Route::match(array $methods, string $uri, $callback) ​

Defines a route that responds to multiple methods.
For example, if the same URL should handle both GET and POST:

php
Route::match(['GET', 'POST'], '/subscribe', function () {
    return 'Subscription form or its processing';
});

👆 This route will work for:

  • Opening /subscribe in a browser (GET);
  • Submitting a form to the same URL (POST).

Methods must be in uppercase (GET, POST, PUT, etc.).

🧮 Route::any(string $uri, $callback) ​

Responds to all HTTP request types: GET, POST, PUT, DELETE, PATCH, OPTIONS.

php
Route::any('/ping', function () {
    return 'pong';
});

This route will work regardless of the client’s method—useful if the request type doesn’t matter.

🧱 Route::crud(string $uri, $controller) ​

Declares a whole set of routes for one controller in a single line:

php
Route::crud('mgr/pb/constructor/blocks', BlockController::class);

That expands to eleven routes:

MethodURIAction
GET{uri}index
POST{uri}create
GET{uri}/{id}show — id constrained to [0-9]+
POST{uri}/{id}update — same constraint
POST{uri}/removeremove
POST{uri}/restorerestore
POST{uri}/deletedelete
POST{uri}/enableenable
POST{uri}/disabledisable
POST{uri}/copycopy
POST{uri}/sortsort

The numeric constraint on {id} is what keeps {uri}/remove from being swallowed by {uri}/{id}.

Middleware applies to all eleven ​

php
Route::crud('mgr/pb/constructor/blocks', BlockController::class)
    ->middleware(['modauth', 'SudoOnly']);

This is the whole point of the helper

A chained ->middleware() after crud() attaches to every route in the batch, not only to the last one declared.

It has to work that way. The alternative — the guard landing on one route out of eleven — does not fail loudly: the listed endpoint still asks for auth, so the set looks secured, while POST {uri}/delete sits wide open next to it. Declaring a CRUD set and guarding it are one action, so they are one call.

->withoutMiddleware() works over the batch in the same way, if you need to lift something the enclosing group applied.

🔁 Redirect Routes ​

Sometimes, instead of processing a request, you just need to redirect the user to another URL. Use the redirect method for this.

🔁 Route::redirect($from, $to, $status = 301) ​

php
Route::redirect('/old-link', '/new-link');

👆 Anyone who opens /old-link will be automatically redirected to /new-link.

By default, this uses status code 301 (permanent redirect), but you can specify another:

php
Route::redirect('/moved', '/here', 302); // temporary redirect

Only the first declaration for a path wins — a second redirect() to the same path is a duplicate static route, and FastRoute fails on the whole set rather than on that one route.

📦 Useful for site reorganization or URL renaming.

🖼 Quick Route for Displaying a Template ​

If you just need to display a template without logic, use the Route::view method.

🖼 Route::view($uri, $view, $data = []) ​

php
Route::view('/about', 'pages/about');

👆 This route will render the pages/about.tpl template.

You can also pass data:

php
Route::view('/thanks', 'pages/thanks', ['title' => 'Thank you for your order']);

Internally, this method calls response()->view(...), so the result is identical to manually returning a template.

🧩 Route Parameters ​

Routes can contain variables—parameters that are automatically extracted from the URL and passed to the handler.

🎯 Required Parameters ​

php
Route::get('/user/{id}', function ($id) {
    return "User profile #$id";
});

👆 Visiting /user/42 will output: User profile #42.

❔ Optional Parameters ​

To make a parameter optional, add ? and specify a default value:

php
Route::get('/user/{name?}', function ($name = 'Guest') {
    return "Hello, $name!";
});

📥 /user → Hello, Guest!
📥 /user/Anna → Hello, Anna!

🔍 Parameter Constraints — where() ​

Allows you to set regular expressions to validate parameters:

php
Route::get('/user/{id}', function ($id) {
    return "User #$id";
})->where('id', '\\d+');

👆 Now /user/abc won’t work—only digits (\d+).

Constraints can be set for multiple parameters:

php
Route::get('/post/{slug}/{id}', function ($slug, $id) {
    return "$slug: $id";
})->where([
    'slug' => '[a-z\-]+',
    'id' => '\\d+',
]);

📦 Accessing Request Inside a Route ​

To access request data, use dependency injection with Request:

php
use Boshnik\PageBlocks\Http\Request;

Route::post('/submit', function (Request $request) {
    $email = $request->input('email');
    return "You entered: $email";
});

🧃 Named Routes ​

Named routes are useful for generating links and redirects:

php
Route::get('/profile/{id}', function ($id) {
    return "Profile #$id";
})->name('profile.show');

Now you can generate a link to this route:

php
route('profile.show', ['id' => 7]); // /profile/7

🧭 This is especially helpful when generating links in templates or using redirects:

php
response()->redirect(route('profile.show', ['id' => $user->id]));

📦 Route Groups ​

The group() method lets you group routes and apply shared settings: prefix, middleware, controller, etc.

🔹 Basic Example ​

php
Route::group(function () {
    Route::get('/a', fn() => 'A');
    Route::get('/b', fn() => 'B');
});

Routes inside the group will be registered normally—without a prefix or additional parameters.

🏁 Prefix ​

Adds a common prefix to all routes in the group:

php
Route::prefix('/admin')->group(function () {
    Route::get('/dashboard', fn() => 'Dashboard');
    Route::get('/users', fn() => 'Users');
});

📥 Registered routes:

  • /admin/dashboard
  • /admin/users

🛡 Shared Middleware ​

php
Route::middleware('auth')->group(function () {
    Route::get('/cabinet', fn() => 'Personal account');
});

All routes in the group will be processed via the auth middleware.

👨‍🏫 Default Controller ​

Sets a shared controller for routes in the group:

php
Route::controller(ProfileController::class)->group(function () {
    Route::get('/edit', 'edit');
    Route::post('/update', 'update');
});

🔁 Calls the methods:

  • ProfileController->edit()
  • ProfileController->update()

🛡 Middleware ​

Middleware are intermediate handlers that run before the controller or closure executes. Examples include auth checks, CSRF protection, etc.

📌 Usage Example ​

php
Route::get('/dashboard', fn() => 'Dashboard')->middleware('auth');

🧩 Multiple Middleware ​

php
Route::middleware(['auth', 'admin'])->group(function () {
    Route::get('/admin', fn() => 'Admin');
});

🚫 Excluding Middleware ​

You can disable middleware applied earlier:

php
Route::get('/open', fn() => 'Open')->withoutMiddleware('auth');

🧾 Where Are Aliases Configured? ​

Middleware can be referenced by names (aliases) or by bare class names. Aliases are configured in core/App/bootstrap/app.php:

php
'middleware' => [
    'auth' => Authenticate::class,
],

⚠️ That file deliberately has no namespace and no use, so Authenticate::class yields the plain string 'Authenticate' — the runtime prefixes it itself. A fully qualified name there (or passed to ->middleware()) is not found after prefixing, and every route using it answers 403. See Middleware.

🛑 Fallback Routes ​

If no route matches, you can define a fallback route that runs as a last resort.

🚨 Route::fallback($callback) ​

php
Route::fallback(function () {
    return response()->view('errors/404')->send();
});

📌 This route only triggers if no other route matches.

✅ Useful for custom 404 pages or other global handlers.

Always place fallback routes at the end of all other routes, or they might "catch" unintended requests.

🔐 CSRF Protection ​

All routes with POST, PUT, PATCH, or DELETE methods are automatically protected against CSRF attacks.

To pass the check, forms must include a hidden token field.

✅ Method 1: {csrf()} ​

You can use a convenient function in templates:

html
<form method="POST">
    {csrf()}
    <button>Submit</button>
</form>

This generates:

html
<input type="hidden" name="_token" value="{csrf_token}">

Also supports spoofing if _method is manually added:

html
<form method="POST">
    {csrf()}
    <input type="hidden" name="_method" value="PUT">
    <button>Update</button>
</form>

🛠 Method 2: Manually Insert the Token ​

You can manually add the hidden field:

html
<input type="hidden" name="_token" value="{csrf_token}">

Useful if you want full control over the HTML.

⚙️ Method 3: Meta Tag with Token ​

If you use pbFetch, it automatically looks for the CSRF token in a meta tag:

html
<meta name="csrf-token" content="{csrf_token}">

Then, pbFetch will include this token in the X-CSRF-TOKEN header when sending requests.

🔐 Token validation is handled by the VerifyCsrfToken middleware (alias csrf), which is automatically applied to all POST, PUT, PATCH, and DELETE routes outside the mgr context.

If the token is missing or invalid, an AJAX request gets JSON with status 403, and an ordinary form post is redirected back with an error message — see CSRF.

© PageBlocks 2019-present