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
27 changes: 15 additions & 12 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
> [!IMPORTANT]
> This update contains breaking changes for plugins. See [#19263](https://github.com/craftcms/cms/pull/19263) for details.

- Added `CraftCms\Cms\View\TemplateManager`, its `CraftCms\Cms\Support\Facades\Template` facade, and `CraftCms\Cms\Twig\Contracts\TwigRendererInterface`, with support for plugin-defined template renderers. `CraftCms\Cms\View\Contracts\TemplateRendererInterface`, `CraftCms\Cms\Twig\TwigRenderer`, and `CraftCms\Cms\Blade\BladeRenderer` were updated for manager-based rendering. Creators registered via `CraftCms\Cms\Support\Facades\Template::extend()` are replayed for each manager scope.
- `template()` and `pageTemplate()` now accept an optional template renderer name.
- `TemplateRendered` and `PageTemplateRendered` events now expose the final renderer name via `$rendererName`; the corresponding before events no longer expose renderer identity.
- Plugins should no longer define `extra.laravel.providers` in `composer.json`. ([#19263](https://github.com/craftcms/cms/pull/19263))
- Removed automatic plugin trait lifecycle hooks. ([#19263](https://github.com/craftcms/cms/pull/19263))
- Added `CraftCms\Cms\Cp\Components\Button`. ([#19248](https://github.com/craftcms/cms/pull/19248))
Expand Down Expand Up @@ -1272,9 +1275,9 @@ Moved the following controllers:

- Updated Twig `{% paginate %}` queries to use Laravel paginators and generate query-string pagination URLs based on the `pageTrigger` general config setting.
- Added `CraftCms\Cms\Twig\Twig` service for managing Twig environments, replacing the Twig management logic previously in `craft\web\View`.
- Added `CraftCms\Cms\Twig\TemplateRenderer` for rendering templates, replacing the rendering logic previously in `craft\web\View`.
- Added `CraftCms\Cms\View\TemplateManager` for rendering templates, replacing the rendering logic previously in `craft\web\View`.
- Added `CraftCms\Cms\Twig\PageLifecycle` for managing the page rendering lifecycle (head/body placeholder replacement), replacing the page lifecycle logic previously in `craft\web\View`.
- Added `CraftCms\Cms\Support\Facades\Twig` facade, resolving to `CraftCms\Cms\Twig\TemplateRenderer`.
- Added `CraftCms\Cms\Support\Facades\Twig` facade, resolving to `CraftCms\Cms\Twig\Twig`.
- Added `CraftCms\Cms\Twig\Environment`, moved from `craft\web\twig\Environment`.
- Added `CraftCms\Cms\Twig\TemplateResolver`.
- Added `CraftCms\Cms\Twig\TemplateLoader`.
Expand All @@ -1287,16 +1290,16 @@ Moved the following controllers:
- Deprecated `craft\web\View::registerCpTwigExtension()`. `CraftCms\Cms\Twig\Twig::registerExtension()` should be used instead.
- Deprecated `craft\web\View::registerSiteTwigExtension()`. `CraftCms\Cms\Twig\Twig::registerExtension()` should be used instead.
- Deprecated `craft\web\View::registerTwigExtension()`. `CraftCms\Cms\Twig\Twig::registerExtension()` should be used instead.
- Deprecated `craft\web\View::renderTemplate()`. `CraftCms\Cms\Twig\TemplateRenderer::renderTemplate()` or the `template()` helper should be used instead.
- Deprecated `craft\web\View::renderSandboxedTemplate()`. `CraftCms\Cms\Twig\TemplateRenderer::renderSandboxedTemplate()` or the `sandboxedTemplate()` helper should be used instead.
- Deprecated `craft\web\View::renderPageTemplate()`. `CraftCms\Cms\Twig\TemplateRenderer::renderPageTemplate()` or the `pageTemplate()` helper should be used instead.
- Deprecated `craft\web\View::renderString()`. `CraftCms\Cms\Twig\TemplateRenderer::renderString()` or the `renderString()` helper should be used instead.
- Deprecated `craft\web\View::renderSandboxedString()`. `CraftCms\Cms\Twig\TemplateRenderer::renderSandboxedString()` or the `renderSandboxedString()` helper should be used instead.
- Deprecated `craft\web\View::renderObjectTemplate()`. `CraftCms\Cms\Twig\TemplateRenderer::renderObjectTemplate()` or the `renderObjectTemplate()` helper should be used instead.
- Deprecated `craft\web\View::renderSandboxedObjectTemplate()`. `CraftCms\Cms\Twig\TemplateRenderer::renderSandboxedObjectTemplate()` or the `renderSandboxedObjectTemplate()` helper should be used instead.
- Deprecated `craft\web\View::normalizeObjectTemplate()`. `CraftCms\Cms\Twig\TemplateRenderer::normalizeObjectTemplate()` should be used instead.
- Deprecated `craft\web\View::getIsRenderingTemplate()`. `CraftCms\Cms\Twig\TemplateRenderer::isRenderingTemplate` should be used instead.
- Deprecated `craft\web\View::getIsRenderingPageTemplate()`. `CraftCms\Cms\Twig\TemplateRenderer::isRenderingPageTemplate` should be used instead.
- Deprecated `craft\web\View::renderTemplate()`. `CraftCms\Cms\View\TemplateManager::renderTemplate()` or the `template()` helper should be used instead.
- Deprecated `craft\web\View::renderSandboxedTemplate()`. `CraftCms\Cms\View\TemplateManager::renderSandboxedTemplate()` or the `sandboxedTemplate()` helper should be used instead.
- Deprecated `craft\web\View::renderPageTemplate()`. `CraftCms\Cms\View\TemplateManager::renderPageTemplate()` or the `pageTemplate()` helper should be used instead.
- Deprecated `craft\web\View::renderString()`. `CraftCms\Cms\View\TemplateManager::renderTwigString()` or the `renderString()` helper should be used instead.
- Deprecated `craft\web\View::renderSandboxedString()`. `CraftCms\Cms\View\TemplateManager::renderSandboxedString()` or the `renderSandboxedString()` helper should be used instead.
- Deprecated `craft\web\View::renderObjectTemplate()`. `CraftCms\Cms\View\TemplateManager::renderObjectTemplate()` or the `renderObjectTemplate()` helper should be used instead.
- Deprecated `craft\web\View::renderSandboxedObjectTemplate()`. `CraftCms\Cms\View\TemplateManager::renderSandboxedObjectTemplate()` or the `renderSandboxedObjectTemplate()` helper should be used instead.
- Deprecated `craft\web\View::normalizeObjectTemplate()`. `CraftCms\Cms\View\TemplateManager::normalizeObjectTemplate()` should be used instead.
- Deprecated `craft\web\View::getIsRenderingTemplate()`. `CraftCms\Cms\View\TemplateManager::isRenderingTemplate()` should be used instead.
- Deprecated `craft\web\View::getIsRenderingPageTemplate()`. `CraftCms\Cms\View\TemplateManager::isRenderingPageTemplate()` should be used instead.
- Deprecated `craft\web\twig\Environment`. `CraftCms\Cms\Twig\Environment` should be used instead.
- Deprecated `craft\web\View::EVENT_AFTER_CREATE_TWIG`. `CraftCms\Cms\Twig\Events\TwigCreated` should be used instead.
- Deprecated `craft\web\View::doesTemplateExist()`. `CraftCms\Cms\Twig\TemplateResolver::doesTemplateExist()` should be used instead.
Expand Down
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@
"@php vendor/bin/testbench serve --ansi"
],
"ci": [
"@php pint",
"@php pint --parallel",
"@rector",
"@phpstan",
"@tests",
Expand Down Expand Up @@ -219,6 +219,7 @@
"SiteGroups": "CraftCms\\Cms\\Support\\Facades\\SiteGroups",
"Sites": "CraftCms\\Cms\\Support\\Facades\\Sites",
"Structures": "CraftCms\\Cms\\Support\\Facades\\Structures",
"Template": "CraftCms\\Cms\\Support\\Facades\\Template",
"TemplateHooks": "CraftCms\\Cms\\Support\\Facades\\TemplateHooks",
"Twig": "CraftCms\\Cms\\Support\\Facades\\Twig",
"Updates": "CraftCms\\Cms\\Support\\Facades\\Updates",
Expand Down
78 changes: 58 additions & 20 deletions docs/blade.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ return [

Craft's template resolver still owns template lookup before either engine renders anything. Public/private template filtering, path containment, localized site template lookup, index template lookup, and registered template roots all apply before Craft decides whether the resolved file should be rendered by Twig or Blade.

Control panel template lookup still defaults to `twig` and `html`. Blade CP and plugin views are rendered through Laravel's view system, either by calling `view()` directly or by using `BladeRenderer`.
Control panel template lookup supports `twig`, `html`, and `blade.php`. Blade application and plugin views can also be rendered directly through Laravel's view system.

## Rendering Templates

Expand All @@ -35,13 +35,13 @@ The engine-neutral renderer resolves a Craft template name and then chooses Twig
```php
<?php

use CraftCms\Cms\View\TemplateRenderer;
use CraftCms\Cms\Support\Facades\Template;

$html = app(TemplateRenderer::class)->renderTemplate('articles/_entry', [
$html = Template::renderTemplate('articles/_entry', [
'entry' => $entry,
]);

$pageHtml = app(TemplateRenderer::class)->renderPageTemplate('articles/show', [
$pageHtml = Template::renderPageTemplate('articles/show', [
'entry' => $entry,
]);
```
Expand All @@ -55,31 +55,39 @@ $html = template('articles/_entry', ['entry' => $entry]);
$pageHtml = pageTemplate('articles/show', ['entry' => $entry]);
```

Automatic selection uses the first registered renderer that supports the resolved file. Pass a `TemplateEngine` or custom renderer name to force a renderer:

```php
<?php

use CraftCms\Cms\View\TemplateEngine;

$html = template('articles/_entry', ['entry' => $entry], renderer: TemplateEngine::Blade);
```

`renderPageTemplate()` and `pageTemplate()` wrap Blade templates in Craft's page lifecycle, so queued head/body resources are rendered into the page placeholders.

If you already have a Laravel view name or a file path, use `BladeRenderer` directly:
If you already have a Laravel view name or file path, resolve the low-level `BladeRenderer` through the `Template` facade:

```php
<?php

use CraftCms\Cms\Blade\BladeRenderer;
use CraftCms\Cms\View\TemplateEngine;
use CraftCms\Cms\View\TemplateMode;
use CraftCms\Cms\Support\Facades\Template;

$renderer = app(BladeRenderer::class);
$renderer = Template::renderer(TemplateEngine::Blade);

$partial = $renderer->renderTemplate('my-plugin::tokens.index', [
'token' => $token,
]);

$page = $renderer->renderPageTemplate('my-plugin::screens.edit', [
'entry' => $entry,
]);
], TemplateMode::Cp);

$inline = $renderer->renderString('Hello, {{ $name }}', [
'name' => 'Craft',
]);
], TemplateMode::Cp);
```

`renderView()` and `renderPageView()` defer to Laravel view resolution. Use them for application and plugin views that are registered with Laravel's view finder. Slash-style view names and namespaced views both work:
Slash-style view names and namespaced views both work:

```php
<?php
Expand All @@ -89,7 +97,37 @@ view('my-plugin::tokens.index', $variables);
view('my-plugin::nested/screen', $variables);
```

Use `view()->file($path, $variables)` or `BladeRenderer::renderFile()` only when you intentionally want to render a concrete file path.
Use `view()->file($path, $variables)` when you intentionally want to render a concrete file path.

## Renderers

`TemplateManager` is a scoped Laravel manager. Its built-in renderer names are `twig` and `blade`, represented by `TemplateEngine`. Custom renderers implement `TemplateRendererInterface`, including file support, resolved-template rendering, and inline-string rendering. A replacement for the built-in Twig renderer must implement `TwigRendererInterface`.

Register renderers once, typically from a service provider’s `boot()` method, through the `Template` facade. The registration is replayed whenever Laravel creates a new manager scope:

```php
<?php

use App\Templates\MarkdownRenderer;
use CraftCms\Cms\Support\Facades\Template;
use Illuminate\Contracts\Container\Container;
use Illuminate\Support\ServiceProvider;

class PluginServiceProvider extends ServiceProvider
{
public function boot(): void
{
Template::extend(
'markdown',
static fn (Container $container) => $container->make(MarkdownRenderer::class),
);
}
}
```

New renderer names are appended after the built-in renderers. Re-registering an existing name replaces its creator without changing its selection position. Like other Laravel managers, an already-resolved renderer remains cached in the current scope until `forgetRenderers()` is called. New manager scopes receive the latest creator automatically and resolve their own renderer instances.

Don’t wrap `extend()` in `callAfterResolving()`, because the manager registers its own scope replay. Renderer creators should resolve scoped dependencies from the supplied container rather than capturing request-specific instances.

## Routes

Expand Down Expand Up @@ -388,21 +426,21 @@ Craft exposes engine-neutral rendering events for both Twig and Blade:
- `CraftCms\Cms\View\Events\PageTemplateRendering`
- `CraftCms\Cms\View\Events\PageTemplateRendered`

Each event includes the template engine:
Before events run before template resolution, so listeners can mutate the template name, variables, or template mode before a renderer is selected. After events expose the final renderer name as a string:

```php
<?php

use CraftCms\Cms\View\Events\TemplateRendering;
use CraftCms\Cms\View\Events\TemplateRendered;
use CraftCms\Cms\View\TemplateEngine;
use Illuminate\Support\Facades\Event;

Event::listen(TemplateRendering::class, function (TemplateRendering $event) {
if ($event->engine !== TemplateEngine::Blade) {
Event::listen(TemplateRendered::class, function (TemplateRendered $event) {
if ($event->rendererName !== TemplateEngine::Blade->value) {
return;
}

$event->variables['fromListener'] = true;
$event->output = trim($event->output);
});
```

Expand Down
26 changes: 7 additions & 19 deletions src/Blade/BladeRenderer.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,11 @@

namespace CraftCms\Cms\Blade;

use CraftCms\Cms\View\BaseTemplateRenderer;
use CraftCms\Cms\View\Contracts\TemplateRendererInterface;
use CraftCms\Cms\View\TemplateMode;
use Illuminate\Container\Attributes\Scoped;
use Illuminate\Support\Facades\Blade;

#[Scoped]
class BladeRenderer extends BaseTemplateRenderer
class BladeRenderer implements TemplateRendererInterface
{
public function supports(string $file): bool
{
Expand All @@ -19,30 +17,20 @@ public function supports(string $file): bool

public function renderTemplate(
string $template,
array $variables,
array $variables = [],
?TemplateMode $templateMode = null,
?string $resolvedTemplate = null,
): string {
return $this->renderInternal(
template: $template,
variables: $variables,
templateMode: $templateMode,
render: fn (string $template, array $variables) => $resolvedTemplate
? view()->file($resolvedTemplate, $variables)->render()
: view($template, $variables)->render()
);
return $resolvedTemplate
? view()->file($resolvedTemplate, $variables)->render()
: view($template, $variables)->render();
}

public function renderString(
string $template,
array $variables = [],
TemplateMode $templateMode = TemplateMode::Site,
): string {
return $this->renderInternal(
'string:'.$template,
$variables,
$templateMode,
fn () => Blade::render($template, $variables),
);
return Blade::render($template, $variables);
}
}
4 changes: 2 additions & 2 deletions src/Route/TemplateRoute.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@
namespace CraftCms\Cms\Route;

use CraftCms\Cms\Cms;
use CraftCms\Cms\Support\Facades\Template;
use CraftCms\Cms\Support\Str;
use CraftCms\Cms\View\TemplateMode;
use CraftCms\Cms\View\TemplateRenderer;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;

Expand All @@ -29,7 +29,7 @@ public function handle(Request $request): Response

abort_if(Cms::config()->headlessMode && $request->isSiteRequest(), 404);

return response(app(TemplateRenderer::class)->renderPageTemplate(
return response(Template::renderPageTemplate(
$template,
$this->variables,
publicOnly: $this->publicOnly,
Expand Down
36 changes: 36 additions & 0 deletions src/Support/Facades/Template.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php

declare(strict_types=1);

namespace CraftCms\Cms\Support\Facades;

use CraftCms\Cms\View\TemplateManager;
use Illuminate\Support\Facades\Facade;
use Override;

/**
* @method static \CraftCms\Cms\View\Contracts\TemplateRendererInterface renderer(\CraftCms\Cms\View\TemplateEngine|string|null $renderer = null)
* @method static TemplateManager extend(\CraftCms\Cms\View\TemplateEngine|string $renderer, \Closure $callback)
* @method static TemplateManager forgetRenderers()
* @method static bool isRenderingTemplate()
* @method static bool isRenderingPageTemplate()
* @method static string renderTemplate(string $template, array $variables = [], \CraftCms\Cms\View\TemplateMode|null $templateMode = null, bool $publicOnly = false, \CraftCms\Cms\View\TemplateEngine|string|null $renderer = null)
* @method static string renderSandboxedTemplate(string $template, array $variables = [], \CraftCms\Cms\View\TemplateMode|null $templateMode = null, bool $publicOnly = false)
* @method static string renderPageTemplate(string $template, array $variables = [], \CraftCms\Cms\View\TemplateMode|null $templateMode = null, bool $publicOnly = false, \CraftCms\Cms\View\TemplateEngine|string|null $renderer = null)
* @method static string renderString(string $template, array $variables = [], \CraftCms\Cms\View\TemplateMode $templateMode = \CraftCms\Cms\View\TemplateMode::Site, \CraftCms\Cms\View\TemplateEngine|string|null $renderer = null)
* @method static string renderTwigString(string $template, array $variables = [], \CraftCms\Cms\View\TemplateMode $templateMode = \CraftCms\Cms\View\TemplateMode::Site, bool $escapeHtml = false)
* @method static string renderSandboxedString(string $template, array $variables = [], \CraftCms\Cms\View\TemplateMode $templateMode = \CraftCms\Cms\View\TemplateMode::Site, bool $escapeHtml = false)
* @method static string renderObjectTemplate(string $template, mixed $object, array $variables = [], \CraftCms\Cms\View\TemplateMode $templateMode = \CraftCms\Cms\View\TemplateMode::Site)
* @method static string renderSandboxedObjectTemplate(string $template, mixed $object, array $variables = [], \CraftCms\Cms\View\TemplateMode $templateMode = \CraftCms\Cms\View\TemplateMode::Site)
* @method static string normalizeObjectTemplate(string $template)
*
* @see TemplateManager
*/
class Template extends Facade
{
#[Override]
protected static function getFacadeAccessor(): string
{
return TemplateManager::class;
}
}
8 changes: 5 additions & 3 deletions src/SystemMessage/Actions/FormatSystemMessageMailAction.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,15 @@
use CraftCms\Cms\SystemMessage\Data\FormattedSystemMessageMail;
use CraftCms\Cms\SystemMessage\Data\RenderedSystemMessage;
use CraftCms\Cms\SystemMessage\SystemMessageRenderContext;
use CraftCms\Cms\Twig\TwigRenderer;
use CraftCms\Cms\View\TemplateEngine;
use CraftCms\Cms\View\TemplateManager;
use CraftCms\Cms\View\TemplateMode;

readonly class FormatSystemMessageMailAction
{
public function __construct(
private SystemMessageRenderContext $renderContext,
private TwigRenderer $templateRenderer,
private TemplateManager $templateManager,
) {}

public function handle(RenderedSystemMessage $message, MailSettings $settings): FormattedSystemMessageMail
Expand All @@ -39,10 +40,11 @@ public function handle(RenderedSystemMessage $message, MailSettings $settings):
$htmlBody = $this->renderContext->run(
siteId: $message->siteId,
language: $message->language,
callback: fn () => $this->templateRenderer->renderTemplate(
callback: fn () => $this->templateManager->renderTemplate(
template: $settings->template,
variables: $viewData,
templateMode: TemplateMode::Site,
renderer: TemplateEngine::Twig,
),
);

Expand Down
Loading
Loading