Introduction
Laravel Collective is a set of components that have been removed from Laravel's core framework and are no longer maintained by Taylor Otwell. Instead there is group of volunteers who decided to maintain following once famous packages:
- laravelcollective/annotations - allows to use annotation in PHP Docblocks, such as (@Hears, @Get, @Middleware)
- laravelcollective/html - delivers Form and Html conveniences (via HtmlBuilder aka Html facade and FormBuilder aka Form facade)
- laravelcollective/remote - allows to SSH into remote servers and run commands (via SSH facade)
- laravelcollective/iron-queue - ensures IronMQ support for the Laravel queue (not ready for Laravel 5.4 yet)
- laravelcollective/bus - delivers Laravel Command Bus functionality (not ready for Laravel 5.4 yet)
In this tutorial, we are going to examine laravelcollective/html, trying to verify its usefulness in your future projects. Let's start then!
Assumptions
- php and composer are installed
- laravel command is available
Solution
Create Laravel project from scratch
laravel new testing-laravel-collective
As you probably know, Laravel base project comes with three packages
"require": {
"php": ">=5.6.4",
"laravel/framework": "5.4.*",
"laravel/tinker": "~1.0"
}
In order to add Laravel Collective Html package, update composer dependency as shown below
composer require "laravelcollective/html":"^5.4.0"
Please note that Laravel Collective Html is not standalone package, it requires five other illuminate packages such as
"require": {
"php": ">=5.6.4",
"illuminate/http": "5.4.*",
"illuminate/routing": "5.4.*",
"illuminate/session": "5.4.*",
"illuminate/support": "5.4.*",
"illuminate/view": "5.4.*"
}
Typically, when you create new Laravel project, you are actually pulling laravel/framework package which replaces all illuminate packages required by Laravel Collective Html.
"replace": {
"illuminate/http": "self.version",
"illuminate/routing": "self.version",
"illuminate/session": "self.version",
"illuminate/support": "self.version",
"illuminate/view": "self.version"
}
As a result, you only see laravelcollective/html package being added to your composer.json.
If this part is clear, let's move on to adding Laravel Collective Html provider to the providers array of config/app.php
'providers' => [
// ...
Collective\Html\HtmlServiceProvider::class,
// ...
],
If you enjoy Laravel Facades, don't forget to add two class aliases to the aliases array of config/app.php
'aliases' => [
// ...
'Form' => Collective\Html\FormFacade::class,
'Html' => Collective\Html\HtmlFacade::class,
// ...
],
Installation part is over, so let's try to explore a few features from Laravel Collective Html package.
Features
General Form creation
Laravel Collective Html | Laravel |
---|---|
{!! Form::open(['url' => '/']) !!} {!! Form::close() !!} |
<form method="post" action="{{ url('/') }}" accept-charset="utf-8"> </form> |
If you care about HTML naming conventions, please keep in mind that Laravel Collective Html produces form with uppercased method and accept-charset as shown below.
<form method="POST" action="http://blogging.app" accept-charset="UTF-8">
<input name="_token" type="hidden" value="XnZwoGRTD4GszGv1kgTuFO9U8rlmMCX4HH7evCdM">
</form>
Sophisticated Form creation
Laravel Collective Html | Laravel |
---|---|
{!! Form::open(['route' => 'home', 'method' => 'put', 'files' => 'yes']) !!} {!! Form::close() !!} |
<form method="post" action="{{ route('home') }}" accept-charset="utf-8" enctype="multipart/form-data"> {{ method_field('PUT') }} {{ csrf_field() }} </form> |
Form Model Bindings
Having following user
{
"id":1,
"name":"Miss Dianna Funk",
"email":"isadore.farrell@example.com",
"active":0,
"created_at":"2017-07-08 16:30:36",
"updated_at":"2017-07-08 16:30:36"
}
passed to the Blade template, you can easily bind it to the form, using Form::model
method, instead of Form::open
.
{!! Form::model($user, ['action' => 'HomeController@user']) !!}
{!! Form::label('email', 'E-mail Address') !!}
{!! Form::email('email') !!}
{!! Form::label('active', 'Is Active?') !!}
{!! Form::checkbox('active', 'Yes') !!}
{!! Form::label('updatedAt', 'Updated At') !!}
{!! Form::date('updatedAt') !!}
{!! Form::label('created_at', '<strong>Created At</strong>', ['class' => 'btn btn-success', 'id' => 'created_at_id'], false) !!}
{!! Form::datetime('created_at', null, ['class' => 'btn']) !!}
{!! Form::submit('Edit') !!}
{!! Form::close() !!}
However there are some caveats.
- if you want to use
Form::date
, make sure your model formats Carbon object properly. In order to do it, you could use Eloquent Mutators or benefit from traitCollective\Html\Eloquent\FormAccessible
- you must name your inputs exactly the same as properties in your Eloquent model. You will face the wall if you intend to construct the input names as arrays such as
user[email]
,user[updatedAt]
.
If you wanted to achieve the same with plain Laravel, here is the snippet
<form action="{{ action('HomeController@user') }}" method="post" accept-charset="utf-8">
{{ csrf_field() }}
<label for="email">Email</label>
<input type="email" id="email" name="email" value="{{ old('email', request('email') ?? $user->email ?? null) }}" />
<label for="active">Is Active?</label>
<input type="checkbox" id="active" name="active" value="Yes" @if(old('active', request('active') ?? $user->active ?? null)) checked @endif />
<label for="updatedAt">Updated At</label>
<input name="updatedAt" type="date" value="{{ old('updatedAt', request('updatedAt') ?? $user->updated_at ?? null) }}" id="updatedAt">
<label for="created_at" class="btn btn-success" id="created_at_id"><strong>Created At</strong></label>
<input class="btn" name="created_at" type="datetime" value="{{ old('createdAt', request('createdAt') ?? $user->created_at ?? null) }}" id="created_at">
<input type="submit" value="Edit" />
</form>
Macros and Components
In order to create a Macro or Component, head over to your AppServiceProvider and inside boot method place the following
FormBuilder::macro('tekmi', function () {
return '<input type="text" value="tekmi">';
});
FormBuilder::component(
'bsTekmi',
'components.form.text',
['name', 'value' => null, 'attributes' => []]
);
Additionally, Components use Blade templates, so don't forget to create one in resources/views/components/form/text.blade.php with following snippet
<div class="form-group">
{{ Form::label($name, null, ['class' => 'control-label tekmi-label']) }}
{{ Form::text($name, $value, array_merge(['class' => 'form-control tekmi-text'], $attributes)) }}
</div>
Having Macro and Component ready, here is how this can be invoked inside your Blade templates
{!! Form::tekmi() !!}
{!! Form::bsTekmi('first_name') !!}
{!! Form::bsTekmi('last_name', 'tekmi', ['class' => 'form-control tekmi-text']) !!}
If you wanted to achieve something similar in pure Laravel, it should be pretty straightforward. Just create a custom class which:
- will use trait
Illuminate\Support\Traits\Macroable
from Laravel Illuminate Support - will reuse the logic from trait
Collective\Html\Componentable
from Laravel Collective Html
Other FormBuilder and HtmlBuilder conveniences
I went carefully through all the public methods from Collective\Html\FormBuilder and Collective\Html\HtmlBuilder classes, using the most complicated combination of parameters possible, as presented below.
{{ Form::token() }}
{!! Form::textarea('description', 'Text', ['size' => '20x5', 'id' => 'description', 'name' => 'description', 'class' => 'text text-long']) !!}
{!! Form::hidden('hidden', 'Hidden', ['id' => 'hidden', 'name' => 'hidden', 'class' => 'text text-hidden']) !!}
{!! Form::password('password', ['id' => 'password', 'name' => 'password', 'class' => 'text text-center']) !!}
{!! Form::search('search', 'Search', ['id' => 'search', 'name' => 'search', 'class' => 'text text-left']) !!}
{!! Form::tel('tel', '031647555333', ['id' => 'tel', 'name' => 'tel', 'class' => 'text text-left']) !!}
{!! Form::number('number', '0.05', ['min' => 0, 'step' => 0.01, 'id' => 'number', 'name' => 'number', 'class' => 'text text-left']) !!}
{!! Form::datetimeLocal('localDateTime', '2017-10-10T21:10:10', ['id' => 'localDateTime', 'name' => 'localDateTime', 'class' => 'text text-left']) !!}
{!! Form::time('time', '10:30:55', ['id' => 'time', 'name' => 'time', 'class' => 'text text-left']) !!}
{!! Form::url('url', 'http://tekmi.nl', ['id' => 'url', 'name' => 'url', 'class' => 'text text-left']) !!}
{!! Form::select('size', ['L' => 'Large', 'S' => 'Small'], null, ['placeholder' => 'Pick a size...', 'id' => 'size', 'class' => 'select select-big'], ['L' => ['class' => 'option option-red']]) !!}
{!! Form::select('animal',['Cats' => ['leopard' => 'Leopard'], 'Dogs' => ['spaniel' => 'Spaniel']], 'spaniel', ['class' => 'select-with-optgroup']) !!}
{!! Form::selectRange('range', 10, 30, 20, ['id' => 'range', 'class' => 'range range-20']) !!}
{!! Form::reset('resetButton', ['name' => 'reset', 'class' => 'reset-now']) !!}
{!! Form::selectMonth('month', 5, ['id' => 'month', 'class' => 'month month-1'], '%h') !!}
{!! Form::selectYear('birthdayDay', 2020, 2000, 2001, ['id' => 'year']) !!}
{!! Form::file('file', ['id' => 'file', 'name' => 'file', 'class' => 'file']) !!}
{!! Form::radio('radio', 'Switch On', null, ['id' => 'radio-1', 'name' => 'radio', 'class' => 'radio tune-in']) !!}
{!! Form::radio('radio', 'Switch Off', true, ['id' => 'radio-2', 'name' => 'radio', 'class' => 'radio tune-in']) !!}
{!! Form::image('images/image.jpg', 'Image Button', ['class' => 'reset-now']) !!}
{!! Form::color('color', '#ff0000', ['id' => 'color', 'class' => 'text-color']) !!}
{!! Form::button('button', ['id' => 'button', 'class' => 'btn btn-success']) !!}
{!! Html::script('app.js', ['type' => 'text/javascript'], true) !!}
{!! Html::style('app.css', ['media' => 'screen'], true) !!}
{!! Html::image('images/image.jpg', 'Image Alt', ['class' => 'image'], true) !!}
{!! Html::favicon('favicon.ico', ['type' => 'image/x-icon'], true) !!}
{!! Html::link('users/1/edit', '<i>Title</i>', ['class' => 'link'], true, false) !!}
{!! Html::linkAsset('img/image.jpg', 'Image', ['class' => 'img'], true) !!}
{!! Html::linkRoute('home', 'Home', ['param' => 'value'], ['class' => 'btn']) !!}
{!! Html::linkAction('HomeController@index', 'Home', ['param' => 'value'], ['class' => 'btn']) !!}
{!! Html::nbsp(5) !!}
{!! Html::mailto('ala@ala.nl', '<i>Mail me</i>', ['class' => 'mail'], false) !!}
{!! Html::ol(['a', 'b', 'c' => ['d', 'e']], ['class' => 'ol-list']) !!}
{!! Html::ul(['a', 'b', 'c' => ['d', 'e']], ['class' => 'ul-list']) !!}
{!! Html::dl(['a' => 'a character', 'b', 'c multiple' => ['d' => 'd is here', 'e' => 'e is there']], ['class' => 'dl-list']) !!}
{!! Html::meta('viewport', 'width=device-width, initial-scale=1') !!}
{!! Html::meta(null, null, ['charset' => 'utf-8']) !!}
{!! Html::meta(null, 'IE=edge', ['http-equiv' => 'X-UA-Compatible']) !!}
{!! Html::tag('input', '', ['type' => 'week', 'value' => '2017-W05']) !!}
Creating pure Laravel counterparts should be straightforward. If you are struggling though, please check an example github repository I've created.
URL generation with global helpers
Laravel Collective Html | Laravel |
---|---|
{{ link_to('/') }} {{ link_to('/', 'Title') }} {{ link_to('/', 'Title', ['class' => 'btn']) }} {{ link_to('/', 'Title', ['class' => 'btn'], true) }} {{ link_to('/', '<strong>Title</strong>', ['class' => 'btn'], true, false) }} |
<a href="{{ url('/') }}">{{ url('/') }}</a> <a href="{{ url('/') }}">Title</a> <a class="btn" href="{{ url('/') }}">Title</a> <a class="btn" href="{{ url('/', [], true) }}">Title</a> <a class="btn" href="{{ url('/', [], true) }}"><strong>Title</strong></a> |
{{ link_to_asset('img/image.jpg') }} {{ link_to_asset('img/image.jpg', 'Image') }} {{ link_to_asset('img/image.jpg', 'Image', ['class' => 'img']) }} {{ link_to_asset('img/image.jpg', 'Image', ['class' => 'img'], true) }} |
<a href="{{ asset('img/image.jpg') }}">{{ asset('img/image.jpg') }}</a> <a href="{{ asset('img/image.jpg') }}">Image</a> <a class="img" href="{{ asset('img/image.jpg') }}">Image</a> <a class="img" href="{{ asset('img/image.jpg', true) }}">Image</a> |
{{ link_to_route('home') }} {{ link_to_route('home', 'Home') }} {{ link_to_route('home', 'Home', ['param' => 'value']) }} {{ link_to_route('home', 'Home', ['param' => 'value'], ['class' => 'btn']) }} |
<a href="{{ route('home') }}">{{ route('home') }}</a> <a href="{{ route('home') }}">Home</a> <a href="{{ route('home', ['param' => 'value']) }}">Home</a> <a class="btn" href="{{ route('home', ['param' => 'value']) }}">Home</a> |
{{ link_to_action('HomeController@index') }} {{ link_to_action('HomeController@index', 'Home') }} {{ link_to_action('HomeController@index', 'Home', ['param' => 'value']) }} {{ link_to_action('HomeController@index', 'Home', ['param' => 'value'], ['class' => 'btn']) }} |
<a href="{{ action('HomeController@index') }}">{{ action('HomeController@index') }}</a> <a href="{{ action('HomeController@index') }}">Home</a> <a href="{{ action('HomeController@index', ['param' => 'value']) }}">Home</a> <a class="btn" href="{{ action('HomeController@index', ['param' => 'value']) }}">Home</a> |
It's worth to know that Laravel's route and action methods provide third parameter called absolute, which is not covered by the Laravel Collective counterparts. If you set it to false, you will get the URI without the domain (e.g: /?param=value or /home?param=value).
<a class="btn" href="{{ route('home', ['param' => 'value'], false) }}">Home</a>
<a class="btn" href="{{ action('HomeController@index', ['param' => 'value'], false) }}">Home</a>
Summary
We have gone through long journey together, trying to discover hidden gems of laravelcollective/html package. Along the way we experienced some issues and shortcomings of it, so let's try to enumerate them now
- not every input type from HTML standards is covered (missing types: range, week, month)
- model bindings cannot handle html name arrays (e.g
users[name]
,users[description]
) -
misleading and not comprehensive documentation (better to explore methods from FormBuilder and HtmlBuilder classes)
- misleading information about
Form::textarea
which doesn't extendForm::input
Form::textarea
uses not documented option like size=10x5- zero information about FormAccesible trait
- no information about HTML facade and its conveniences
- misleading information about
- package tightly coupled to Laravel, but not adapted into Lumen (here is an explanation)
- generating URLs via
link_to*
methods orHTML::link*
look a bit redundant, comparing to Laravel route, action, url global functions. link_to_action
andlink_to_route
miss 3rd parameter absolute, which is used by Laravel route and action helpersFormBuilder::selectRange
,FormBuilder::selectYear
andFormBuilder::selectMonth
don't use 5th parameter fromFormBuilder::select
- Inconsistent HTML naming conventions
If I missed anything relevant, please write your findings on Twitter.