grafite/html
Grafite HTML is a Laravel package that provides a fluent HTML builder and helper utilities for generating markup directly in PHP. It streamlines forms, links, and UI components with a clean, chainable API for Blade views and controllers.
Install and confirm it actually loads
composer require grafite/html
Before writing a single line against it, open composer.json in the package and check two things: the illuminate/* version constraint (this package predates current Laravel releases, so it may need --with-all-dependencies or it may simply refuse to install), and whether a HtmlServiceProvider is listed under extra.laravel.providers. If auto-discovery is absent, register manually in config/app.php:
'providers' => [Grafite\Html\HtmlServiceProvider::class],
'aliases' => ['Html' => Grafite\Html\Facades\Html::class],
Where to look first
src/ — the main builder class and any facade. The public methods are the documentation for a package this small.tests/ — the fastest spec you will get. Assertions like $this->assertEquals('<a href="...">', ...) tell you exact output, whitespace, attribute ordering, and self-closing behaviour, which is where these helpers usually surprise you.README.md — often the only usage example that exists.First use case
The realistic win is data-driven markup: a nav, a breadcrumb, a badge/button, a table row — anything you would otherwise build with string concatenation or a tangle of @if in Blade. Start with one partial and render a list of links from a config array, then compare the diff against the Blade you replaced. If the diff isn't clearly shorter, stop there.
Keep generation at the view boundary. Resolve the builder in a controller or view composer and pass the resulting string/object to the view; don't let HTML strings leak into models or services. In Blade:
{!! $breadcrumbs !!} {{-- only if the object isn't Htmlable --}}
Test whether the return value implements Illuminate\Contracts\Support\Htmlable. If it does, {{ $breadcrumbs }} renders unescaped automatically and you should prefer that — it signals intent better than {!! !!}.
Wrap it, don't sprinkle it. Put every call behind your own thin class or Blade component:
class Ui
{
public function button(string $label, array $attrs = []): string
{
return (string) Html::tag('button', $label, $attrs); // verify method name in src/
}
}
This costs ten minutes and buys you a one-file migration path when the package goes unmaintained (which, at 2 stars and 0 dependents, is the base case). Define a small HtmlBuilder interface in your app if more than one or two developers will touch it.
Attribute arrays are the main ergonomic win. Pass attributes as associative arrays and merge conditionally:
$attrs = array_merge(['class' => 'btn'], $isPrimary ? ['class' => 'btn-primary'] : []);
Check the package's class-merging behaviour first — naive array_merge will clobber the earlier class key, so most helpers either accept arrays of classes or expect you to join them yourself.
Compose with Blade, don't replace it. Use the package for the repetitive leaf nodes (buttons, links, icons) and keep @foreach, @csrf, @method, and component slots in Blade. Do not attempt to generate forms with it — CSRF token injection and method spoofing belong to Laravel's directives.
Test the output, not the call. Assert on strings:
$this->assertSame('<a class="nav-link" href="/about">About</a>', (string) $html);
Snapshot two or three representative elements into a test so an upgrade (or your own wrapper refactor) fails loudly on whitespace and attribute-order changes.
Escaping is the whole risk surface. An HTML generator that returns raw markup is an XSS vector the moment user data reaches it. Check src/ for htmlspecialchars/e() on both text content and attribute values — many small helpers escape one and forget the other. Escape at the call site:
Html::link(e($user->name), route('users.show', $user));
Never pass {!! !!}-worthy output through the builder and assume it's safe because "the package handles it."
Empty and boolean attributes. Verify how null, false, and true are treated — you want null to omit the attribute and true to render disabled="disabled" (or bare disabled). If the helper stringifies blindly you'll ship class="" and disabled="", which is invalid-ish HTML and will confuse your CSS selectors.
Auto-discovery and alias collisions. The Html alias is a classic conflict (collective/html used the same name historically). If another package claims it, rename your alias to HtmlBuilder in config/app.php and update call sites. A "Class 'Html' not found" or "target class does not exist" error on a fresh install is almost always missing discovery or a stale composer dump-autoload.
No licence file. NOASSERTION means GitHub could not detect a licence — that is a legal unknown, not a permissive licence. For client or commercial work, get sign-off or fork the code into your own namespace with an explicit licence before it ships. Combined with 0 dependents and no release cadence, assume abandonment: pin an exact version, commit the lockfile, and vendor the source if the feature is load-bearing.
Config publishing. If there is a config/html.php, check whether a publish tag exists. Without one you can't override defaults without editing vendor, which will be wiped on the next composer update. Copy the defaults into your own service provider's mergeConfigFrom instead.
Extension points. If the class uses Macroable, you can add elements without forking:
Html::macro('pill', fn ($text, $tone = 'info') => Html::tag('span', $text, ['class' => "pill pill-{$tone}"]));
If it doesn't, extend the class, bind your subclass to the container interface in a service provider, and keep the facade pointing at your implementation.
Debugging. dd((string) $html) and dd(get_class($html)) answer most questions about why output looks wrong — you are usually looking at a magic __toString, and a null there silently yields "". Also check for trailing whitespace/newlines in tests/ expectations; those strings will show up in assertSee failures.
Consider the native alternative before committing. Laravel now ships Illuminate\Support\HtmlString, Arr::toCssClasses(), and Blade components/@props, which cover most of what this package does with first-party support. Benchmark the package against a 15-line Blade component on your actual first use case — if the component is comparable, take the component and skip the dependency.
How can I help you explore Laravel packages today?