Exception while rendering Blade in a component results in death of laravel
Reference: https://github.com/LvckyAPI/filament-debug
I have a form in which I can enter code (HTML with Blade).
Next to it is a field that is supposed to render the whole thing live.
If I enter something like {{fake()}}, the rendering dies, of course, and everything else crashes with a big error screen.
The exception cannot be caught.
-> My initial solution was to use concurrency.
Unfortunately, this resulted in even more problems. Serialization fails, languages are no longer passed.
My goal is to render three fields. If an error occurs in one rendering process, it should be caught and displayed in the field without affecting the rendering of the others.
Here is my code for LivePreview currently using concurrency:
Translated with DeepL.com (free version)
<?php
namespace App\View\Components;
use App\Enums\MessageTypeEnum;
use App\Http\Controllers\MessageController;
use App\Models\Message;
use Closure;
use Illuminate\Contracts\View\View;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Concurrency;
use Illuminate\View\Component;
use Throwable;
class MessageLivePreview extends Component
{
public function __construct(
public Message $message,
public string|Closure|null $content = null,
public string|Closure|null $subject = null,
public string|Closure|null $plainText = null,
public bool $debug = false,
)
{
}
public function render(): View|Closure|string
{
if ($this->subject) $this->message->subject = $this->subject;
if ($this->content) $this->message->html = $this->content;
if ($this->plainText) $this->message->plain_text = $this->plainText;
$message = $this->message->toArray();
// --- Prepare previews using a Collection ---
$previews = collect([
'subject' => Arr::get($message, 'subject'),
'html' => match (Arr::get($message, 'type')) {
MessageTypeEnum::EMAIL->value => Arr::get($message, 'html'),
MessageTypeEnum::CHAT->value => Arr::get($message, 'markdown'),
MessageTypeEnum::SMS->value => Arr::get($message, 'plain_text'),
},
'plain' => Arr::get($message, 'type') === MessageTypeEnum::EMAIL->value
? Arr::get($message, 'plain_text')
: '',
]);
// --- Render each preview independently with cache ---
$results = $previews->mapWithKeys(function ($content, $key) {
// Initialisierung der arguments innerhalb der Closure
$arguments = MessageController::arguments($this->message);
$locale = $arguments['utils']->locale ?? 'default';
$cacheKey = 'messages:preview:' . md5($content . '|' . $locale . '|' . $key);
if ($this->debug) {
try {
return [$key => Blade::render($content, $arguments)];
} catch (Throwable $e) {
return [$key => match ($key) {
'subject' => Blade::render(
'<x-error-display :throwable="$throwable" :headerTitle="$headerTitle" />',
['throwable' => $e, 'headerTitle' => __('message.error.error_rendering_subject_preview')]
),
'html' => Blade::render(
'<x-error-display :throwable="$throwable" :headerTitle="$headerTitle" />',
['throwable' => $e, 'headerTitle' => __('message.error.error_rendering_html_preview')]
),
'plain' => "Error rendering Plain Text: " . $e->getMessage(),
}];
}
} else {
return [$key => Cache::remember($cacheKey, 3600, function () use ($arguments, $content, $key) {
try {
return Concurrency::run(fn() => Blade::render($content, $arguments))[0];
} catch (Throwable $e) {
return match ($key) {
'subject' => Blade::render(
'<x-error-display :throwable="$throwable" :headerTitle="$headerTitle" />',
['throwable' => $e, 'headerTitle' => __('message.error.error_rendering_subject_preview')]
),
'html' => Blade::render(
'<x-error-display :throwable="$throwable" :headerTitle="$headerTitle" />',
['throwable' => $e, 'headerTitle' => __('message.error.error_rendering_html_preview')]
),
'plain' => "Error rendering Plain Text: " . $e->getMessage(),
};
}
})];
}
});
// --- Return final view ---
return view('messages.preview')
->with('renderedSubject', !empty($previews['subject']) ? $results['subject'] : 'NO SUBJECT')
->with('preview', $results['html'])
->with('plain', $results['plain']);
}
}
// preview.blade.php
@php
$previewEncoded = base64_encode($preview);
$plainEncoded = base64_encode($plain ?? '');
@endphp
<div style="height: 100%; display: flex; flex-direction: column; gap: 2px">
<h1 style="flex: none">{!! $renderedSubject !!}</h1>
<hr style="flex: none"/>
<iframe style="flex: 2; min-height: 100vh; width: 572px"
src="data:text/html;charset=utf-8;base64,{{ $previewEncoded }}"></iframe>
@if(!empty($plain))
<hr style="flex: none"/>
<iframe style="flex: 1; min-height: 100vh; width: 572px"
src="data:text/plain;charset=utf-8;base64,{{ $plainEncoded }}"></iframe>
@endif
</div>
// filament form
CodeEditor::make('subject')
->language(CodeEditor\Enums\Language::Php)
->label(__('message.field.subject'))
->hintIcon(Heroicon::OutlinedQuestionMarkCircle, __('message.field.subject_hint'))
->default(null)
->live(debounce: 1000)
->columnSpanFull(),
CodeEditor::make('html')
->label(__('message.field.html'))
->default(null)
->language(CodeEditor\Enums\Language::Php)
->live(debounce: 1000)
->visible(fn(Get $get) => $get('type')->allowedTextTypes()->contains(MessageTextTypeEnum::HTML)),
MessagePreview::make('__preview')
->label(__('message.field.preview'))
->visibleOn(['edit', 'view']),
//message preview compoennt
<?php
namespace App\Filament\Forms\Components;
use Filament\Forms\Components\ViewField;
class MessagePreview extends ViewField
{
protected string $view = 'filament.forms.components.message-preview';
}
<x-dynamic-component
:component="$getFieldWrapperView()"
:field="$field"
>
<div
x-data="{ state: $wire.$entangle(@js($getStatePath())).defer }"
{{ $getExtraAttributeBag() }}>
<x-message-live-preview
@viteReactRefresh
:message="$record"
:content="$get('html')"
:subject="$get('subject')"
:plainText="$get('plain_text')"
/>
</div>
</x-dynamic-component>
Can anyone provide me better solution without this hack fuck and problems
Please or to participate in this conversation.