beyondcode/laravel-tag-helper
Laravel Tag Helper by Beyondcode makes it easy to generate HTML tags programmatically in Laravel. Build elements and attributes with a simple API, avoid messy string concatenation, and keep Blade templates cleaner. Ideal for reusable components and dynamic markup, it is lightweight and framework-fri...
Install with Composer: composer require beyondcode/laravel-tag-helper. The package targets the Laravel 5.5 auto-discovery era, so it should register BeyondCode\TagHelper\TagHelperServiceProvider automatically. If you are on anything older, add the provider to config/app.php manually. If Composer refuses the install on a current Laravel/PHP constraint set, stop — do not paper over it with --ignore-platform-reqs in a production lockfile.
Read the source before the README. The repo is archived (last release 2018-10-08), the documentation contradicts the changelog, and there are zero dependents to sanity-check against. The only trustworthy spec is the code. Open, in this order:
composer.json — autoload.files (global helper file), declared illuminate/support constraint, PHP constraint.src/helpers.php (or wherever autoload.files points) — this is the entire public entry point. Note every declared function name verbatim.Tag/TagHelper class — attribute methods, content methods, escaping behavior, void-tag handling.Reconcile the scope contradiction first. The description advertises HTML tag helpers; the 1.1.0 release notes advertise auth, guest, and if helpers. Grep before you write a line:
grep -RnE "function (tag|tag_|auth|guest|if)\b" vendor/beyondcode/laravel-tag-helper/src/
If auth() is declared as a global function, you have a hard collision with Laravel's own auth() helper and the package is effectively unusable as-shipped. If if exists at all it cannot be a plain global function (if is a PHP reserved word) — find out how it is actually exposed before assuming anything.
Check the collision surface explicitly. In a scratch branch, dump the declared functions before and after autoloading:
$before = get_defined_functions()['user'];
// boot with the package installed
dd(array_diff(get_defined_functions()['user'], $before));
Anything unexpected in that diff is a CI/static-analysis hazard.
Minimal first use case — a link in Blade whose href, class, and disabled-style state come from view data. Instead of string concatenation:
{{-- before: unreadable once conditionals pile up --}}
<a href="{{ route('users.show', $user) }}"
class="btn {{ $user->isActive() ? 'btn-primary' : 'btn-secondary' }}"
@if($disabled) aria-disabled="true" @endif>
{{ $user->name }}
</a>
you get a fluent builder whose conditionals vanish when false:
{{ tag('a')
->attribute('href', route('users.show', $user))
->class(['btn', $user->isActive() ? 'btn-primary' : 'btn-secondary'])
->attribute('aria-disabled', $disabled ? 'true' : null)
->content(e($user->name)) }}
Confirm the exact method names (attribute, class, id, content) against the source — the archived docs are not reliable on signatures.
Before you go further, do the side-by-side: modern Blade gives you @class([...]), @checked, @selected, @disabled, anonymous components, and Illuminate\Support\HtmlString. If those cover the use case, prefer them and treat this package purely as a pattern reference.
Fluent builder inside Blade. The canonical usage is a single expression per element, rendered with {{ }}. Keep attribute calls on separate lines for reviewability; the whole point is that the markup shape is legible without @if sprinkling:
{{ tag('input')
->attribute('type', 'text')
->attribute('name', "address[{$index}][line1]")
->attribute('value', old("address.{$index}.line1"))
->attribute('required', $required)
->selfClosing() }}
Conditional attributes as the primary workflow. The high-value pattern is attribute($name, $value) where a null/false/empty value suppresses the attribute entirely. This is what replaces:
<option value="{{ $id }}" @if($selected) selected @endif>{{ $label }}</option>
with something re-evaluated from state, not from template branches. Validate the suppression semantics in the source — whether it is null, false, or both — and write a rendering test that pins the behavior, because this is the one behavior the whole ergonomic argument rests on.
Dynamic class and data-* from state. Build class lists from arrays or computed properties and let the helper flatten them; build data-* attributes from model/state arrays in one loop rather than one hardcoded attribute per line. Both of these are where hand-written Blade gets ugliest, so they are the best trial surface for a pilot.
Void and self-closing tags. <input>, <img>, <br>, <hr>, <meta>, <link> must not emit a closing tag. Find the exact mechanism (selfClosing() vs. an internal void-tag list) and test each one you use — emitting <input></input> will silently break some email clients and HTML validators even though browsers tolerate it.
Reusable fragments. Two viable integrations:
App\Support\Ui\TableRow that internally uses the helper, so if you later drop the package you change one file.<x-form.input :name="$name" :required="$required" /> delegating to the helper, or replacing it entirely.Either way, keep the helper calls behind a seam. Direct tag() calls scattered across forty views is the migration cost you are trying to avoid.
Mail templates. This is the strongest genuine use case: mail templates have no JavaScript escape hatch, so inline style attributes and conditional table markup are pure string assembly. A fluent builder with conditional attributes produces far more diffable templates. Test rendering by asserting on the compiled HTML string, not by eyeballing the inbox.
Nested content. Where the element wraps other markup, prefer a Blade slot/component over passing pre-rendered HTML into content(). Passing {!! ... !!} into the builder is the fastest way to lose escaping guarantees.
Testing pattern. Because this is a view-only utility, cover it with rendering tests rather than unit-testing internals:
$html = Blade::render('{{ tag("a")->attribute("href", "/x")->attribute("disabled", false)->content("Go") }}');
$this->assertStringContainsString('<a href="/x">Go</a>', $html);
$this->assertStringNotContainsString('disabled', $html);
Pin exactly the cases you depend on — attribute suppression, escaping of quote characters in attribute values, and void-tag output.
FROZEN — do not expect upstream updates note) over a Composer dependency, if you adopt it at all.auth() will fatal at boot against Laravel's own helper, and if cannot be a normal global function. Diff get_defined_functions() in a scratch branch before integrating and again after any Laravel upgrade.content() run e(), or does it accept raw HTML? Does it escape attribute values (quotes, <, &)? Double-escaping (&amp;) and non-escaping (XSS via a dynamic href) are both plausible failure modes. Never route user-controlled values into attribute calls without confirming the escaping path, and never pass {!! !!}-rendered output into content().auth/guest/if exist and evaluate guard state, they are presentation sugar at best. Keep @auth, @guest, @can, and policies/gates as the only authorization mechanism, and note in code review that a helper is not a gate. Their semantics are undocumented and unmaintained — an inverted check is a silent access-control defect.composer why-not beyondcode/laravel-tag-helper against your target constraint, and confirm behavior on your actual Laravel/PHP pair in a throwaway branch. There is no compatibility matrix to consult.php artisan view:clear and php artisan config:clear before touching code.null, false, '', 0. Some values you want to render (value="0", data-count="0") are exactly the ones a naive falsiness check drops. Pin this with a test.@class/@checked/@selected/@disabled plus anonymous components). If you vendor, name an owner. If you find it already in production, inventory call sites, confirm no global-symbol collisions, add rendering tests, and put it on the debt backlog rather than standardizing on it.How can I help you explore Laravel packages today?