tereta/route

Maintainers

Package info

gitlab.com/tereta/library/route

Issues

pkg:composer/tereta/route

Statistics

Installs: 141

Dependents: 8

Suggesters: 0

Stars: 0

1.0.13 2026-05-05 21:03 UTC

This package is auto-updated.

Last update: 2026-05-05 18:29:45 UTC


README

🌐 Русский | English

Table of Contents

Overview

Routing module for the Tereta framework. It turns an HTTP request into a controller invocation: resolves the current domain and site, resolves the route, instantiates the controller, and returns a Response. Supports a multi-site architecture β€” a single project can serve multiple sites across different domains, with HTTPS, subpaths, and non-standard ports.

The main entry point is Services\Route::singleton()->getByUrl(): it reads the current URL and HTTP method from $_SERVER and returns a Models\Request with the resolved Domain, Site, and Route. For reverse resolution (generating URLs by alias), use Services\Url::alias().

Dependencies

  • tereta/core β€” Singleton, Factory, Chain traits, the base Value model, base exceptions
  • tereta/di β€” resolves controllers and chain links through the DI container
  • tereta/db β€” ORM base Cores\Model, ModelFactory, Connection::transaction()
  • tereta/cli (suggest) β€” required for the route:site CLI command
  • tereta/application (suggest) β€” auto-registers the module and scans controller attributes

Architecture

LayerRole
Module.phpRegisters the site, route, domain tables via SetupContract
Services/Business services: Route, Domain, Site, Url, Helper, Get, Post, Files
Models/DTOs Request, Response and ORM models Route, Site, Domain
Factories/Assemble resolution chains (Router, Router\Alias) and instantiate controllers (Controller, Controller\Attribute)
Chains/Route resolution links: Route\Attribute, Route\Db, Route\Attribute\Alias
Attributes/PHP attributes: #[Controller] for controllers, #[Route] for chain links
Interfaces/Contracts: Controller, Chains\Route
Commands/CLI: route:site (list / create / delete)
Exceptions/NotFoundException, Core, Domain

Services and factories use traits from tereta/core: Singleton for global access, Factory for parameterised DI construction.

Multi-Site Support

  • Site β€” a logical unit with a name and identifier; not tied to a specific host.
  • Domain β€” a physical host bound to a site. Fields:
    • domain β€” host with optional port (e.g. example.com:8080)
    • secure β€” HTTPS flag
    • path β€” subpath within the host (e.g. /admin)
    • site_id β€” reference to Site
    • general β€” flag marking the site's primary domain (used in reverse resolution)

A single Site can have multiple Domain records. Domain resolution (Services\Domain::getByUrl()) sorts matches by path length in descending order β€” specific paths (/admin) intercept requests before generic ones (/). Host extraction correctly handles non-standard ports.

Routing

Forward resolution (URL β†’ controller) is performed in Services\Route::getByUrl() via a chain built by Factories\Router::createChain() and ordered by #[RouteChainAttribute(priority)]. The current configuration:

  1. Chains\Route\Attribute (priority 100) β€” matches the URI against the regex from #[Controller(match: …)] and looks up the controller in memory via Factories\Controller\Attribute::getClass(). Does not touch the database.
  2. Chains\Route\Db β€” if the previous link returned null, looks up a record in the route table by path = $uri.

If both links return null, an Exceptions\NotFoundException is thrown. The route table (schema: Resources/db/route.xml) is used when routes must be created dynamically (CMS, user-defined URLs) and cannot be declared as attributes at compile time.

Supported HTTP methods (constants on Services\Route): METHOD_GET, METHOD_POST, METHOD_PUT, METHOD_DELETE, METHOD_PATCH, METHOD_OPTIONS, METHOD_HEAD, METHOD_ANY. An unknown method raises Exceptions\Core with code 405.

Declaring a Controller

🌐 Русский | English

A controller implements Interfaces\Controller and declares one or more routes via the #[Controller] attribute (which is marked IS_REPEATABLE):

use Tereta\Route\Attributes\Controller;
use Tereta\Route\Interfaces\Controller as ControllerContract;
use Tereta\Route\Models\Request as RequestModel;
use Tereta\Route\Models\Response as ResponseModel;
use Tereta\Core\Data\Value;

#[Controller(
    alias:  'user.profile',
    match:  '^user/(\d+)/profile$',
    uri:    'user/%s/profile',
    method: 'GET',
)]
class UserProfileController implements ControllerContract
{
    public function __construct(
        private RequestModel $requestModel,
    ) {}

    public function handle(): ResponseModel
    {
        return ResponseModel::factory()->create([
            'handler' => 'view/user/profile',
            'data'    => Value::factory()->create([
                'data' => ['url' => $this->requestModel->getUrl()],
            ]),
        ]);
    }
}

Fields of the #[Controller] attribute:

FieldPurpose
matchRegular expression for forward resolution (URL β†’ controller)
aliasName used for reverse resolution (alias β†’ URL)
urisprintf URL template with %s placeholders for reverse resolution
methodHTTP method (GET, POST, …) or null for any

RequestModel is injected into the controller's constructor automatically: Factories\Controller::create() passes it to the tereta/di container, which fills the matching parameter.

Generating URLs by Alias

use Tereta\Route\Services\Url;
use Tereta\Route\Services\Route as RouteService;

$urlService = Url::factory()->create(['siteModel' => $siteModel]);

// Base site URL: https://example.com
echo $urlService->get();

// URL with an extra URI: https://example.com/some/path
echo $urlService->get('some/path');

// URL by alias with parameters: https://example.com/user/42/profile
echo $urlService->alias('user.profile', [42], RouteService::METHOD_GET);

Url::alias() delegates to Services\Route::getByAlias(), which uses a separate chain from Factories\Router\Alias consisting only of Chains\Route\Attribute\Alias. The DB link does not participate in this chain β€” see below for the reason.

If the alias template contains sprintf placeholders but parameters are missing (or fewer than expected), Url::alias() throws an ArgumentCountError with the alias name and the template.

Route Model: Two Roles

The Tereta\Route\Models\Route class is annotated with #[Model(table: 'route')], which formally makes it an ORM entity for the route table. However, in the current implementation it is used in two distinct roles:

  1. ORM entity β€” when resolving a route via Chains\Route\Db and when working with the route table directly. In this role, the path field (the concrete request URI) is populated, matching the column of the same name in the schema (Resources/db/route.xml).

  2. In-memory DTO for reverse resolution (alias β†’ URL) β€” Chains\Route\Attribute\Alias creates a RouteModel instance bypassing the database and stores a uri field in it β€” a sprintf template with %s placeholders, taken from the #[Controller(uri: …)] attribute. This field does not exist in the table schema and lives only in the memory of that specific instance.

Difference between path and uri

  • path β€” the concrete request URI, e.g. user/42/profile. Stored in the database, read by Models\Request::getUrl().
  • uri β€” a template with placeholders for URL generation, e.g. user/%s/profile. Used only by Services\Url::alias() to substitute parameters via sprintf. Not persisted to the database.

Why it works this way

Reverse resolution (Url::alias()) is by design intended to work only with routes declared via controller attributes β€” because only a controller carries a sprintf template. A DB route's path is already concrete, and there is conceptually nothing to generate "with parameters" from it. Therefore Chains\Route\Db does not participate in the alias chain, and the alias resolver and the DB resolver do not overlap.

Request Lifecycle

  HTTP Request ($_SERVER, $_GET, $_POST, $_FILES)
       β”‚
       β–Ό
  Services\Helper::getUrl()         β€” current URL + scheme from $_SERVER
       β”‚
       β–Ό
  Services\Route::getByUrl()
       β”‚
       β–Ό
  Services\Domain::getByUrl()       β€” resolves Domain β†’ Site
       β”‚
       β–Ό
  Factories\Router::createChain()   β€” chain: Attribute β†’ Db
       β”‚
       β–Ό
  Models\Request                    β€” DTO: Domain + Site + Route + method
       β”‚
       β–Ό
  Factories\Controller::create()    β€” instantiation via tereta/di
       β”‚
       β–Ό
  Interfaces\Controller::handle()   β€” controller returns a Response
       β”‚
       β–Ό
  Models\Response                   β€” handler, data, status, meta, headers

Models\Response extends Tereta\Core\Data\Value and stores the handler (template/view name), body (data), HTTP status (defaults to 200), meta, and headers. Delivering this object to the client is the responsibility of the calling layer (e.g. the Application module).

Helper Services

Singleton wrappers around PHP superglobals, extending Tereta\Core\Data\Value:

ServiceSource
Services\Get$_GET
Services\Post$_POST
Services\Files$_FILES
Services\HelperBuilds the current URL from $_SERVER['HTTP_HOST'] / REQUEST_URI / HTTPS / HTTP_X_FORWARDED_PROTO
use Tereta\Route\Services\Get;
use Tereta\Route\Services\Post;
use Tereta\Route\Services\Helper;

$page = Get::singleton()->get('page');      // GET parameter
$name = Post::singleton()->get('name');     // POST parameter
$url  = Helper::singleton()->getUrl();      // Current URL

CLI Commands

Site management:

./cli.php route:site list                                    # List sites
./cli.php route:site create <identifier> "<name>" "<url>"    # Create a site
./cli.php route:site delete <identifier>                     # Delete a site

Example:

./cli.php route:site create default "My Site" "https://example.com"

create creates a Site and its primary Domain in a single transaction; delete removes the site along with its associated domains.

Author and License

Author: Tereta Alexander
Website: tereta.dev
License: Apache License 2.0. See LICENSE.

 www.β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•—β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•—β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•— β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•—β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•— β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•—
     β•šβ•β•β–ˆβ–ˆβ•”β•β•β•β–ˆβ–ˆβ•”β•β•β•β•β•β–ˆβ–ˆβ•”β•β•β–ˆβ–ˆβ•—β–ˆβ–ˆβ•”β•β•β•β•β•β•šβ•β•β–ˆβ–ˆβ•”β•β•β•β–ˆβ–ˆβ•”β•β•β–ˆβ–ˆβ•—
        β–ˆβ–ˆβ•‘   β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•—  β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•”β•β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•—     β–ˆβ–ˆβ•‘   β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•‘
        β–ˆβ–ˆβ•‘   β–ˆβ–ˆβ•”β•β•β•  β–ˆβ–ˆβ•”β•β•β–ˆβ–ˆβ•—β–ˆβ–ˆβ•”β•β•β•     β–ˆβ–ˆβ•‘   β–ˆβ–ˆβ•”β•β•β–ˆβ–ˆβ•‘
        β–ˆβ–ˆβ•‘   β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•—β–ˆβ–ˆβ•‘  β–ˆβ–ˆβ•‘β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•—   β–ˆβ–ˆβ•‘   β–ˆβ–ˆβ•‘  β–ˆβ–ˆβ•‘
        β•šβ•β•   β•šβ•β•β•β•β•β•β•β•šβ•β•  β•šβ•β•β•šβ•β•β•β•β•β•β•   β•šβ•β•   β•šβ•β•  β•šβ•β•
                                                      .dev

Copyright (c) 2024-2026 Tereta Alexander