Skip to content

Latest commit

 

History

History
508 lines (378 loc) · 21.3 KB

routing.md

File metadata and controls

508 lines (378 loc) · 21.3 KB

Routing

The framework implements a hybrid routing system that combines Laravel's powerful routing capabilities with WordPress's template hierarchy. A request that reaches the Laravel router follows a clear priority order:

  1. Laravel Routes First: All routes defined in your routes/web.php file take precedence. This gives you full control over specific URLs and allows you to leverage Laravel's routing features when needed.

  2. WordPress Template Hierarchy as Fallback: If no Laravel route matches the current URL, a catch-all fallback route hands the request to FrontendController, which asks WordPress which template the request needs and renders the matching Blade view.

This "Laravel-first, WordPress-fallback" approach provides the best of both worlds:

  • Full control and flexibility of Laravel routing when you need it
  • Automatic handling of standard WordPress content through the template hierarchy
  • No need to define routes for every WordPress page or post

Some requests never reach the router: WordPress answers them itself (the admin, wp-login.php, the REST API, robots.txt, the favicon, feeds and trackbacks). See How Pollora runs WordPress inside Laravel.

Incoming Request
       │
       ▼
┌────────────────┐
│ Admin, login,  │    Yes    ┌─────────────────┐
│ REST, robots,  ├──────────►│ WordPress       │
│ favicon, feed? │           │ answers it      │
└──────┬─────────┘           └─────────────────┘
       │ No
       ▼
┌──────────────┐
│ Match Laravel│    Yes    ┌─────────────────┐
│    Route?    ├──────────►│ Execute Laravel │
└──────┬───────┘           │     Route       │
       │                   └─────────────────┘
       │ No
       ▼
┌──────────────┐
│  Fallback to │           ┌─────────────────┐
│ Frontend     ├──────────►│ Render the      │
│ Controller   │           │ template found  │
└──────────────┘           └─────────────────┘

Laravel Routing Basics

In Laravel, routes are defined in the routes/web.php file. A route associates a URL with a controller action or a closure that returns a view. For example:

// routes/web.php

use App\Http\Controllers\HomeController;

Route::get('/', [HomeController::class, 'index']);

Here, when the URL /home is accessed, the index method of the HomeController is invoked.

Route Groups and Middleware

You can group routes to apply shared attributes, such as middleware, prefixes, or namespaces. Middleware can help perform actions before or after a request reaches the controller. For example:

// Group routes with middleware and prefix
Route::middleware('auth')->prefix('admin')->group(function () {
    Route::get('dashboard', 'AdminController@dashboard');
    Route::get('settings', 'AdminController@settings');
});

Route Alias (Naming)

Route naming, or aliasing, allows you to refer to routes by a name instead of their URL. This is particularly useful when generating URLs or redirecting. Here's how you can name a route:

// Named route for the home page
Route::get('/', [HomeController::class, 'index'])->name('home');

You can find more about Laravel's routes in the official documentation.

WordPress-Style Route Declarations

Pollora Framework provides WordPress-specific routes that integrate with WordPress's conditional tags system.

WordPress Routes vs Standard Routes

There are two main methods to define routes in your application:

  1. Standard Laravel Routes (Route::get(), Route::post(), Route::any(), etc.):

    • These routes follow Laravel's standard routing behavior
    • They do not interact with WordPress's conditional tags system
    • They do not automatically receive WordPress-specific middleware
  2. WordPress-Specific Routes (Route::wp() and Route::wpMatch()):

    • These routes integrate with WordPress's conditional tags system
    • They automatically receive WordPress-specific middleware:
      • WordPressBindings: Adds WordPress objects (post, query) to the route
      • WordPressHeaders: Manages HTTP headers for WordPress responses
      • WordPressShutdown: Runs WordPress's shutdown hooks before the response is sent
      • WordPressTemplateEnhancement: Runs WordPress's template enhancement filter on the page, which moves block styles into the <head>
    • WordPress's body classes and is_404() are left as WordPress resolved them; only routes WordPress does not answer are adjusted (see Middleware)
    • They are processed through WordPress's conditional logic
    • Route::wp() accepts all HTTP verbs
    • Route::wpMatch() allows specifying specific HTTP verbs

WordPress Route Conditions

WordPress routes use conditions that can be specified in two ways:

  1. Using predefined aliases (shorter syntax)
  2. Using actual WordPress conditional function names (more explicit)

The framework provides a default set of aliases in the config/wordpress.php file. These aliases map WordPress conditional tags to route URIs:

return [
    'conditions' => [
        // Core WordPress conditions
        'is_404' => '404',
        'is_archive' => 'archive',
        'is_attachment' => 'attachment',
        'is_author' => 'author',
        'is_category' => ['category', 'cat'],
        'is_date' => 'date',
        'is_day' => 'day',
        'is_front_page' => ['/', 'front'],
        'is_home' => ['home', 'blog'],
        'is_month' => 'month',
        'is_page_template' => 'template',
        'is_page' => 'page',
        'is_paged' => 'paged',
        'is_post_type_archive' => ['post-type-archive', 'postTypeArchive'],
        'is_search' => 'search',
        'is_single' => 'single',
        'is_singular' => 'singular',
        'is_sticky' => 'sticky',
        'is_subpage' => ['subpage', 'subpageof'],
        'is_tag' => 'tag',
        'is_tax' => 'tax',
        'is_time' => 'time',
        'is_year' => 'year',

        // WooCommerce conditions
        'is_shop' => 'shop',
        'is_product' => 'product',
        'is_cart' => 'cart',
        'is_checkout' => 'checkout',
        'is_account_page' => 'account',
        'is_product_category' => 'product_category',
        'is_product_tag' => 'product_tag',
        'is_wc_endpoint_url' => 'wc_endpoint',
    ],
    // ... other WordPress configuration options
];

The framework provides a default set of aliases in its config/wordpress.php file. You can override or extend these aliases in your application's config/wordpress.php file.

The WooCommerce conditions (shop, product, cart, checkout, account…) are added by pollora/woocommerce: composer require pollora/woocommerce in a project that runs WooCommerce.

Publishing the WordPress Configuration File

To customize WordPress route conditions, you can publish the framework's configuration file to your application:

php artisan vendor:publish --tag=wordpress

This command will copy the framework's configuration file to your application's config/ directory, allowing you to customize it according to your needs.

Each alias maps a WordPress conditional function (like is_page()) to a route URI or array of URIs. For example, 'is_page' => 'page' means that when you use Route::wp('page', ...) or Route::wp('is_page', ...), it will check if the current request matches the is_page() WordPress condition.

Using WordPress Routes

To define a WordPress-specific route, you can use either the wp or wpMatch method. You have two ways to specify the WordPress condition:

  1. Using the predefined aliases (shorter syntax)
  2. Using the actual WordPress conditional function names (more explicit)

Here are examples of both approaches:

// Using aliases (shorter syntax)
Route::wp('single', function () {
    return view('post');
});

// Using actual WordPress function names (more explicit)
Route::wp('is_single', function () {
    return view('post');
});

// Using wpMatch with aliases
Route::wpMatch('GET', 'page', 'landing-page', function() {
    return view('pages.landing');
});

// Using wpMatch with actual WordPress function names
Route::wpMatch('GET', 'is_page', 'landing-page', function() {
    return view('pages.landing');
});

// Using wpMatch with multiple HTTP verbs and aliases
Route::wpMatch(['GET', 'POST'], 'page', ['contact', 'request-a-quote'], [FormController::class, 'index']);

// Using wpMatch with multiple HTTP verbs and actual WordPress function names
Route::wpMatch(['GET', 'POST'], 'is_page', ['contact', 'request-a-quote'], [FormController::class, 'index']);

Both methods accept a variable number of arguments:

  • For wp:

    • First argument is the WordPress condition (either an alias or the actual function name)
    • Last argument is the callback function or controller action
    • Any arguments in between are passed as parameters to the WordPress condition function
  • For wpMatch:

    • First argument is the HTTP method(s) ('GET', 'POST', etc. or an array of methods)
    • Second argument is the WordPress condition (either an alias or the actual function name)
    • Last argument is the callback function or controller action
    • Any arguments in between are passed as parameters to the WordPress condition function

For example:

// Route for GET requests to a specific page using alias
Route::wpMatch('GET', 'page', 'landing-page', function() {
    return view('pages.landing');
});

// Route for GET requests to a specific page using actual function name
Route::wpMatch('GET', 'is_page', 'landing-page', function() {
    return view('pages.landing');
});

// Route for POST requests to a form page using alias
Route::wpMatch('POST', 'page', 'contact', [FormController::class, 'submit']);

// Route for POST requests to a form page using actual function name
Route::wpMatch('POST', 'is_page', 'contact', [FormController::class, 'submit']);

// Route accepting both GET and POST for a product using alias
Route::wpMatch(['GET', 'POST'], 'singular', 'product', 123, function() {
    return view('product.show');
});

// Route accepting both GET and POST for a product using actual function name
Route::wpMatch(['GET', 'POST'], 'is_singular', 'product', 123, function() {
    return view('product.show');
});

// Route for all HTTP verbs using wp shortcut with alias
Route::wp('tax', 'product_cat', 'electronics', function() {
    return view('taxonomy.product-category');
});

// Route for all HTTP verbs using wp shortcut with actual function name
Route::wp('is_tax', 'product_cat', 'electronics', function() {
    return view('taxonomy.product-category');
});

Important Differences from Standard Routes

When using Route::wp() or Route::wpMatch() instead of Route::any() or other standard methods:

  1. The route will be processed through WordPress's conditional tags system
  2. WordPress-specific middleware will be automatically applied
  3. WordPress objects like $post and $wp_query will be available in the route parameters
  4. The route will only match if the WordPress condition is met

Template Routes

When working with WordPress custom templates (defined in themes/[theme-name]/config/templates.php), you can create specific routes to handle these templates. For example:

// This will match any page using the "contact" template
Route::wp('template', 'contact', function () {
    return view('page');
});

// This will match any page using the "landing" template and pass data to the view
Route::wp('template', 'landing', function () {
    return view('page.landing', [
        'features' => Feature::all(),
    ]);
});

⚠️ Important: Template routes must be declared before the generic page route (Route::wp('page')), otherwise they won't be taken into account.

Extending Route Conditions

You can extend the WordPress route conditions by adding your own entries to the conditions array in your application's config/wordpress.php file:

// config/wordpress.php
return [
    'conditions' => [
        // Add your custom conditions
        'is_custom_post_type' => 'custom-post-type',
        'is_special_page' => ['special-page', 'special'],
        
        // Override existing conditions
        'is_page' => ['page', 'static-page'],
    ],
];

You can also create your own WordPress conditional functions and use them in your routes:

// functions.php or a custom plugin file
function is_special_page() {
    // Your custom logic to determine if this is a special page
    return is_page() && has_term('special', 'page_category');
}

// routes/web.php
Route::wp('special-page', function() {
    return view('pages.special');
});

This approach allows you to create highly customized routing logic while maintaining the clean syntax of WordPress-style routes.

How the Blade view is found

When no route matches, WordPress decides which template the request needs (single, page, archive, 404...) and Pollora looks for the matching Blade view: it swaps .php for .blade.php in each candidate of WordPress's template hierarchy and searches the theme's view paths. The mapping is described in Theme Structure. To add candidates of your own, use WordPress's {type}_template_hierarchy filters, as shown in Extending the Template Hierarchy.

Automatic Template Routing

The framework implements a cascading routing system that works as follows:

  1. Requests WordPress answers itself: the admin, wp-login.php, the REST API and the special requests below (robots.txt, favicon, feeds, trackbacks) are answered by WordPress. The Laravel router does not run for them.

  2. Laravel Custom Routes: The framework checks if a matching route exists in your routes/web.php file. These routes have high priority and will be executed if they match the requested URL.

  3. WordPress Template Hierarchy: If no custom route matches, the fallback route calls FrontendController, which:

    • Determines the appropriate template hierarchy based on WordPress conditional tags
    • Sequentially searches for matching Blade views in your views directory
    • Returns the first view found in the hierarchy order
  4. Fallback Handling: If the hierarchy gives no Blade view, a template file WordPress resolved (a block theme's template-canvas.php, for example) is included as it is. A request for a page that does not exist, with no 404 template in the theme, gets Laravel's 404 page.

For example, for an "About" page, the process would be as follows:

// 1. First checks if a custom route exists in routes/web.php
Route::wp('page', 'about-us', function() {
    return view('pages.about');
});

// 2. If no route matches, FrontendController checks the template hierarchy:
// - page-about.blade.php
// - page.blade.php
// - singular.blade.php
// - index.blade.php

// 3. If no view is found, a block theme's template file is included, or Laravel's 404 page is returned

This approach offers several benefits:

  • Allows fine-grained customization via Laravel routes when needed
  • Maintains compatibility with WordPress template hierarchy
  • Provides automatic fallback to more generic templates
  • Reduces boilerplate code by automatically handling standard cases
  • Ensures that all requests are handled appropriately, even special WordPress requests

⚠️ Important: Routes defined in routes/web.php always take precedence over the automatic template system. Use this advantage to:

  • Add custom logic to specific pages
  • Inject specific data into your views
  • Handle special cases requiring processing before display

Special WordPress Requests

The framework hands these WordPress request types back to WordPress:

  • robots.txt: Requests for /robots.txt are handled by WordPress's built-in do_robots action
  • favicon.ico: Requests for /favicon.ico are handled by WordPress's built-in do_favicon action
  • Feeds: RSS and Atom feed requests are handled by WordPress's built-in do_feed function
  • Trackbacks: Trackback requests are handled by WordPress's built-in trackback handler

They are answered once template_redirect has run, and the request ends there: the Laravel router never sees them, so a Route::wp() declared for them is never reached. To change what they return, use WordPress's own hooks, for example the robots_txt filter for robots.txt:

add_filter('robots_txt', fn (string $output): string => $output."\nDisallow: /private/");

Fallback Routes

After all other routes, the framework registers a catch-all fallback route, {any}, which delegates to the FrontendController. It matches every path except those starting with api/. This controller:

  1. Determines the appropriate template based on WordPress's template hierarchy
  2. Renders the corresponding Blade view
  3. Includes the template file WordPress resolved when there is no Blade view (a block theme's template), and returns Laravel's 404 page when nothing fits

This ensures that all requests are handled appropriately, even if no explicit route is defined.

Theme & Plugin API Routes

Themes and plugins can define their own routes by placing route files in a routes/ directory at their root. The framework automatically discovers and loads these files during registration.

Route Files

File Prefix Use case
routes/api.php /api JSON endpoints, AJAX handlers, headless APIs
routes/web.php none Custom pages, form handlers, redirects

Basic Usage

// themes/my-theme/routes/api.php

use Illuminate\Support\Facades\Route;
use Theme\MyTheme\Http\Controllers\Api\ProductSearchController;

Route::get('/products/search', ProductSearchController::class);
// → accessible at GET /api/products/search
// themes/my-theme/routes/web.php

use Illuminate\Support\Facades\Route;

Route::get('/custom-page', function () {
    return view('pages.custom');
});

API routes are standard Laravel routes — you can use controllers, middleware, route groups, rate limiting, and all other Laravel routing features.

Lightweight API Mode

API routes (/api/*) automatically benefit from lightweight mode: WordPress loads without plugins, reducing response times from ~1.3s to ~100ms. The theme is still loaded, so routes, controllers, and services work normally.

This is controlled by the wordpress.api_plugins configuration key:

// config/wordpress.php

// No plugins loaded (fastest — default)
'api_plugins' => [],

// Selective: only these plugins are loaded
'api_plugins' => ['woocommerce', 'advanced-custom-fields-pro'],

// Glob patterns: load all plugins matching a prefix
'api_plugins' => ['woocommerce*'],
// → matches woocommerce, woocommerce-subscriptions, woocommerce-payments, etc.

// Load all plugins (disable lightweight mode)
'api_plugins' => ['*'],
// or
'api_plugins' => null,

Since plugins are not loaded by default, API route handlers should use Eloquent / DB facade instead of WordPress or plugin functions (e.g., get_permalink(), wc_get_product()). When a handler needs a specific plugin, add it to the api_plugins array or use the function_exists() pattern to provide fallbacks:

// Works with or without WooCommerce loaded
if (function_exists('get_woocommerce_currency_symbol')) {
    $currency = get_woocommerce_currency_symbol();
} else {
    $currency = DB::table('options')
        ->where('option_name', 'woocommerce_currency')
        ->value('option_value') ?? 'EUR';
}

Plugin Routes

Plugins follow the same convention. Place a routes/api.php or routes/web.php in the plugin root:

my-plugin/
├── app/
│   └── Http/Controllers/Api/
│       └── StatsController.php
├── routes/
│   └── api.php        ← GET /api/stats
├── config/
└── my-plugin.php
// plugins/my-plugin/routes/api.php

use Illuminate\Support\Facades\Route;
use Plugin\MyPlugin\Http\Controllers\Api\StatsController;

Route::get('/stats', StatsController::class);

Comparison with WordPress REST API

Feature Theme/Plugin API routes WordPress REST API
Prefix /api/ /wp-json/
Framework Laravel router WP REST infrastructure
Plugins loaded Configurable (none by default) All
Response time ~100ms ~1.3s
Authentication Laravel middleware WP nonce / application passwords
ORM Eloquent / DB facade WP_Query / wpdb

Use theme/plugin API routes for performance-critical endpoints (search suggestions, autocomplete, dashboards). Use the WordPress REST API when you need full WordPress context (Gutenberg editor, third-party plugin integrations).

See also