clean/view
Clean View provides a lightweight, expressive view layer for Laravel and PHP applications. Render templates with layouts, partials, sections, shared data, custom directives, and automatic escaping through a clean API that keeps presentation logic organized, reusable, and easy to maintain.
clean/view is a lightweight view-rendering component from the clean PHP organisation. It is a standalone template layer — do not assume Laravel's view() helper, Blade directives, or the View facade exist after installing it. Treat it as a small renderer you wire into your own code.
Minimal steps
composer require clean/view
ls -R vendor/clean/view
cat vendor/clean/view/README.md
cat vendor/clean/view/composer.json
composer.json first. Check the require block (PHP version, PSR interfaces), the autoload PSR-4 namespace, and whether it declares a Laravel extra.laravel.providers entry. If there's no provider, there is no auto-discovery and no Laravel integration — you construct the renderer yourself.grep -rn "public function" vendor/clean/view/src | head -50
Look for a renderer/factory class and a render(string $template, array $data) style entry point, plus whatever interface represents the template path configuration.$view = new \Clean\View\View(__DIR__ . '/templates');
echo $view->render('welcome', ['name' => 'Ada']);
(Adjust the class/path-argument names to whatever the source actually exposes — verify before writing.)Do not start by trying to register it as a replacement for Blade in an existing Laravel app. Start by rendering one template in isolation, then decide whether the ergonomics justify the integration cost.
Standalone renderer in a non-Laravel context. The cleanest fit. Wire the renderer once in your bootstrap and pass it through your container:
// bootstrap.php
$container->set(ViewInterface::class, fn () => new View(
templatePath: __DIR__ . '/../resources/views',
cachePath: __DIR__ . '/../storage/views', // if supported
));
In a Laravel app, bind it in a service provider with an explicit singleton and a config key, rather than relying on auto-discovery:
// App\Providers\CleanViewServiceProvider
public function register(): void
{
$this->app->singleton(ViewInterface::class, fn ($app) => new View(
$app['config']->get('view.clean.path', resource_path('clean-views')),
));
}
Keep it out of the HTTP view layer. Laravel's view() / View::make() / @extends machinery is deeply coupled to Illuminate\View\*. If you want clean/view in a Laravel app, use it for a narrow, non-HTTP surface — rendered email bodies, PDFs, CLI output, generated Markdown — and leave web responses to Blade. Mixing two template engines in the same response pipeline is where bugs breed.
Explicit data contracts. With no @props/@aware equivalents, every template's input is a plain array. Collapse that into small value objects or DTOs and pass arrays at the boundary; that's how you keep templates from drifting into "whatever keys the controller happened to set."
Layouts via composition, not inheritance. If the package offers partials/includes, build a layout template that renders a content variable pre-rendered by the caller. If it only supports flat rendering, do the nesting yourself:
$body = $view->render('emails/receipt-body', $data);
$html = $view->render('emails/layout', ['content' => $body] + $data);
Escaping you control. Assume the renderer escapes nothing unless the source proves otherwise. Wrap every interpolation originating from user input in htmlspecialchars($v, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') before handing it to the renderer, or pass pre-escaped values and keep raw ones under an obvious _raw suffix.
Testing. Snapshot the rendered output rather than asserting on template internals; that keeps your tests valid if you later swap the renderer out:
$this->assertSame(file_get_contents(__DIR__.'/fixtures/receipt.html'), $view->render('receipt', $data));
src/ before adding it to a dependency graph that matters; treat it as vendored third-party code you are now responsible for. If the surface area is a few hundred lines, consider forking it into app/Support and dropping the Composer dependency entirely.extra.laravel in composer.json before assuming php artisan vendor:publish works. If absent, register everything by hand.require block against your runtime. A low-activity package often pins stale PSR versions (psr/container ^1.0 alongside a Laravel version expecting ^2.0), producing Composer conflicts that look like your app's fault. Resolve with an alias or --with-all-dependencies only after confirming the code is actually compatible.null returns. Small renderers frequently return an empty string or null when a template file is missing rather than throwing. Add your own existence check before rendering, or you will ship blank emails. Wrap the render call in a guard that asserts non-empty output in non-production.$templatePath . '/' . $name . '.php' will happily load ../../etc/passwd if $name is ever user-influenced. Never pass request input as the template name; map it through an allow-list of known views.grep -o '\$[a-z_]*' template.php | sort -u). Missing-key warnings buried in output buffering are the usual culprit.ViewRenderer interface on day one so the rest of your app never imports Clean\View\* directly.Illuminate\View pulled into a non-Laravel app via illuminate/view), Twig, or Latte — all maintained, all documented, all with caching. Adopt clean/view only for its size or a specific constraint; otherwise the maintenance cost outweighs the footprint saving.How can I help you explore Laravel packages today?