Enables Laravel Blade templates in Craft CMS as a modern alternative to Twig.
Tagged version 0.1.
This is the state of the project at the time it was handed over to the client.
At this point, it is acknowledged that portions of the code and documentation are AI-generated, not systematically tested, and may be incomplete or incorrect.
See Architecture Overview for implementation details.
A client is considering porting a Laravel application to Craft (multi-site, drafts, etc.) that has a large number of Blade templates. This plugin is intended to simplify the evaluation and enable a step-by-step approach.
It is not intended to be a comprehensive or permanent solution, but merely to support this specific project.
Functionality will only be fixed or improved as needed.
This plugin requires Craft CMS 5.8.0 or later, and PHP 8.2 or later.
Internally, it uses the Laravel Illuminate packages (including Blade) version 10.x, matching the version Craft CMS 5.x uses for Laravel collections.
Add this to the composer.json file in your project root to require the plugin:
{
"require": {
"wsydney76/craft5-blade": "^0.1.0"
},
"repositories": [
{
"type": "vcs",
"url": "https://github.com/wsydney76/craft5-blade"
}
]
}Then run composer update to install the plugin.
Install the plugin: ddev craft plugin/install _blade.
Blade supports both Control Panel (CP) settings and a config file.
Once the plugin is installed, you can configure the runtime in the Craft CP:
- Settings → Plugins → Blade (
_blade)
The CP settings page currently exposes these settings:
bladeViewsPath— Base path where Blade views live (e.g.@root/resources/views).bladeCachePath— Directory where compiled Blade templates are written (must be writable).bladeRoutePrefixes— Comma-separated route prefixes for the direct URL rendering route.bladeComponentPaths— Anonymous component directories, optionally namespaced by prefix (e.g.ui→<x-ui::*>).
If you want to customize Blade settings via code, create a config file at config/_blade.php.
If a setting is defined in config/_blade.php, it overrides the CP value.
Those overridden fields will show a warning in the CP and cannot be edited there.
<?php
use craft\helpers\App;
return [
'bladeViewsPath' => App::env('BLADE_VIEWS_PATH') ?? '@templates/_blade',
'bladeCachePath' => App::env('BLADE_CACHE_PATH') ?? '@runtime/blade/cache',
// Anonymous component roots (optional)
'bladeComponentPaths' => [
['path' => '@templates/_shared', 'prefix' => 'shared'],
],
// Route prefix(es) for direct rendering URLs
// e.g. /pages/articles/list -> view "pages.articles.list"
'bladeRoutePrefixes' => 'pages,blog',
];Settings:
bladeViewsPath— Path to the Blade views directory. Defaults to@root/resources/views.bladeCachePath— Path to the compiled Blade template cache directory. Defaults to@runtime/blade/cache.bladeComponentPaths— Additional anonymous component paths with (optional) prefixes.bladeRoutePrefixes— Prefixes for URL routes pointing directly to Blade templates. Defaults toblade. Comma-separated values; multiple routes will be registered.
Path values support Craft aliases (e.g. @root, @runtime).
If the bladeViewsPath is changed, you may need to adjust your IDE settings to recognize Blade templates in that directory.
See Customize for additional configuration options.
- Full Blade syntax support — Use Laravel Blade features including components, directives, and control structures.
- Blade components — Create and use reusable components with props.
- Custom directives — Define custom Blade directives for your application.
- Twig integration — Call Twig templates from Blade using the
@renderTwig()directive. - Global data sharing — Access Craft global variables in Blade templates (like
craft, site name, etc.). - Template inheritance — Use Blade's powerful layout system with
@extendsand@section.
- Does not support Laravel-specific helper functions and Blade directives that depend on Laravel features not present in Craft CMS.
- Does not offer equivalent functionality for some advanced Craft Twig features/tags (e.g.
nav). - Does not fully support Template localization
- Currently only used with the Entry element type. Other element types may work but are not yet tested.
- The central
BladeBootstrap.phpclass is mostly AI-generated and may look like a complete mess for Laravel/Blade experts. But it works for the tested use cases... - Not yet reviewed in terms of performance/memory usage.
- Support for Craft's Twig functions and filters is experimental.
- Does not support Livewire-like reactive components out of the box.
The main entry point for interacting with Blade is the wsydney76\blade\View class.
Missing methods can be added as needed.
Create your Blade templates in the resources/views directory (or the path configured).
The template cache is stored in storage/runtime/blade/cache (or the path configured).
Create .blade.php files in your views directory:
<x-layout :title="$entry->title">
<article class="prose prose-lg max-w-none">
@if ($entry->image)
<x-image :image="$entry->image->one()" width="1024" height="400" />
@endif
<h1 class="text-3xl font-bold">{{ $entry->title }}</h1>
<x-meta :entry="$entry" />
@if ($entry->teaser)
<p class="my-4 text-xl font-bold">{{ $entry->teaser }}</p>
@endif
@markdown($entry->body)
<div class="mx-auto mt-8">
<x-blocks :blocks="$entry->bodyContent->all()" />
</div>
</article>
</x-layout>Create reusable components in resources/views/components/ (or the paths configured):
Anonymous components are just Blade views in your components folder.
@props(['title' => 'My Site'])
<!DOCTYPE html>
<html>
<head>
...
<title>{{ $title }}</title>
{!! $craft->vite->script('/resources/js/app.js', false) !!}
</head>
<body>
@renderTwig('_layouts/nav.twig')
...
<main>
{{ $slot }}
</main>
...
</body>
</html>@props(['image' => null, 'width' => null, 'height' => null])
@if ($image)
{!! $image->getImg(['width' => $width, 'height' => $height]) !!}
@endifUse the components like this:
<x-layout title="My Page">
<x-image :image="$entry->image->one()" width="600" height="400" />
... content ...
</x-layout>Example: modules/main/components/EntriesList.php
<?php
namespace modules\main\components;
use craft\elements\Entry;
use Illuminate\Contracts\View\View;
use Illuminate\Support\Collection;
use Illuminate\View\Component;
class EntriesList extends Component
{
public ?string $title = null;
public ?Collection $entries = null;
public function __construct(
string $section = '*',
?string $title = null,
?int $limit = 5,
?string $orderBy = 'postDate desc',
) {
$this->title = $title;
$this->entries = Entry::find()
->section($section)
->limit($limit)
->orderBy($orderBy)
->collect();
}
public function render(): View
{
return view('components.entries-list');
}
}Example: resources/views/components/entries-list.php
When using class-based components, prefer using the component’s public props directly.
(Depending on your Illuminate/View version and how the engine is bootstrapped, $component may not be available.)
@props(['title' => null, 'entries' => []])
@if ($entries->count())
@if ($title)
<h3>{{ $title }}</h3>
@endif
<ul>
@foreach ($entries as $entry)
<li>
<a href="{{ $entry->url }}" class="text-blue-600 hover:underline">
{{ $entry->title }}
</a>
<span class="text-sm text-gray-600">{{ $entry->postDate }}</span>
</li>
@endforeach
</ul>
@endifRegister class-based components in your module/plugin bootstrap:
use modules\main\components\EntriesList;
use wsydney76\blade\View;
View::component('entries-list', EntriesList::class);<x-entries-list section="article" :title="t('Latest via class component')" />Supports dynamic components:
@foreach ($entry->myMatrixField->all() as $block)
<x-dynamic-component
:component="'blocks.' . $block->type->handle"
:block="$block" />
@endforeachRender Blade templates from plugins or controllers:
use wsydney76\blade\View;
View::renderTemplate('mytemplate', [
'entries' => $entries
])Accepts an array of views, the first existing one will be used:
View::renderTemplate(['custom.template', 'fallback.template'], [
'data' => $data
])You can also use a more familiar syntax:
return view('greeting')
->with('name', 'Victoria')
->with('occupation', 'Astronaut');
return View::first(['custom.admin', 'admin'], $data);Call Twig templates using the @renderTwig() directive:
@renderTwig('_layouts/nav.twig', [...someData...])Call Blade templates using the renderBlade() function:
{{ renderBlade('component.blocks.text', { text: 'Craft', class: 'text-xl' }) }}This can be used to embed Blade components in Twig layouts, so that you can gradually migrate templates.
{% extends "_layouts/main.twig" %}
{% block proseContent %}
{{ renderBlade('film.filmdetails', {entry}) }}
{% endblock %}In order to use Blade templates for Craft entries, set the template in the section settings
action:main/blog/showby route (controller action)
use wsydney76\blade\View;
...
public function actionShow(): string
{
$entry = Craft::$app->urlManager->getMatchedElement();
if (!$entry) {
throw new NotFoundHttpException('Page not found');
}
$prevNextCriteria = [
'section' => $entry->section->handle,
];
return View::renderTemplate('article', [
'entry' => $entry,
'prev' => $entry->getPrev($prevNextCriteria),
'next' => $entry->getNext($prevNextCriteria)
]);
}The current element can be accessed via Craft::$app->urlManager->getMatchedElement().
Craft will automatically set the correct content type header text/html for the response.
blade:blog.show(by prefix)blog/show.blade.php(by file path/extension, relative to bladeViewsPath setting).
The current element is available in Blade automatically:
- It’s injected into the view context based on the element’s short class name (lowercased), e.g.
Entry→$entry,Product→$product.
The plugin registers a site route that can render a Blade view directly from a URL.
This is mainly used for routes that do not correspond to Craft elements, e.g. static pages or special endpoints.
- Default prefix:
blade - Config key:
bladeRoutePrefixes(plugin settings /config/_blade.php). Comma-separated values; multiple routes will be registered.
Examples (default prefix):
/blade/articlesrenders Blade viewblade.articles/blade/articles/list/bydaterenders Blade viewblade.articles.list.bydate
Notes:
- The
{view}portion is treated as a slash-delimited path and is normalized to a dotted view name. - The endpoint is accessible anonymously by default.
- You’re responsible for implementing appropriate security measures (e.g. access control, input validation, and sanitization of user-provided data).
To customize the prefix, add this to config/_blade.php:
return [
'bladeRoutePrefixes' => 'views,pages'
];Custom controller actions can be set up using the usual Craft mechanisms and finally render Blade templates using View::renderTemplate().
By default, Blade has no awareness of Craft's template localization.
As a workaround, pass an array of possible localized templates to Blade where needed:
PHP:
$currentSite = Craft::$app->getSites()->getCurrentSite();
return View::renderTemplate(["{$currentSite->handle}.article.index", 'article.index'], [...]);Blade:
@includeFirst(["{$currentSite->handle}.meta", 'meta'], ['entry' => $entry])Components are not supported.
Some helper functions are available to simplify this, but not fully tested yet:
PHP:
View::renderLocalized('article.show', [...]);Blade:
@includeLocalized('meta', ['entry' => $entry]) Experimental.
Handle pagination in the controller using the View::paginate() helper method:
use wsydney76\blade\View;
...
public function actionIndex()
{
return View::renderTemplate(
'posts.index',
[
'entry' => Craft::$app->urlManager->getMatchedElement(),
...View::paginate(Entry::find()->section('post')->limit(10), 'posts', 'pageInfo')
],
);
}The View::paginate() method accepts:
$query- The element query, optionally with limit set$resultsKey- The key name for results (default: 'elements')$pageInfoKey- The key name for page info (default: 'pageInfo')$config- Optional configuration array (pageSize, currentPage, etc.).
An array with the keys specified in $resultsKey and $pageInfoKey is returned, where
$resultsKeycontains a collection with the paginated results$pageInfoKeycontains an instance ofcraft\web\twig\variables\Paginate. See docs for details.
To determine the current page, Craft::$app->request->getPageNum() is used, respecting the pageTrigger general setting.
To determine the page size, either the limit set on the query or the pageSize config is used. If not set, defaults to 100.
Alternatively, handle pagination directly in the Blade template using the @paginate() directive:
@paginate(Entry::find()->section('post')->limit(4), 'posts', 'pageInfo')Both methods provide $posts with the page results and $pageInfo with pagination information:
<ul>
@foreach($posts as $post)
<li>{{ $post->title }}</li>
@endforeach
</ul>
<p>
Showing page {{ $pageInfo->currentPage }} of {{ $pageInfo->totalPages }}.
@if($pageInfo->currentPage > 1)
<a href="{{ $pageInfo->getPrevUrl() }}">Previous page</a>
@endif
@if($pageInfo->currentPage < $pageInfo->totalPages)
<a href="{{ $pageInfo->getNextUrl() }}">Next page</a>
@endif
</p>Experimental.
The @cache directive pair mirrors Craft’s Twig {% cache %} tag behavior.
Basic usage (no options):
@cache
... the content to cache ...
@endcacheWith options (all keys optional):
@cache([
'key' => 'thekey',
'global' => true,
'duration' => '1 hour',
'expiration' => 1735689600,
])
Hallo
@endcacheConditional caching (Craft Twig {% cache if ... %} / {% cache unless ... %} equivalents):
@cache(['if' => craft()->app->request->isMobileBrowser()])
This is only cached for mobile browsers.
@endcache
@cache(['unless' => $currentUser])
This is cached unless a user is logged in.
@endcacheOptions:
key(string): Cache key override. If omitted, a deterministic key is generated.global(bool): Whether the cache is global. Default:false.duration(?string): Cache duration (e.g.'1 hour'). Default:null.expiration(mixed): Explicit expiration value (timestamp/DateTime/etc.). Default:null.if(mixed): Only use the cache when this is truthy.unless(mixed): Only use the cache when this is falsey.
Note: Uses Craft's TemplateCaches service under the hood, so (in theory) should behave the same, including cache invalidation.
Cache fragments created via Twig and via Blade are interoperable: if both use the same cache key and are 'global', they refer to the same underlying Craft template cache entry and can be reused interchangeably.
All Craft global variables (except _globals) are available in Blade templates:
<p>App Name: {{ $systemName }}</p>
<p>Site URL: {{ $siteUrl }}</p>
<p>User name: {{ $currentUser->name }}</p>
<p>Craft variable: {{ $craft->app->language }}</p>
@php($entries = $craft->entries()->section('*')->all())For a mapping of Twig’s built-in functions and filters to Blade helper functions, see the TWIG_MAPPINGS.md file.
Experimental.
As a first step towards supporting Craft's Twig functions and filters in Blade templates, the Craft Twig extension was fed into an AI model, and the functions and filters were converted to standalone PHP functions in BladeHelpers.php and BladeFilters.php, along with some docs.
These results are published here unedited and untested for evaluation; no guarantees are made regarding completeness or correctness.
Blade helper functions are automatically available in your templates and include:
- Craft CMS functions - URL helpers, config helpers, element queries, etc.
- Twig filters as functions - Most Craft CMS Twig filters are available as PHP functions for use in Blade (see
BLADE_FILTERS_MAPPING.mdfor a complete list) - HTML helpers - Common HTML output functions
- Translation helper -
__()function for translation
See BLADE_FUNCTIONS_MAPPING.md and HELPER_FILTERS_MAPPING.md for mapping to Craft's core functionality.
See BLADE_FUNCTIONS_QUICK_REFERENCE.md and BLADE_FILTERS_QUICK_REFERENCE for mapping to Craft's core functionality.
In the current state of this PoC, no further work is planned except for fixing concrete issues as they arise.
Note that some functions and filters must not be escaped in Blade templates to work correctly, e.g. HTML output functions like csrfInput(). Use {!! ... !!} instead of {{ ... }} for these.
Possible next steps:
- Testing...
- Drop functions that have equivalents in Laravel Blade (e.g. dump, dd).
- Drop functions that have equivalents in Laravel Helper classes? (e.g. Arr::xxx, Str::xxx).
- Drop functions that map directly to PHP native functions (e.g. array handling).
- Drop functions that map directly to Craft helper methods? (e.g. siteUrl() => UrlHelper::siteUrl()).
- Drop functions that map directly to Craft services? (e.g. entryType() ⇒ Craft::$app->getEntries()->getEntryTypeByHandle()).
- Implement as directives instead of functions in order to avoid escaping issues? (e.g.
@csrfInputinstead of{!! csrfInput() !!}). - Drop functions that will most likely never be used in a lifetime (e.g.
gql()). - Implement Laravel style helper functions for common services? (e.g.
request()vs.Craft::$app->getRequest()). - Check Craft's Twig tags and see if some can be implemented as Blade directives (e.g.
requireAdmin).
The following Blade directives are predefined:
@markdown($text, $flavor = 'original', $purifierConfig = null)- Render purified Markdown content to HTML@paginate($query, $resultsKey = 'elements', $pageInfoKey = 'pageInfo')- Handle pagination for an element query (experimental)@renderTwig($template, $data = [])- Render a Twig template from Blade@includeLocalized($template, $data = [])- Include a localized template (experimental)@requireAdmin- Require admin access for the current user (throws 403 otherwise)@requirePermission($permission)- Require a specific permission for the current user (throws 403 otherwise)@requireLogin- Require the user to be logged in (throws 403 otherwise)@requireGuest- Require the user to be logged out (throws 403 otherwise)@redirect($url, $statusCode=302)- Redirects to a given URL (throws a redirect response).@header($headerLine)- Sets an HTTP response header, matching Craft’s Twig{% header %}tag compiler behavior@cache($options = []) ... @endcache- Template fragment caching (Craft’s Twig{% cache %}equivalent)
Blade can be customized from your module or plugin by registering custom directives, components, stringables, shared data, view composers, etc.
Customizations can be defined
- in your controller (preferred for best granular control),
- in the
init()method of your module/plugin bootstrap class (place inCraft::$app->onInitcallback to ensure Craft is fully initialized), - or in the
config/_blade.phpconfig file (see Config-driven Customization below).
Define custom Blade directives in your plugin or module:
View::directive('datetime', function($expression) {
return "<?php echo ($expression)->format('Y-m-d H:i'); ?>";
})Usage in Blade templates:
<p>Published at: @datetime($entry->postDate)</p>Share global data across all Blade templates:
View::share('settings', Entry::find()->section('settings')->one());Then access it in any Blade template:
<footer class="mt-12 border-t border-b-gray-500 pt-4">
© {{ $settings->copyright }} {{ $now->format('Y') }}
</footer>This mimics Craft's preloadSingles feature for Twig templates. Kind of.
Define custom Blade If statements in your plugin or module:
View::if('dev', function (): bool {
return Craft::$app->getConfig()->getGeneral()->devMode;
});Usage in Blade templates:
@dev
<p>Running in dev mode</p>
@else
<p>Running in production mode</p>
@enddev
@unlessdev
<p>Running in production mode</p>
@enddevRegister custom stringable handlers to automatically format objects that don't implement a __toString method:
View::stringable(\DateTime::class, function($dateTime) {
return $dateTime->format('Y-m-d H:i');
});Usage in Blade templates:
<p>Posted: {{ $entry->postDate }}</p>
<!-- Outputs: Posted: 2025-12-28 14:30 -->Multiple stringables can be registered for different classes:
// Format DateTime objects
View::stringable(\DateTime::class, function($dateTime) {
return $dateTime->format('Y-m-d H:i');
});
// Format Money objects (example)
View::stringable(Money\Money::class, function($money) {
if ($money === null) {
return null;
}
return \craft\helpers\MoneyHelper::toString($money);
});Note that you can't pass additional parameters to the stringable handler. If you need more control, consider using a custom Blade directive or helper function instead.
Laravel-style Blade view composers are supported.
This lets you attach data to views globally or per-view, without having to pass everything from every controller.
Register composers from your module/plugin:
use wsydney76\blade\View;
View::composer('article.show', function ($view) {
$view->with('composerMessage', 'Injected by a view composer');
});
// Wildcards are supported by the underlying Illuminate view factory:
View::composer('*', function ($view) {
$view->with('composerMessage', 'Injected by a view composer');
});Then use the injected variables in your Blade template:
{{ $composerMessage }}Experimental, AI generated.
For convenience, you can also define customizations in the config/_blade.php config file.
return [
'bladeShared' => [
'copyright' => '© ' . date('Y'),
'settings' => Entry::find()->section('settings')->one(),
],
'bladeDirectives' => [
'relativeTime' => function($expression) {
return "<?php echo Craft::\$app->getFormatter()->asRelativeTime($expression); ?>";
},
],
'bladeStringables' => [
\DateTime::class => function($dateTime) {
return $dateTime->format('Y-m-d H:i');
},
],
'bladeIfs' => [
'itsFriday' => function (): bool {
return date('N') === '5';
},
],
'bladeComponents' => [
'alert' => Alert::class,
],
'bladeViewComposers' => [
'config' => function ($view) {
$view->with('entriesCount', Entry::find()->section('*')->count());
},
],
];Note: while this seems convenient, you lose control over when exactly customizations are registered. So this may have a negative impact on performance, e.g. when unnecessary queries are executed.
Helpers are regular PHP functions. Define them in regular PHP files that are required by your module/plugin. Note that loading via composer autoloading may not work because Craft is not initialized at that point.
If multiple controllers are used, extend from a base controller to set common Blade settings:
use wsydney76\blade\View;
...
public function beforeAction($action): bool
{
// Share global settings entry
View::share('settings', Entry::find()->section('settings')->one());
// Register stringable for DateTime objects
View::stringable(\DateTime::class, function($dateTime) {
return $dateTime->format('Y-m-d H:i');
});
// Datetime directive
View::directive('datetime', function($expression) {
return "<?php echo ($expression)->format('Y-m-d H:i'); ?>";
});
return parent::beforeAction($action);
}The plugin installs the Illuminate/Support package as a dependency, which provides Laravel's helper classes like Arr, Str, etc.
You can use these classes in your Blade templates and PHP code as needed, which is especially useful when porting existing Laravel applications.
Note that some functionality may not work as expected outside a full Laravel application context. This may especially apply to facades, because, well, there is nothing behind the facade.
@use('Illuminate\Support\Arr')
@use('Illuminate\Support\Str')
@use('Illuminate\Support\Number')
@use('Illuminate\Support\Pluralizer')
@dump(Arr::add(['name' => 'Desk'], 'price', 100))
@dump(Arr::crossJoin([1, 2], ['a', 'b']))
{{ Str::repeat('abc', 3) }}
{{ Str::replaceFirst('_', ':', 'abc_def_ghi') }}
{{-- Fluent strings (Str::of) allow chaining operations in a readable way --}}
{{ Str::of(' hello ? from ? ')->trim()->replaceArray('?', ['world', 'blade'])->headline()->append('!') }}
{{ Number::ordinal(21) }}
{{ Number::clamp(105, min: 10, max: 100) }}
{{ Pluralizer::plural('item') }} = items
{{ Pluralizer::plural('person', 3) }} = people
{{ Pluralizer::singular('geese') }} = goose
Remove cached Blade templates via console command:
php craft clear-caches/bladeOr via Control Panel: Utilities → Caches → Blade Template Cache.
The template cache has to be cleared when Blade custom directives are updated.
Livewire-like reactive components are not supported, as Livewire is deeply bound to core Laravel.
Consider porting existing components to Twig using the Sprig plugin (or similar).
Otherwise, you can integrate with Alpine.js (which is used by Livewire behind the scenes) to come somewhat close and keep most of your controller logic and templates.
See REACTIVECOMPONENTS.md for an example implementation of a reactive search component using Alpine.js.
Craft plugins that work with Twig should also work with Blade
- if they expose functionality via the Craft variable (e.g.
craft.thePlugin.doSomething) - if they expose functionality via plain PHP classes/services
Plugins that expose functionality via Twig extensions (functions, filters, tags) will not work out of the box.
{!! $craft->vite->script('/resources/js/app.js', false) !!}Example component usage:
<x-image class="my-8" :image="$image" :transform="['width' => 768, 'ratio' => 25/9]" />The component code (e.g. in resources/views/components/image.blade.php):
@props([
'image' => null,
'transform' => ['width' => 800, 'ratio' => 16 / 9],
])
@if ($image)
<img
{{ $attributes }}
src="{{ $craft->imagerx->transformImage($image, $transform) }}"
alt="{{ $image->alt ?? $image->title }}"
/>
@endif Or register the component globally in your module/plugin bootstrap:
View::share('imagerx', Craft::$app->plugins->getPlugin('imager-x')->imager);Then use it in Blade templates:
src="{{ $imagerx->transformImage($image, $transform) }}" Needs confirmation, but guessing that Blitz does not care about the template engine used.
@php($craft->blitz->options(['cachingEnabled' => false])) If a Laravel app is being ported to Craft CMS, you may want to keep using Eloquent ORM for accessing custom tables you don't want to migrate to Craft elements.
See ELOQUENT.md for a starting point on how to set up Eloquent in Craft CMS.
You will finally want to migrate to Craft's ActiveRecord models, but this may help to get started quickly.
Make sure plugins supporting Laravel/Blade are installed and enabled. PhpStorm >= 2025.3 has some Laravel support built-in.
- Install the Laravel Idea plugin
- PHP → Blade: Add custom directives for better code completion
- Editor → General → Appearance: Check "Always enable Blade template highlighting"
- Languages & Frameworks → Laravel Idea → Views: Check "Default Views Path"
- Languages & Frameworks → Laravel Idea → Languages → Blade: Check "Blade component views directory"
- Editor → Live Templates: You may want to add custom Blade snippets here for convenience.
Examples:
- Abbreviation:
bfor(or anything unique you like) - Applicable in: HTML (not PHP!)
- Edit Variables: ARRAY =
"$entries", VARIABLE ="$entry", STUFF =""
@foreach($ARRAY$ as $VARIABLE$)
$STUFF$$END$
@endforeach
- Abbreviation:
bnl2br(or anything unique you like) - Applicable in: HTML (not PHP!)
- Edit Variables: VARIABLE =
"$variable"
{!! nl2br(e($VARIABLE$)) !!}
No experience with other IDEs, but guessing that similar settings should be available.
This should pick up custom fields for Craft elements, make sure storage/runtime/compiled-classes is indexed by your IDE (mark directory as 'not excluded', if necessary).
Type hints can be added in Blade templates using @php blocks.
@php
/** @var \craft\elements\Entry $entry */
@endphp
<h1>{{ $entry->title }}</h1>
<p> {{ $entry->myCustomField }}</p>If you want to define type hints globally for common variables like $entry, place a PHP file anywhere where the IDE indexes it:
<?php
/** @var \craft\web\twig\variables\CraftVariable $craft */
global $craft;
/** @var \craft\elements\User $currentUser */
global $currentUser;
/** @var \craft\elements\Entry $entry */
global $entry;
/** @var \craft\elements\Asset $asset */
global $asset;
/** @var \craft\elements\Asset $image */
global $image;Example setup, adjust to your needs.
Update your package.json to include prettier-plugin-blade and prettier-plugin-tailwindcss:
{
"scripts": {
"prettier-views": "npx prettier --write \"resources/views\" --parser blade"
},
"devDependencies": {
"@prettier/plugin-php": "^0.24.0",
"prettier": "^3.6.2",
"prettier-plugin-blade": "^2.1.21",
"prettier-plugin-tailwindcss": "^0.6.14"
}
}Then run npm install to install the packages.
Create a .prettierrc config file in your project root:
{
"plugins": [
"@prettier/plugin-php",
"prettier-plugin-tailwindcss",
"prettier-plugin-blade"
],
"singleQuote": true,
"tabWidth": 4,
"printWidth": 100,
"semi": true,
"trailingComma": "es5",
"tailwindStylesheet": "./resources/css/app.css"
}Run npm run prettier-views to format all Blade templates in the resources/view directory.
PHPStorm settings (may differ for different versions):
- Languages & Frameworks → JavaScript → Prettier:
- Include blade suffix in
Run for files: `**/*.{js,ts,jsx,tsx,cjs,cts,mjs,mts,json,vue,astro,blade.php,php} - Check
Automatic prettier configuration,Run on save,Run on paste,Prefer prettier configuration to IDE code style.
- Include blade suffix in
- Tools → Actions on Save: Check
Run prettier, disableReformat code. - Languages & Frameworks → JavaScript → Runtime: Check that Node runtime is set correctly.