Skip to content
Greg Bowler edited this page May 25, 2026 · 3 revisions

WebEngine routing is built on the phpgt/routing package, but most applications do not need to work with that package directly. The framework creates the request, loads the router, matches files from the project, builds the view and logic assemblies, and then lets the dispatcher execute the response.

This page is a reference for how that routing behaviour fits together inside a WebEngine application.

The default routing model

If an application does not provide its own router, WebEngine uses the framework router. That router has two routes:

  1. Page routes, which match browser-facing HTML requests against files in page/
  2. API routes, which match structured response requests against files in api/

The route is selected from the incoming request method, path, and Accept header. A normal browser request is usually handled by the page route because it accepts text/html and application/xhtml+xml. API requests are handled by the API route when the client asks for application/json or application/xml.

Once the route has been chosen, WebEngine looks for matching files on disk. It does not expect us to maintain a central list of every page in the application. The path still maps to the project structure, as covered in file-based routing.

Routing an Assembly of logic and view files

Routing does not render the response by itself. Its job is to decide which files belong to the request. This is done by building a file Assembly.

WebEngine records those files in two ordered lists:

  • the logic assembly, containing PHP files that may define go and do_* functions
  • the view assembly, containing the view files that make up the response body, either HTML or JSON

For example, a request to /shop/music may produce a view assembly like this:

page/_header.html
page/shop/_header.html
page/shop/@category.html
page/shop/_footer.html
page/_footer.html

and a logic assembly like this:

page/_common.php
page/shop/_common.php
page/shop/@category.php

The dispatcher later executes the logic assembly and streams the view assembly into the response: routing answers "what belongs to this request?", while dispatching answers "how do we run it?".

File matching rules

WebEngine's default router uses the Routing package's path matcher to scan the relevant directory and compare each file with the requested URI.

The important matching rules are:

  • direct files can match directly, such as /about to page/about.html and page/about.php
  • directory index files can match clean URIs, such as /blog to page/blog/index.html
  • dynamic names use @, such as /blog/hello-world to page/blog/@slug.html
  • catch-all dynamic names use @@, for deeper path captures
  • _common, _header, and _footer are special shared files that can be included because of where they sit in the directory tree

Static files take priority over dynamic placeholders. If both page/request/secrets.html and page/request/@request-id.html exist, the URI /request/secrets will resolve to the concrete secrets.html file rather than being swallowed by the dynamic route.

Headers and footers are also sorted deliberately. Outer headers are applied before inner headers, and inner footers are applied before outer footers, so nested sections can wrap their own content without breaking the site-wide frame.

Dynamic paths

When a route uses @ placeholders, the captured values are available in page logic through GT\Routing\Path\DynamicPath.

use GT\Routing\Path\DynamicPath;

function go(DynamicPath $path):void {
	$slug = $path->get("slug");
	// ... your code
}

For page/blog/@slug.html and a request to /blog/hello-world, get("slug") returns hello-world.

Calling get() without a name returns the deepest matched dynamic value. For catch-all routes using @@, getExtra() exposes the remaining path segments.

Keep dynamic routes specific enough to stay readable. A path such as /product/chair/wooden-dining-chair is usually easier to understand and maintain than a route that captures a whole area of the site in one broad @@ placeholder.

Content negotiation

The router can choose between more than one handler for the same URI by looking at the request's Accept header. This is how WebEngine can support an HTML page and an API representation without treating them as completely separate applications.

In the default router:

  • the page route accepts text/html and application/xhtml+xml
  • the API route accepts application/xml and application/json

If several router callbacks could match, the Routing package compares the accepted media types and their quality values, then chooses the best match. If a client sends Accept: */*, the first compatible match is used.

In practice, most WebEngine applications can keep this simple. Use normal page routes for browser pages, use API routes when the response is genuinely structured data, and only reach for custom content negotiation when one URI intentionally serves more than one representation.

Logic stream wrapper

Page logic files often define the same function names. It is normal for several files in the same request to define go(), because each file belongs to a different part of the route.

WebEngine can load those files safely because the Routing package provides a logic stream wrapper. When a classless PHP logic file is loaded, the wrapper gives it a generated namespace based on its path. That means page/_common.php and page/account/settings.php can both define go() without colliding in PHP's global namespace.

For application code, the main rule is simple: page logic files should declare functions and classes, not produce output as a side effect. Let the logic hooks set response state, call services, and bind data into the view model.

Redirects

Redirects are handled before the main page lifecycle begins. This means old or moved URLs can be resolved before WebEngine starts building a page response.

WebEngine looks for a single redirect file in the project root:

  • redirect.ini
  • redirect.csv
  • redirect.tsv

Only one of those files should exist. If more than one redirect file is present, WebEngine treats that as ambiguous.

CSV and TSV files use the old path, the new path, and an optional status code:

/about-us,/about,301
/products/old-chair,/products/wooden-dining-chair,308

INI files group redirects by status code:

[301]
/about-us = /about

[308]
/products/old-chair = /products/wooden-dining-chair

Redirect rules are path-based. Use them for site migrations, renamed pages, and deliberately moved resources. For request-specific decisions, such as redirecting after a form submission, set the redirect from page logic instead.

Custom routers

Most applications should start with the default router. A custom router is useful when the application needs to change the routing shape itself, for example:

  • separate route groups with different view models
  • custom content negotiation rules
  • routing to files outside the standard page/ and api/ directories
  • specialist routes that should not follow the normal file-based mapping

The router file and class are configured with router.router_file and router.router_class. By default, WebEngine looks for router.php containing AppRouter.

A custom router extends GT\Routing\BaseRouter and uses route attributes:

use GT\Http\Request;
use GT\Routing\BaseRouter;
use GT\Routing\Method\Any;
use GT\Routing\Path\PathMatcher;
use GT\WebEngine\View\HTMLView;

class AppRouter extends BaseRouter {
	#[Any(name: "page-route", accept: "text/html,application/xhtml+xml")]
	public function page(Request $request):void {
		$matcher = new PathMatcher("page");
		$this->setViewClass(HTMLView::class);

		// Add matching files to the logic and view assemblies here.
	}
}

In a custom router, keep the same boundary in mind: the router should select files and view type, not run page behaviour directly. The dispatcher and page logic should still own execution.

X-Redirect and X-Logic-Execution headers

Routing usually stays easy to reason about when each URI relates to an obvious file in the project. If a request becomes difficult to trace, use this list to debug:

  • Check the X-Redirect header - if the page was redirected, this will show exactly which line of code performed the redirect.
  • Check the X-Logic-Execution header, showing the order of the go and do functions that have been executed as part of the page response.
  • Check if a static file and dynamic placeholder both match the same URI.
  • Check if page/example.html and page/example/index.html are competing for the same route.
  • Check shared _common files in any directory above the current deepest directory.

If the route still feels surprising, prefer simplifying the path structure over adding more routing rules. WebEngine works best when the URL, view file, and logic file are easy to connect in our heads.


Read about page logic conventions next, or see development diagnostics when you need to inspect a request.

Clone this wiki locally