---
title: "Forte: парсинг та редагування Laravel Blade-шаблонів"
url: https://laravelukraine.com/blog/forte-parsing-ta-redaguvannia-laravel-blade-sabloniv
date: 2026-09-03
source: https://laravel-news.com/forte-blade-parser?utm_medium=feed&utm_source=feedpress.me&utm_campaign=Feed%3A+laravelnews
---

# Forte: парсинг та редагування Laravel Blade-шаблонів

Laravel-застосунки часто потребують однакових змін у кожному Blade-файлі - наприклад, перейменування компонента або нормалізації директиви. Однорядковий `sed` замінює кожен збіг, включно з тими, що всередині рядків, коментарів та атрибутів, які ви не хотіли міняти. Forte, написаний [John Koster](https://github.com/JohnathonKoster), парсить `.blade.php` у типізоване синтаксичне дерево, дає змогу запитувати дерево та рендерить переписаний шаблон назад.

Koster також є автором `prettier-plugin-blade`. Версія 3 цього форматера працює на Forte.

Використовуйте Forte, коли потрібно з'ясувати, що містять ваші в'юшки, застосувати одну зміну до сотень файлів або зламати білд, коли шаблон порушує конвенції вашої команди.

## Встановлення

Forte потребує PHP 8.2, розширення `dom` та Laravel 10-13:

```
composer require fortephp/forte
```

Сервіс-провайдер реєструється автоматично, тому фасад `Forte` доступний одразу.

## Парсинг

`Forte::parse()` приймає рядок, `Forte::parseFile()` приймає шлях:

```
use Forte\Facades\Forte;
$doc = Forte::parse('<div class="mt-4">Hello, {{ $name }}!</div>');
$doc = Forte::parseFile('resources/views/welcome.blade.php');
```

Обидва методи проходять через лексер, який перетворює вихідний код у токени, та tree builder, який збирає вузли. `Document` зберігає результат.

Якщо тег не закритий або `@if` без `@endif`, парсер записує діагностику до документа і повертає часткове дерево. Ви можете запитувати та переписувати це дерево, як і будь-яке інше. Два зламані в'юшки у застосунку з чотирма сотнями в'юшок створюють дві діагностики, а решта 398 парсяться нормально.

Якщо розпарсити файл і відрендерити його назад без змін, ви отримаєте ті самі байти, включно з пробілами.

## Запити до дерева

Методи запитів повертають ліниві колекції, тому ви можете фільтрувати та трансформувати їх так само, як будь-яку іншу Laravel-колекцію:

```
$forms = $doc->queryElements('form');
$conditionals = $doc->queryBlockDirectives(['if', 'unless']);
$components = $doc->queryComponents(['x-alert', 'livewire:*']);
```

Для одного відомого вузла є прямі пошуки:

```
$navigation = $doc->elementById('primary-navigation');
$hasHead = $doc->hasElement('head');
```

`isDynamic()` повідомляє, чи значення прийшло з прив'язаного атрибута, а `attributeTokens('class')` розбиває список класів на масив:

```
$form = $doc->firstElement('form');
$method = $form?->attribute('method');
$isDynamic = $method?->isDynamic() ?? false;
$classes = $form?->attributeTokens('class') ?? [];
```

Forte будує `DOMDocument` з дерева і запускає вираз через PHP `DOMXPath`, що і покриває вимогу `ext-dom`. Blade-конструкції стають елементами у просторі імен `forte`, тому блоки `@if` - це `forte:if`, а ехо - `forte:echo`:

```
$divs = $doc->xpath('//div[@class]')->get();
$conditionals = $doc->xpath('//forte:if')->get();
```

Збіги повертаються як вузли Forte, а не об'єкти `DOMElement`, тому ви можете передати результат запиту до перезапису. Пошук кожного `<a>` всередині `<nav>`, що не має `href`, - це один вираз замість рекурсивного проходу.

## Переписування

`apply()`, `rewrite()` та `rewriteWith()` кожен повертають новий `Document` і залишають оригінал без змін, тому ви можете зберегти обидва і порівняти їх.

`rewriteWith()` обробляє разові зміни через замикання. Колбек отримує `NodePath`, а не сам вузол:

```
use Forte\Rewriting\NodePath;
$newDoc = $doc->rewriteWith(function (NodePath $path) {
 if ($path->isTag('a') && str_starts_with($path->getAttribute('href') ?? '', 'http')) {
 $path->setAttribute('target', '_blank');
 $path->setAttribute('rel', 'noopener noreferrer');
 }
});
echo $newDoc->render();
```

`NodePath` надає доступ до батьківського елемента, сусідів, предків та глибини вузла, на який він вказує, а також до методів `getAttribute()`, `setAttribute()`, `removeAttribute()`, `addClass()`, `renameTag()`, `replaceWith()`, `remove()`, `insertBefore()` та `insertAfter()`. `skipChildren()` і `stopTraversal()` завершують обхід достроково, коли ви знайшли потрібний вузол.

Forte ставить зміни у чергу, а не застосовує їх одну за одною, тому прохід по великому шаблону створює один новий документ замість одного на кожну зміну.

Для чогось довшого за замикання, напишіть візитор:

```
use Forte\Rewriting\Visitor;
use Forte\Rewriting\NodePath;
class NormalizeAlerts extends Visitor
{
 public function enter(NodePath $path): void
 {
 if (! $path->isTag('div') || ! $path->hasAttribute('data-alert')) {
 return;
 }
 $level = $path->getAttribute('data-alert') ?? 'info';
 $path->renameTag('x-alert');
 $path->setAttribute('type', $level);
 $path->removeAttribute('data-alert');
 }
}
```

`enter()` запускається до того, як відвідуються діти вузла, а `leave()` - після. Більшість проходів потребують лише `enter()`. Використовуйте `leave()`, коли зміна залежить від того, що сталося з дітьми, наприклад, розгортання елемента після того, як його вміст було переписано.

Є також `RewriteBuilder` для декларативної версії: виберіть вузли через XPath, потім поставте у чергу мутації для застосування до збігів.

## Створення нових вузлів

Переписування часто потребують нового вузла для заміни. `Builder` створює його:

```
use Forte\Rewriting\Builders\Builder;
Builder::element('div')->class('wrapper')->text('Hello');
Builder::directive('if', '($show)');
Builder::echo('$name');
```

Передайте результат до `replaceWith()`, `insertBefore()` або `insertAfter()`.

## Аудит, кодмоди та CI-перевірки

### З'ясування вмісту в'юшок

Перед видаленням компонента знайдіть кожну в'юшку, що все ще його рендерить:

```
use Forte\Facades\Forte;
use Illuminate\Support\Facades\File;
foreach (File::allFiles(resource_path('views')) as $file) {
 if (! str_ends_with($file->getFilename(), '.blade.php')) {
 continue;
 }
 $uses = Forte::parseFile($file->getPathname())
 ->queryComponents(['x-alert'])
 ->count();
 if ($uses > 0) {
 echo "{$file->getRelativePathname()}: {$uses}\n";
 }
}
```

`grep -rc 'x-alert' resources/views` відповідає на схоже питання в один рядок, але рахує згадки в коментарях, у рядках `@php` та в атрибуті `class="x-alert-icon"` поряд із справжніми. Підрахунок вище - це кількість разів, коли компонент рендериться.

### Виконання однієї зміни в сотнях в'юшок

Додавання `loading="lazy"` до кожного `<img>`, що не має атрибута `loading`, - це прохід, який можна запустити, переглянути як diff і перезапустити після коригування:

```
use Forte\Facades\Forte;
use Forte\Rewriting\NodePath;
use Illuminate\Support\Facades\File;
foreach (File::allFiles(resource_path('views')) as $file) {
 if (! str_ends_with($file->getFilename(), '.blade.php')) {
 continue;
 }
 $doc = Forte::parseFile($file->getPathname());
 $updated = $doc->rewriteWith(function (NodePath $path) {
 if ($path->isTag('img') && ! $path->hasAttribute('loading')) {
 $path->setAttribute('loading', 'lazy');
 }
 });
 file_put_contents($file->getPathname(), $updated->render());
}
```

`<img>`, написаний всередині коментаря, рядка або блоку `@php`, парситься як інший тип вузла, тому `isTag('img')` для нього повертає false. Зробити це правильно з регулярним виразом вимагає більше уваги, ніж решта роботи.

Якщо прохід був неправильним, виправте скрипт, запустіть `git restore resources/views` і спробуйте знову.

### Провал білда при порушенні конвенції

`POST`-форма без `@csrf` - це один вираз, тому перевірка вміщається в тест:

```
use Forte\Facades\Forte;
use Illuminate\Support\Facades\File;
test('every POST form has a CSRF token', function () {
 $offenders = [];
 foreach (File::allFiles(resource_path('views')) as $file) {
 if (! str_ends_with($file->getFilename(), '.blade.php')) {
 continue;
 }
 $missing = Forte::parseFile($file->getPathname())
 ->xpath('//form[@method="POST"][not(.//forte:csrf)]')
 ->count();
 if ($missing > 0) {
 $offenders[] = $file->getRelativePathname();
 }
 }
 expect($offenders)->toBeEmpty();
});
```

Цей вираз читається зліва направо: кожна `<form>` з `method="POST"`, зберігаючи ті, що не мають `@csrf` десь всередині. `.//` - це частина "десь всередині", тому форма, чий `@csrf` знаходиться в сусідній формі, все ще рахується як порушник.

`@foreach`, чия перша дитина не має `wire:key`, також є одним виразом:

```
$missingKeys = $doc->xpath('//forte:foreach[*[1][not(@*[name()="wire:key"])]]')->count();
```

`//forte:foreach` знаходить кожен блок `@foreach`. `*[1]` - це перший дочірній елемент цього блоку. `not(@*[name()="wire:key"])` зберігає блоки, чия перша дитина не має атрибута `wire:key`. Форма `@*[name()="..."]` потрібна, оскільки XPath читає двокрапку в `wire:key` як роздільник простору імен.

Обидва вирази є тестами у власному тестовому наборі Forte. Другий залежить від вкладеності та від того, яка дитина йде першою.

## Коли регулярного виразу достатньо

Для одного перейменування в тридцяти в'юшках `sed` або структурний пошук вашої IDE плюс ретельне читання diff - це менше роботи, ніж написання візитора. Те саме стосується зміни, яку ви робите один раз і більше ніколи не перевіряєте.

Forte варто налаштовувати у цих випадках:

- Правило залежить від структури: "всередині форми", "перша дитина", "вкладено в `@foreach`". Grep знаходить рядки, тому не може виразити це.
- Зміна зачіпає стільки файлів, що читання всього diff вручну займає більше часу, ніж написання проходу.
- Перевірка запускається на кожному коміті, де збіг всередині коментаря або рядка ламає білд без причини.

## Chisel та Reload

Два інші пакети побудовані на Forte, і ви можете використовувати обидва без написання візитора. Chisel - це `prettier-plugin-blade` v3, переписаний на новому парсері; проєкт повідомляє, що він форматує складні реальні шаблони в 140 разів швидше за попередню версію. Потребує Node 18 або новіше:

```
npm i -D prettier prettier-plugin-blade@^3
```

Форматування Blade тепер має більше опцій, включно з [власною підтримкою Blade у Laravel Pint](https://laravel-news.com/blade-formatting-in-laravel-pint) та [format-on-save у PhpStorm](https://laravel-news.com/automatic-blade-formatting-on-save-in-phpstorm).

Reload - це Vite-плагін, який патчить зміни Blade на сторінку без повного оновлення. Його документація називає його експериментальним, і він повертається до повного оновлення після `max_patches_before_reload` інкрементальних патчів:

```
composer require fortephp/reload --dev
```

Він стежить за `resources/views/**/*.blade.php` і інструментує елементи, компоненти, директиви та включення. Опція `refresh` у `laravel-vite-plugin` вже [перезавантажує сторінку при зміні Blade-файлу](https://laravel-news.com/laravel-blade-hot-refresh-with-vite). Reload патчить DOM замість цього.

Forte має ліцензію MIT і наразі у версії v1.1.0. Вихідний код на [GitHub](https://github.com/fortephp/forte), документація та інтерактивний playground на [fortephp.com](https://fortephp.com).
