kkxdev / laravel-collective-html-spatie-html-bridge
LaravelCollective/HTML compatibility wrapper backed by Spatie Laravel HTML — preserves existing Blade templates while migrating off laravelcollective/html.
Package info
github.com/kkxdev/laravel-collective-html-spatie-html-bridge
pkg:composer/kkxdev/laravel-collective-html-spatie-html-bridge
Requires
- php: ^8.0
- illuminate/http: ^8.0|^9.0|^10.0|^11.0|^12.0|^13.0
- illuminate/routing: ^8.0|^9.0|^10.0|^11.0|^12.0|^13.0
- illuminate/session: ^8.0|^9.0|^10.0|^11.0|^12.0|^13.0
- illuminate/support: ^8.0|^9.0|^10.0|^11.0|^12.0|^13.0
- spatie/laravel-html: ^3.0
Requires (Dev)
- mockery/mockery: ^1.4
- orchestra/testbench: ^6.0|^7.0|^8.0|^9.0|^10.0|^11.0
- phpunit/phpunit: ^9.5|^10.0|^11.0|^12.0
Suggests
None
Provides
None
Conflicts
Replaces
None
This package is auto-updated.
Last update: 2026-10-31 09:04:30 UTC
README
Drop-in compatibility layer — keep every existing
Form::andHtml::Blade call working while the rendering engine moves from the abandonedlaravelcollective/htmlto the actively-maintainedspatie/laravel-html.
The problem
laravelcollective/html is no longer maintained and will eventually block Laravel upgrades. A straight find-and-replace
migration rewrites every Blade template at once — a high-risk, all-or-nothing change in a production codebase.
The solution
This package sits between your templates and Spatie's library. It:
- exposes the identical
Form::*/Html::*facade API your Blade files already use - delegates all rendering to
spatie/laravel-htmlv3 under the hood - preserves every LaravelCollective behaviour (value precedence, checkbox hidden inputs, method spoofing, model binding, macros, …)
- allows zero template changes on day one, then gradual migration at your own pace
Requirements
| Requirement | Version |
|---|---|
| PHP | ^8.0 |
| Laravel | 8, 9, 10, 11, 12, 13 |
| spatie/laravel-html | ^3.0 |
Tested on each Laravel release with a PHP version it officially supports (L8 → PHP 8.0/8.1, L9 → 8.0/8.2, L10 → 8.1/8.3, L11 → 8.2/8.4, L12/L13 → 8.3). Laravel 8 apps on PHP 7.x cannot use this package (PHP 8.0 minimum).
This package conflicts with laravelcollective/html: remove it before installing.
Installation
1. Remove the old package
composer remove laravelcollective/html
2. Install this package
composer require kkxdev/laravel-collective-html-spatie-html-bridge
The service provider and facade aliases are auto-discovered via the extra.laravel section in composer.json — no
further config needed for most apps.
3. Manual registration (only if auto-discovery is disabled)
// config/app.php 'providers' => [ Kkxdev\HtmlCompat\HtmlCompatServiceProvider::class, ], 'aliases' => [ 'Form' => Kkxdev\HtmlCompat\Facades\Form::class, 'Html' => Kkxdev\HtmlCompat\Facades\Html::class, ],
Usage
Form facade
All existing Blade syntax continues to work without modification.
{{-- Open a form --}} {!! Form::open(['route' => 'users.store']) !!} {!! Form::label('name', 'Full name') !!} {!! Form::text('name', null, ['class' => 'form-control', 'required' => true]) !!} {!! Form::label('email', 'Email') !!} {!! Form::email('email', null, ['class' => 'form-control']) !!} {!! Form::submit('Save', ['class' => 'btn btn-primary']) !!} {!! Form::close() !!}
Model binding
Pre-populates every field from a model. Old flash input always wins over the model value.
{!! Form::model($user, ['route' => ['users.update', $user->id], 'method' => 'PUT']) !!} {!! Form::text('name') !!} {{-- filled from $user->name --}} {!! Form::email('email') !!} {{-- filled from $user->email --}} {!! Form::close() !!}
All supported Form methods
{{-- Inputs --}} {!! Form::text('name', null, ['class' => 'form-control']) !!} {!! Form::email('email') !!} {!! Form::password('password') !!} {!! Form::hidden('user_id', 42) !!} {!! Form::number('age', null, ['min' => 0, 'max' => 120, 'step' => 1]) !!} {!! Form::textarea('bio', null, ['rows' => 5, 'class' => 'form-control']) !!} {{-- Select with placeholder --}} {!! Form::select('status', $statuses, null, ['placeholder' => 'Choose...', 'class' => 'form-select']) !!} {{-- Checkbox — a hidden input is emitted automatically so an unchecked box still submits a value --}} {!! Form::checkbox('active', 1, true) !!} {{-- Renders: <input type="hidden" name="active" value="0"> <input type="checkbox" name="active" value="1" checked> --}} {{-- Suppress the hidden input --}} {!! Form::checkbox('active', 1, true, ['no_hidden' => true]) !!} {{-- Custom unchecked value --}} {!! Form::checkbox('active', 1, true, ['unchecked_value' => 'off']) !!} {{-- Radio --}} {!! Form::radio('role', 'admin') !!} {!! Form::radio('role', 'user', true) !!} {{-- File — remember to set 'files' => true on open --}} {!! Form::open(['url' => '/upload', 'files' => true]) !!} {!! Form::file('avatar') !!} {!! Form::close() !!} {{-- Label --}} {!! Form::label('first_name', 'First name', ['class' => 'form-label']) !!} {{-- If no value given, the name is auto-formatted: first_name → "First Name" --}} {!! Form::label('first_name') !!} {{-- Submit / button --}} {!! Form::submit('Save', ['class' => 'btn btn-primary']) !!} {!! Form::button('Reset', ['type' => 'reset', 'class' => 'btn btn-secondary']) !!}
Form::open() options
| Key | Description |
|---|---|
url |
Raw URL string or ['/path', param, …] array |
route |
Named route — 'users.store' or ['users.update', $id] |
action |
Controller action — 'UserController@store' or ['UserController@update', $id] |
method |
HTTP verb. PUT, PATCH, DELETE emit a hidden _method field |
files |
true → adds enctype="multipart/form-data" |
Any additional key is passed through as a raw HTML attribute on <form>.
{!! Form::open(['route' => 'users.store', 'class' => 'needs-validation', 'novalidate' => true]) !!}
Additional methods (LaravelCollective parity)
{{-- Inputs: DateTimeInterface values are formatted automatically --}} {!! Form::date('born', $carbon) !!} {{-- Y-m-d --}} {!! Form::time('at') !!} {!! Form::datetimeLocal('when') !!} {!! Form::month('m') !!} {!! Form::week('w') !!} {!! Form::url('site') !!} {!! Form::tel('phone') !!} {!! Form::search('q') !!} {!! Form::color('c') !!} {!! Form::range('r') !!} {!! Form::reset('Clear') !!} {!! Form::image('img/go.png', 'go') !!} {!! Form::datalist('cities', ['Delhi']) !!} {{-- Selects --}} {!! Form::selectRange('age', 18, 65) !!} {!! Form::selectYear('y', 2000, 2030) !!} {!! Form::selectMonth('m') !!} {{-- Components (Form and Html) --}} @php Form::component('bsText', 'components.form.text', ['name', 'value' => null, 'attributes' => []]); @endphp {!! Form::bsText('email') !!} {{-- Html --}} {!! Html::linkRoute('users.show', 'View', ['id' => 5]) !!} {!! Html::linkAction('UserController@index') !!} {!! Html::linkAsset('a.pdf') !!} {!! Html::mailto('a@b.c') !!} {!! Html::ul(['a', 'b']) !!} {!! Html::favicon('/f.ico') !!} {!! Html::meta('author', 'me') !!} {!! Html::nbsp(2) !!}
Global helpers link_to(), link_to_asset(), link_to_route() and link_to_action() are also provided.
Html facade
{!! Html::link('https://example.com', 'Visit us', ['class' => 'btn btn-link']) !!} {!! Html::script('/js/app.js', ['defer' => true]) !!} {!! Html::style('/css/app.css') !!} {!! Html::image('/img/logo.png', 'Logo', ['class' => 'img-fluid', 'width' => '120']) !!} {!! Html::tag('div', ['class' => 'container', 'id' => 'main']) !!} {{ Html::entities('<script>alert(1)</script>') }} {{ Html::decode('<b>Bold</b>') }}
Attribute behaviour
| Options array value | Result |
|---|---|
'class' => 'form-control' |
class="form-control" |
'required' => true |
required="required" |
'disabled' => false |
(omitted) |
'disabled' => null |
(omitted) |
'data-id' => 10 |
data-id="10" |
'aria-label' => 'Name' |
aria-label="Name" |
'autocomplete' => true |
autocomplete="autocomplete" |
data-* and aria-* attributes pass through without any special handling.
Value resolution precedence
For every field, the displayed value is resolved in this order:
1. old() — flash input from a previous (failed) form submission
2. Model value — attribute from the model bound via Form::model()
3. Explicit — second argument passed directly to the method
4. null — nothing rendered for value=""
Bracket notation for nested fields is fully supported:
{!! Form::text('user[name]') !!} {{-- old input key: user.name --}} {!! Form::text('address[city]') !!} {{-- old input key: address.city --}} {!! Form::text('meta[tags][]') !!} {{-- old input key: meta.tags --}}
Macro support
Register custom helpers in any service provider:
use Kkxdev\HtmlCompat\FormBuilder; use Kkxdev\HtmlCompat\HtmlBuilder; FormBuilder::macro('dateField', function (string $name, $value = null, array $options = []) { return $this->text($name, $value, array_merge(['type' => 'date'], $options)); }); HtmlBuilder::macro('badge', function (string $text, string $color = 'primary') { return new \Illuminate\Support\HtmlString( "<span class=\"badge bg-{$color}\">" . e($text) . "</span>" ); });
Then in Blade:
{!! Form::dateField('published_at', null, ['class' => 'form-control']) !!} {!! Html::badge('New', 'success') !!}
Unsupported methods — graceful degradation
Calling a method that does not exist will never throw a fatal error. Instead:
- A
warningis written to the Laravel log. - An empty
HtmlStringis returned so the page continues to render.
[2024-01-15 10:23:45] local.WARNING: Unsupported Form method called — no output rendered.
{"method":"customWidget","class":"Kkxdev\\HtmlCompat\\FormBuilder"}
This lets you discover undiscovered usages in production logs without taking the site down.
Supported method reference
Form
| Method | Notes |
|---|---|
open(array $options) |
Accepts url, route, action, method, files, plus raw HTML attrs |
model($model, array $options) |
Binds Eloquent model, array, or stdClass |
close() |
Outputs </form> and clears model binding |
token() |
Standalone <input type="hidden" name="_token"> |
label($name, $value, $options, $escape) |
Auto-capitalises snake_case names when no value given |
text($name, $value, $options) |
|
email($name, $value, $options) |
|
password($name, $options) |
Never pre-populated from old() or model |
hidden($name, $value, $options) |
|
number($name, $value, $options) |
Supports min, max, step |
textarea($name, $value, $options) |
|
select($name, $list, $selected, $options) |
placeholder key adds an empty first option |
checkbox($name, $value, $checked, $options) |
Auto-emits hidden input; opt out with no_hidden; custom value with unchecked_value |
radio($name, $value, $checked, $options) |
|
file($name, $options) |
|
submit($value, $options) |
|
button($value, $options) |
type key sets button type (default button) |
input($type, $name, $value, $options) |
Generic escape hatch for any input type |
getValueAttribute($name, $value) |
Exposed for callers that need direct value resolution |
resetModel() |
Detaches model without closing the form |
Html
| Method | Notes |
|---|---|
link($url, $title, $attributes, $secure, $escape) |
Title defaults to URL; escaping on by default |
script($url, $attributes) |
Renders <script src="…"></script> |
style($url, $attributes) |
Renders <link rel="stylesheet" href="…"> |
image($url, $alt, $attributes) |
Renders <img src="…" alt="…"> |
tag($tag, $attributes) |
Generic opening tag builder |
entities($value) |
htmlentities() wrapper |
decode($value) |
html_entity_decode() wrapper |
Migration strategy
Migration is entirely optional — this wrapper can be kept permanently. If you want to move off it over time, here is a safe path:
Phase 1 — Drop-in (day one)
composer remove laravelcollective/html composer require kkxdev/laravel-collective-html-spatie-html-bridge
No template changes. All Form:: and Html:: calls route through this package.
Phase 2 — Incremental template migration
Replace individual Blade calls with the native Spatie API as you touch each file:
{{-- Before --}} {!! Form::text('name', null, ['class' => 'form-control']) !!} {{-- After --}} {!! html()->text('name')->class('form-control') !!}
Both styles can coexist in the same template during the transition.
Phase 3 — Remove the wrapper
Once all templates have been migrated, remove this package:
composer remove kkxdev/laravel-collective-html-spatie-html-bridge
Architecture
Blade template
│
│ {!! Form::text('name') !!}
▼
Kkxdev\HtmlCompat\Facades\Form ← facade (container accessor)
│
▼
Kkxdev\HtmlCompat\FormBuilder ← stateful builder (singleton per request)
│
├── Support\ValueResolver ← old() › model › explicit › null
│
└── Support\AttributesBag ← boolean attr handling, data-*, aria-*
│
▼
spatie/laravel-html elements ← actual HTML rendering + escaping
Running tests
composer install vendor/bin/phpunit
The test suite runs on Orchestra Testbench (Laravel 8–13) and covers:
- Form open / close / method spoofing / CSRF
- All input types and their attribute handling
- Model binding (Eloquent, array, stdClass)
old()flash input precedence- Checkbox hidden input behaviour
- Nested bracket-notation field names
- Macro registration and invocation
- Graceful degradation for unknown methods
License
MIT — kkxdev