Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased](https://github.com/Pollora/framework/compare/v13.35.0...develop)

### Fixed
- A plugin that answers from `template_redirect` and includes the theme's query template itself (`include get_query_template('404')`, WooCommerce's Review Order page) printed the Blade source of the view: the `{type}_template` filters now hand back a loader that renders it while `template_redirect` runs (#419)
- `template_redirect` ran before the application's providers had booted — WordPress is loaded from a provider's boot, the theme's providers boot after it — so a view rendered from it lacked the theme's view composers and shared data. It now runs once every provider has booted, still before routing, and `pollora_loaded` after it (#419)
- Trackbacks answered 500: `wp-trackback.php` was required from a method, where it found no `$wp` and loaded WordPress a second time (#419)

## [v13.35.0](https://github.com/Pollora/framework/compare/v13.34.6...v13.35.0) - 2026-10-06

### Added
Expand Down
6 changes: 6 additions & 0 deletions phpstan-baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,12 @@ parameters:
count: 1
path: src/ThirdParty/WooCommerce/Infrastructure/Services/WooCommerce.php

-
message: '#^Call to an undefined method Illuminate\\Contracts\\View\\View\:\:makeLoader\(\)\.$#'
identifier: method.notFound
count: 1
path: src/View/Application/UseCases/MakeTemplateIncludableUseCase.php

-
message: '#^Constant WP_CLI_VERSION not found\.$#'
identifier: constant.notFound
Expand Down
3 changes: 2 additions & 1 deletion rector.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,10 @@
// $_SERVER directly is the point rather than an oversight.
ServerVariableToRequestFacadeRector::class => [
__DIR__.'/src/WordPress/QueryTrait.php',
// Its test sets $_SERVER['SCRIPT_FILENAME'] to drive that method;
// Its tests set $_SERVER['SCRIPT_FILENAME'] to drive that method;
// going through the facade would stop it reaching the code at all.
__DIR__.'/tests/Unit/WordPress/LaravelServingRequestTest.php',
__DIR__.'/tests/Unit/WordPress/DeferredTemplateRedirectTest.php',
],
__DIR__.'/tests/Unit/helpers.php',
// Binding sources each broken on purpose (an untyped field, a private
Expand Down
48 changes: 48 additions & 0 deletions src/View/Application/UseCases/MakeTemplateIncludableUseCase.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<?php

declare(strict_types=1);

namespace Pollora\View\Application\UseCases;

use Illuminate\Contracts\View\Factory as ViewFactory;
use Pollora\View\Domain\Contracts\TemplateFinderInterface;

/**
* Turns a Blade template into a file PHP can include.
*
* Included as it is, a `.blade.php` file prints its own source. The loader
* renders the view instead, with the composers and shared data of the
* application, as WooCommerce templates already are.
*/
class MakeTemplateIncludableUseCase
{
public function __construct(
private readonly TemplateFinderInterface $templateFinder,
private readonly ViewFactory $viewFactory
) {}

/**
* @param string $templatePath Template path WordPress resolved
* @return string The loader of the view, or the path unchanged when it is not a Blade view
*/
public function execute(string $templatePath): string
{
if (! str_ends_with($templatePath, '.blade.php')) {
return $templatePath;
}

$realPath = realpath($templatePath);

if ($realPath === false) {
return $templatePath;
}

$viewName = trim((string) $this->templateFinder->getViewNameFromPath($realPath), '\\/.');

if ($viewName === '' || ! $this->viewFactory->exists($viewName)) {
return $templatePath;
}

return $this->viewFactory->make($viewName)->makeLoader();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,13 @@ public function execute(): void
$this->hierarchyFilter->extendTemplateHierarchy(...),
10
);

// `404_template`… : for plugins that include get_query_template() themselves
$this->filter->add(
str_replace('_hierarchy', '', $filterName),
$this->hierarchyFilter->includableTemplate(...),
PHP_INT_MAX
);
}

// Theme templates filter
Expand Down
10 changes: 10 additions & 0 deletions src/View/Domain/Contracts/TemplateHierarchyFilterInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,16 @@ public function extendTemplateHierarchy(array $files): array;
*/
public function resolveTemplateInclude(string $template): string;

/**
* The template a `{type}_template` filter hands back: a file PHP can
* include while `template_redirect` runs, when a plugin includes the
* query template itself instead of going through `template_include`.
*
* @param string $template Template path WordPress resolved
* @return string Template path to use (original or a loader)
*/
public function includableTemplate(string $template): string;

/**
* Add Blade compatibility for theme templates.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
use Pollora\Hook\Domain\Contract\Action;
use Pollora\Hook\Domain\Contract\Filter;
use Pollora\View\Application\Services\TemplateHierarchyService;
use Pollora\View\Application\UseCases\MakeTemplateIncludableUseCase;
use Pollora\View\Application\UseCases\RegisterTemplateHierarchyFiltersUseCase;
use Pollora\View\Application\UseCases\ResolveBladeTemplateUseCase;
use Pollora\View\Domain\Contracts\TemplateFinderInterface;
Expand Down Expand Up @@ -63,7 +64,8 @@ private function registerDomainContracts(): void
$this->app->bind(TemplateHierarchyFilterInterface::class, fn (Application $app): WordPressTemplateHierarchyFilter => new WordPressTemplateHierarchyFilter(
$app->make(TemplateFinderInterface::class),
$app->make(ResolveBladeTemplateUseCase::class),
$app->get('view')->getFinder()
$app->get('view')->getFinder(),
$app->make(MakeTemplateIncludableUseCase::class)
));
}

Expand All @@ -78,6 +80,12 @@ private function registerUseCases(): void
$app->get('view')
));

// Make Template Includable Use Case
$this->app->bind(MakeTemplateIncludableUseCase::class, fn (Application $app): MakeTemplateIncludableUseCase => new MakeTemplateIncludableUseCase(
$app->make(TemplateFinderInterface::class),
$app->get('view')
));

// Register Template Hierarchy Filters Use Case
$this->app->bind(RegisterTemplateHierarchyFiltersUseCase::class, fn (Application $app): RegisterTemplateHierarchyFiltersUseCase => new RegisterTemplateHierarchyFiltersUseCase(
$app->make(Filter::class),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use Illuminate\Support\Str;
use Illuminate\View\FileViewFinder;
use Illuminate\View\ViewFinderInterface;
use Pollora\View\Application\UseCases\MakeTemplateIncludableUseCase;
use Pollora\View\Application\UseCases\ResolveBladeTemplateUseCase;
use Pollora\View\Domain\Contracts\TemplateFinderInterface;
use Pollora\View\Domain\Contracts\TemplateHierarchyFilterInterface;
Expand All @@ -23,7 +24,8 @@ class WordPressTemplateHierarchyFilter implements TemplateHierarchyFilterInterfa
public function __construct(
private readonly TemplateFinderInterface $templateFinder,
private readonly ResolveBladeTemplateUseCase $resolveBladeTemplateUseCase,
private readonly ViewFinderInterface $viewFinder
private readonly ViewFinderInterface $viewFinder,
private readonly MakeTemplateIncludableUseCase $makeTemplateIncludable
) {}

/**
Expand Down Expand Up @@ -56,6 +58,23 @@ public function resolveTemplateInclude(string $template): string
return $this->resolveBladeTemplateUseCase->execute($template);
}

/**
* A plugin that answers from `template_redirect` may include the query
* template itself (`include get_query_template('404')`, WooCommerce's
* Review Order page): it gets a loader that renders the view. Outside
* `template_redirect` the path is left alone — the FrontendController
* resolves the hierarchy later and needs the Blade path, and WordPress's
* own template loader must still meet the raw-output guard.
*/
public function includableTemplate(string $template): string
{
if (! function_exists('doing_action') || ! doing_action('template_redirect')) {
return $template;
}

return $this->makeTemplateIncludable->execute($template);
}

/**
* Add Blade compatibility for theme templates.
*/
Expand Down
98 changes: 64 additions & 34 deletions src/WordPress/QueryTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
namespace Pollora\WordPress;

use Exception;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;

Expand Down Expand Up @@ -75,11 +76,15 @@ private function isWordPressInstalled(): bool
* Run the WordPress bootstrap process.
*
* This method initializes WordPress and handles special request types
* like robots.txt, favicon, feeds, trackbacks, and AJAX requests. AJAX requests
* are handled early and bypass the normal WordPress query processing to prevent
* them from reaching the Laravel routing system. Normal template resolution
* like robots.txt, favicon, feeds and trackbacks. Normal template resolution
* is delegated to the Laravel routing system.
*
* `template_redirect` runs once every provider has booted, still before
* routing: WordPress is loaded from this provider's boot, and the theme's
* providers, registered by functions.php, boot after it. A plugin that
* renders a page from `template_redirect` (WooCommerce's Review Order 404)
* then gets the theme's view composers and shared data.
*
* @throws \RuntimeException If WordPress core functions are not available
*/
protected function runWp(): void
Expand All @@ -88,41 +93,66 @@ protected function runWp(): void
throw new \RuntimeException('The WordPress core functions are not available. Ensure WordPress is loaded.');
}

if ($this->laravelIsServingTheRequest()) {
// Initialize WordPress for the current request
wp();

if (wp_using_themes()) {
$this->action->do('template_redirect');
}

// Handle special request types that should bypass Laravel routing
if (is_robots()) {
$this->action->do('do_robots');
exit;
}

if (is_favicon()) {
$this->action->do('do_favicon');
exit;
}

if (is_feed()) {
do_feed();
exit;
}

if (is_trackback()) {
require_once ABSPATH.'wp-trackback.php';
exit;
}
if (! $this->laravelIsServingTheRequest()) {
$this->action->do('pollora_loaded');

return;
}

// Initialize WordPress for the current request
wp();

App::booted(function (): void {
$this->withWordPressErrorHandling(function (): void {
$this->handleTemplateRedirect();

// For normal requests, let Laravel routing handle template resolution
// Do not load WordPress template-loader.php as we use FrontendController instead
$this->action->do('pollora_loaded');
});
});
}

/**
* Fire `template_redirect`, then answer the requests WordPress serves itself.
*/
private function handleTemplateRedirect(): void
{
if (wp_using_themes()) {
$this->action->do('template_redirect');
}

// Handle special request types that should bypass Laravel routing
if (is_robots()) {
$this->action->do('do_robots');
exit;
}

if (is_favicon()) {
$this->action->do('do_favicon');
exit;
}

if (is_feed()) {
do_feed();
exit;
}

// For normal requests, let Laravel routing handle template resolution
// Do not load WordPress template-loader.php as we use FrontendController instead
$this->action->do('pollora_loaded');
if (is_trackback()) {
// wp-trackback.php is written for the global scope: without $wp it
// loads WordPress a second time, and it reads $posts and $wpdb.
global $wp, $wpdb, $posts;

require_once ABSPATH.'wp-trackback.php';
exit;
}
}

/**
* Run a callback with WordPress-safe error handling.
*/
abstract private function withWordPressErrorHandling(callable $callback): void;

/**
* Whether this request is Laravel's to serve.
*
Expand Down
Loading
Loading