Luminova Framework

PHP Luminova: HTTP Routing Implementation Examples

Last updated: 2025-08-21 17:51:10

Explore practical examples of HTTP routing in Luminova. Learn how to define routes, use attributes, handle dynamic URI segments, and implement controllers for web requests.

This page shows different examples of using Luminova HTTP routing with a View Controller.For deeper understanding, see these references first:


Examples

Method-Based Routes

When you are not using attribute routing, you define your URI prefixes in /public/index.php.To learn more about prefixes and context files, see Routing URI Prefix.

Example:The following setup maps any request URI prefix starting with:

  • /admin to the routes/admin.php file.
  • /api to the routes/api.php file.
  • While the Prefix::WEB is global fall all URI prefix that doesn't have a custom handler

So visiting https://example.com/admin/ will automatically load that route file.

// /public/index.php

use Luminova\Boot;
use Luminova\Routing\Prefix;

Boot::http()->router->context(
    new Prefix(Prefix::WEB, [ErrorController::class, 'onWebError']),
    new Prefix(Prefix::API, [ErrorController::class, 'onApiError']),
    new Prefix('admin', [ErrorController::class, 'onAdminError'])
)->run();

Note:Do not rename the default Context::WEB route. Changing its name can cause unexpected errors in your application.


Route context file

Every URI prefix you register can have a matching file in /routes/.For example, the admin prefix above corresponds to /routes/admin.php.Inside this file, you define all routes under /admin/ using the global $router instance.

// /routes/admin.php

/**
 * Available in file scope
 * 
 * @var Luminova\Routing\Router $router
 */

$router->get('/admin/', 'AdminController::index');

Attribute-Based Routes

When attribute routing is enabled, you no longer need to manually register URI prefixes in /public/index.php.Instead, you define them directly inside your controller class using the #[Prefix] attribute.

Global Website Controller Example:

The following controller automatically handles all website requests (except /api/... and /admin/...) and uses ErrorController::onWebError for global error handling, if an error occurs.

// /app/Controllers/Http/Controller.php

namespace App\Controllers\Http;

use Luminova\Base\BaseController;
use Luminova\Attributes\Prefix;
use App\Errors\Controllers\ErrorController;

// Exclude /api and /admin from this controller
#[Prefix(pattern: '/(?!api|admin).*', onError: [ErrorController::class, 'onWebError'])]
class Controller extends BaseController
{
    // Define controller methods here
}

Custom Admin Prefix Controller Example:

Define controller to handle all URI prefix with (admin).

// /app/Controllers/Http/AdminController.php

namespace App\Controllers\Http;

use Luminova\Base\BaseController;
use Luminova\Attributes\Prefix;
use App\Errors\Controllers\ErrorController;

#[Prefix(pattern: '/admin/(:root)', onError: [ErrorController::class, 'onWebError'])]
class AdminController extends BaseController
{
    // Define controller methods here
}

Custom API Prefix Controller Example:

Define controller to handle all URI prefix with (api).

// /app/Controllers/Http/AdminController.php

namespace App\Controllers\Http;

use Luminova\Base\BaseController;
use Luminova\Attributes\Prefix;
use App\Errors\Controllers\ErrorController;

// Optionally restrict with version code e.g /api/1.0.0
// #[Prefix(pattern: '/api/(:version)', onError: [ErrorController::class, 'onApiError'])]

#[Prefix(pattern: '/api/(:root)', onError: [ErrorController::class, 'onWebError'])]
class RestController extends BaseController
{
    // Define controller methods here
}

With attribute routing, your index.php setup stays clean because the router automatically reads prefixes from attributes:

// /public/index.php

use Luminova\Boot;

Boot::http()->router->context()->run();

Middlewares

Middleware lets you intercept incoming requests before they reach your controllers.This is useful for tasks like authentication, access control, or pre-processing.

If middleware fails (returns an error), the controller will never be called — instead, the defined error handler will run.

Middleware should return:

  • STATUS_SUCCESS (allow request to proceed)
  • STATUS_ERROR (block request and trigger error)

Using middleware with a route attribute

In this example, we use session authentication.We assume that the session object is available as $this->app->session inside your App\Application class.

// /app/Controllers/Http/Controller.php

use Luminova\Attributes\Route;

#[Route('/(:root)', methods: ['ANY'], middleware: Route::HTTP_BEFORE_MIDDLEWARE)]
public function middleware(): int
{
    if ($this->app->session->isOnline()) {
        return STATUS_SUCCESS;
    }

    return STATUS_ERROR;
}

Using middleware with method-based routing

If you’re not using attributes, you can attach middleware directly in your route file:

// /routes/web.php

$router->middleware('ANY', '/(:root)', 'Controller::middleware');

Using middleware with a closure

You can also define middleware inline using a closure:

// /routes/web.php

$router->middleware('ANY', '/(:root)', static function (App\Application $app): int {
    if ($app->session->isOnline()) {
        return STATUS_SUCCESS;
    }

    return STATUS_ERROR;
});

Middleware in bind URI group

You can also define middleware in the router’s bind method to apply it to grouped routes.

// /routes/web.php

$router->bind('/user', static function() use($router, $app): void {
    // In group global scope.
    $router->middleware('ANY', '/(:root)', 'Controller::middleware');
    $router->get('/profile', 'Controller::profile');
});

Rendering templates

Luminova uses the Luminova\Template\View class to render pages.You call the view()->render() method to display a template and pass any variables you need.

Here’s how to set up a simple landing page route.


Rendering with an attribute

Define a controller method with a #[Route] attribute:

// /app/Controllers/Http/Controller.php

use Luminova\Attributes\Route;

#[Route('/', methods: ['GET'])]
public function index(): int
{
    return $this->app->view->view('index')->render([
        'title' => 'Home Page'
    ]);
}

Rendering with method-based routing

You can also render templates inside a route context file such as /routes/web.php.

Using a controller method handler:

// /routes/web.php

$router->get('/', 'Controller::index');

Using a closure handler:

// /routes/web.php

$router->get('/', static function (App\Application $app): int {
    return $app->view->view('index')->render([
        'title' => 'Home Page'
    ]);
});

5. REST API responses

While Luminova\Template\View can technically output JSON or other formats (as long as you use a template file in /resources/Views/), this approach isn’t ideal for small API responses.

For APIs, it’s better to use the Luminova\Template\Response or global response() helper to return JSON, XML, or any other structured format directly — without creating a template file.


Returning JSON with an attribute

// /app/Controllers/Http/RestController.php

use Luminova\Attributes\Route;
use Luminova\Template\Response;

#[Route('/api/info', methods: ['GET'])]
public function info(): int
{
    return (new Response(200))->json([
        'status' => 'OK',
        'title' => 'Home Page'
    ]);
}

Returning a response object instead

Luminova routes can return:

  • an HTTP status code (int),
  • a Luminova\Template\Response object, or
  • any class implementing Luminova\Interface\ViewResponseInterface.

This gives you flexibility — use a status code for simple results or a full response object when you need more control.

// /app/Controllers/Http/RestController.php

use Luminova\Attributes\Route;
use Luminova\Template\Response;

#[Route('/api/info', methods: ['GET'])]
public function info(): Response
{
    return (new Response(200))->content([
        'status' => 'OK',
        'title' => 'Home Page'
    ]);
}

Returning JSON with method-based routing

// /routes/api.php

$router->get('/api/info', 'RestController::info');

Returning JSON with a closure

// /routes/api.php

use Luminova\Template\Response;

$router->get('/api/info', static function () use ($app): int {
    return (new Response (200))->json([
        'status' => 'OK',
        'title' => 'Home Page'
    ]);
});

Dynamic URI segments

You can capture parts of the URL using patterns, named placeholders, or predefined placeholder patterns.This allows your routes to handle URLs like https://example.com/user/peter1 dynamically.

In this example, we use predefined placeholder patterns because they are cleaner and enforce strict validation.They work like regular expressions, but without the extra complexity — since Luminova already provides common patterns.

Tip:Avoid using named placeholders like /foo/{username} because they do not validate the data type or format of the captured segment. Predefined placeholders automatically ensure the value matches the expected pattern.

Capturing segments with an attribute

// /app/Controllers/Http/Controller.php

use App\Models\User;
use Luminova\Attributes\Route;

#[Route('/user/(:username)', methods: ['GET'])]
public function profile(string $username): int
{
    return $this->app->view->view('user.profile')->render([
        'username' => $username,
        'user' => (new User())->find($username)
    ]);
}

Capturing segments with method-based routing

// /routes/web.php

$router->get('/user/(:username)', 'Controller::profile');

These examples capture a dynamic username from the URL and pass it to the controller method to fetch and display the user profile.


Handling HTTP requests

When processing form submissions or API calls (e.g., updating a user profile via POST), it’s best to use a dedicated controller instead of mixing logic into your main web controller.

You can extend either:

  • BaseController (for API or non-view logic)
  • BaseViewController (if you also render templates)

Handling request methods with an attribute

// /app/Controllers/Http/Controller.php

use Luminova\Attributes\Route;
use App\Models\User;

#[Route('/user/(:username)', methods: ['POST'])]
public function update(string $username): int
{
    $name = $this->request->getPost('name');
    $email = $this->request->getPost('email');

    $updated = (new User())->update($username, [
        'name' => $name,
        'email' => $email
    ]);

    return response()->json([
        'status' => $updated ? 'OK' : 'ERROR',
        // Add additional response fields as needed
    ]);
}

Handling requests methods with method-based routing

// /routes/user.php

$router->post('/user/(:username)', 'Controller::update');

Nested binding and Aliases

You can group related routes together under a shared path segment.For example, if you want to handle /user and nested paths like /user/peter1, you can bind them using a closure or define them directly in a controller with attributes.

Route Aliases (Redirects or Alternate Paths)**

Show how to point multiple URLs to the same controller method — useful for legacy routes or SEO.

#[Route('/about', methods: ['GET'], aliases: ['/about-us', '/company/about'])]
public function about(): int 
{
    return $this->view('about');
}

Using method-based routing with bind

You can bind all /user/... routes to a single block in your route file:

// /routes/web.php

use Luminova\Routing\Router;
use App\Application;

$router->bind('/user/(:root)', function (Router $router, Application $app) {
    $router->get('/', 'Controller::dashboard');
    $router->get('/(:username)', 'Controller::profile');
});

Using separate route attributes

You can map each route to its own controller method:

// /app/Controllers/Http/Controller.php

use App\Models\User;
use Luminova\Attributes\Route;

#[Route('/user', methods: ['GET'])]
public function dashboard(): int
{
    return $this->view('user.dashboard');
}

#[Route('/user/(:username)', methods: ['GET'])]
public function profile(string $username): int
{
    return $this->view('user.profile', [
        'username' => $username,
        'user' => (new User())->find($username)
    ]);
}

Using multiple route attributes on one method

If you prefer to handle both /user and /user/(:username) with the same method, you can stack route attributes, use aliases or (:optional) placeholder.

1. Stack multiple route attributes

Define more than one #[Route] attribute on the same method:

// /app/Controllers/Http/Controller.php

use App\Models\User;
use Luminova\Attributes\Route;

#[Route('/user', methods: ['GET'])]
#[Route('/user/(:username)', methods: ['GET'])]
public function user(?string $username = null): int
{
    if ($username === null) {
        return $this->view('user.dashboard');
    }

    return $this->view('user.profile', [
        'username' => $username,
        'user' => (new User())->find($username)
    ]);
}

2. Use an optional placeholder

Use (:optional) if the second segment is not required:

// /app/Controllers/Http/Controller.php

use App\Models\User;
use Luminova\Attributes\Route;

#[Route('/user/(:optional)', methods: ['GET'])]
public function user(?string $username = null): int
{
    if ($username === null) {
        return $this->view('user.dashboard');
    }

    return $this->view('user.profile', [
        'username' => $username,
        'user' => (new User())->find($username)
    ]);
}

3. Define route aliases

Use a single #[Route] with an aliases array for cleaner definitions:

// /app/Controllers/Http/Controller.php

use App\Models\User;
use Luminova\Attributes\Route;

#[Route('/user', methods: ['GET'], aliases: ['/user/(:optional)'])]
public function user(?string $username = null): int
{
    if ($username === null) {
        return $this->view('user.dashboard');
    }

    return $this->view('user.profile', [
        'username' => $username,
        'user' => (new User())->find($username)
    ]);
}

Your Choice?

  • Stacking attributes — explicit but longer.
  • Optional placeholder — simplest if the paths are similar.
  • Aliases — clean and centralized.

Custom Template Folder

By default, templates load from resources/Views. If you want to organize views in sub-folders — for example, to separate users templates from admin templates — you can set a custom view folder using setFolder().

This can be done globally (for a whole controller or route group) or locally (just before rendering a single view).

Setting a custom view folder in a controller

If all views in a controller should come from the same folder, call setFolder() inside onCreate(), __construct(), or your middleware method:

// /app/Controllers/Http/Controller.php

use Luminova\Base\BaseController;

class Controller extends BaseController 
{
    protected function onCreate(): void 
    {
        $this->app->view->setFolder('users');
    }
}

Setting a custom view folder in a controller method

A per-method override inside a controller, where only one method uses a custom folder without changing the folder for the whole controller. This shows that setFolder() can be called anywhere — not just at controller creation or in routes.

Example:

// /app/Controllers/Http/Controller.php

use Luminova\Base\BaseController;

class Controller extends BaseController 
{
    public function dashboard(): int
    {
        return $this->view('dashboard'); // uses default folder
    }

    public function profile(): int
    {
        return $this->app->view->setFolder('users')
            ->view('profile')
            ->render();
    }
}

Setting a custom folder inside a route context

You can also set folders in routes/web.php or similar files:

// /routes/web.php

use Luminova\Routing\Router;
use App\Application;

// Set globally for the entire route file
$app->view->setFolder('users');

$router->bind('/user', static function() use ($router, $app): void {
    // Set for all routes in this group
    $app->view->setFolder('users');

    $router->get('/', static function() use ($app): int {
        // Or set only for this route before rendering
        return $app->view->setFolder('users')
            ->view('dashboard')
            ->render();
    });
});

Why use this?

  • Keeps templates organized by feature (e.g. resources/Views/users, resources/Views/panel).
  • Works well with HMVC-style structure — each module or context can have its own view folder automatically.
  • Reduces repetitive folder references in your view() calls.