webman/blade
Blade template engine integration for Webman. Render Laravel-style Blade views in Webman apps with layouts, sections, components, and view caching, making it easy to reuse familiar templating patterns without adopting the full Laravel framework.
Install and enable
composer require webman/blade
The plugin ships a config file at config/plugin/webman/blade/app.php (created on first install). The things you actually care about there:
enable — must be true, otherwise view() falls back to whatever other view driver is installed (or throws).app/view) and the compiled-template cache directory (typically runtime/views)..html by default, not .blade.php).Make sure the cache directory is writable by the user running the Workerman process — a permission error there produces a confusing "failed to open stream" from deep inside the compiler rather than a clear message.
Restart, don't just refresh
webman runs resident PHP processes. After installing the plugin or editing its config you must reload/restart:
php start.php restart -d
Config changes are not picked up by a browser refresh. Template file changes are (the compiler compares mtimes), which is the source of much "why did the config change do nothing" confusion.
First use case
namespace app\controller;
use support\Request;
class UserController
{
public function show(Request $request, $id)
{
$user = \app\model\User::find($id);
return view('user/show', ['user' => $user]);
}
}
view() returns a support\Response (the rendered HTML plus headers), so returning it from a controller is the whole contract. If you need the string instead — emails, PDF generation, JSON-wrapped fragments, caching — use raw_view('user/show', [...]).
Where to look first
config/plugin/webman/blade/app.php — paths and suffix.app/view/ — create layout/main.html and a couple of pages before writing anything clever.runtime/views/ — the compiled PHP. This is your debugger; every stack trace from a template points here.Layout + partials, the standard shape
<!-- app/view/layout/main.html -->
<!doctype html>
<html>
<head><title>@yield('title', 'App')</title></head>
<body>
@include('partials/nav')
@yield('content')
</body>
</html>
<!-- app/view/user/show.html -->
@extends('layout/main')
@section('title', $user->name)
@section('content')
<h1>{{ $user->name }}</h1>
@foreach ($orders as $order)
<p>{{ $loop->iteration }}. {!! $order->label() !!}</p>
@endforeach
@endsection
Shared data via middleware, not globals
Because workers are long-lived, the clean pattern is a middleware that builds a per-request data array and each controller merges it:
// app/middleware/ViewData.php
public function process(Request $request, callable $handler)
{
$request->viewData = ['user' => $request->session->get('user')];
return $handler($request);
}
// controller
return view('dashboard', $request->viewData + ['stats' => $stats]);
Avoid stashing anything request-scoped in static properties or compiler-shared state; it survives into the next request handled by the same worker.
Components for reusable UI
Anonymous components (no PHP class needed) work well for a design system:
app/view/components/alert.html
app/view/components/button.html
<x-alert type="warning">Disk almost full</x-alert>
<x-button href="/logout">Sign out</x-button>
Inside the component file, $type, $href, and $slot are available. This is the cheapest way to stop duplicating markup across dozens of templates.
Strings instead of responses
raw_view() is the pattern for anything non-HTTP-shaped: rendering a notification email body, generating a PDF, producing an HTML fragment for an AJAX endpoint, or building a cached HTML block:
$html = raw_view('mail/welcome', ['user' => $user]);
It also works from CLI scripts and cron tasks in webman, which is genuinely useful — one template language for web and background work.
Custom directives for domain markup
Rather than repeating formatting logic in templates, register directives once at bootstrap and use them everywhere:
$compiler->directive('money', fn ($expr) => "<?php echo number_format($expr, 2); ?>");
// template: @money($invoice->total)
Register these in a bootstrap file so every worker has them; a directive registered at runtime inside one request won't exist in the others.
Plugin views for modular apps
webman plugins each carry their own view directory, and the view helper can be pointed at a plugin's app so layout/inheritance resolves within that plugin. Use this to keep a plugin's templates self-contained — but keep the shared layout in the host app, otherwise every plugin needs a copy.
Laravel directives and helpers do not exist here. This is the number one trap. @csrf, @auth, @can, @error, @env, @vite, route(), asset(), old(), csrf_field(), and config() are Laravel-framework glue, not Blade-the-compiler. Templates containing them compile fine and then blow up at render time with "Call to undefined function". Write your own helpers or directives for each one you actually need.
Stale compiled templates. Normal edits are picked up via mtime, but deploys that copy files and keep old mtimes (rsync without --checksum, some container builds) will silently serve old output. Clear runtime/views as part of your deploy, and note that opcache.validate_timestamps=0 in production makes the problem worse — rewritten compiled files aren't re-read until the process recycles.
Every template error points at runtime/views/xxxx.php. Learn to read that file. The line number in the compiled output rarely matches your template, but the surrounding PHP is usually enough to identify the block. A missing @endsection, an unclosed @if, or a stray @ in the middle of text all produce errors that read like PHP syntax errors.
Undefined variables are fatal, not empty. There's no Laravel-style "silently null" behaviour. Use {{ $title ?? 'Default' }} and @isset($x) / @empty($x) liberally, especially in partials that get included from multiple pages.
Rendering runs on the event-loop worker. A template that lazy-loads a relation inside @foreach will do N blocking queries while holding a worker. Eager-load before the view() call. The same applies to {{ $user->expensiveAccessor() }} — it's an N+1 in disguise.
Never let user input pick the template name. view($request->input('theme') . '/index') is a path-traversal and template-injection vector; compiled templates are PHP executed on the server. Whitelist theme names against an array.
Escaping discipline. {{ }} runs htmlspecialchars with ENT_QUOTES — good. {!! !!} is raw; audit every occurrence. Note that {{ }} is not safe inside <script> blocks or HTML attribute contexts that need JS/URL encoding; use json_encode() or urlencode() explicitly there.
Don't echo in templates. Debugging with var_dump() writes to the output buffer after headers may already be sent, and in a streamed response it corrupts the body. Log instead, or dump into a <pre> in a dedicated debug view.
Form/CSRF handling is on you. Since there's no @csrf, either mint your own token helper and directive, or rely on an auth/session plugin that provides one. Verify it exists before you build twenty forms.
Check the release cadence before betting heavily on it. The last release is recent (2025-08-05) but the package has very little external usage, so weird interactions with newer Blade versions (new component syntax, <x-slot>, fragment support) may be untested. Pin the version in composer.json and keep a smoke test that renders one page of each layout so an upgrade failure is caught immediately rather than in production.
How can I help you explore Laravel packages today?