Why a CRUD Generator
CRUD is an essential part of many applications – at the very least, even if they mainly interact with some external API, most apps need some kind of administrative panel where users can Create, Read, Update and Delete different kind of (often related) objects. This often translates more or less directly into operations on a relational database.
Coding these features sometimes can be redundant, with similar code in models, controllers, views of different objects.
Options and alternatives
The Laravel ecosystem offers a great variety of packages and features, from the included basic command line class generator (php artisan make:whatever) to complex plugins – complete applications to manage CRUD features, Authentication/Authorization, even creating DB tables and related classes directly form the web app itself.
For my current project, I was looking for something in between – a way to speed up the basics, generating boilerplate code for CRUD operations in Models, Controllers and Views. Jump start with the repetitive code, then focus on the app’s specific features.
This can be tricky: automatically generating code is not a trivial task, and additional features and customization options add up exponentially in terms of complexity. Complexity means losing control over how things work, how the code is written, and so on. So, I checked some (actually really great!) packages, and put aside most of them because.. they were too much. They imposed their way of doing things, or just some pre-made architectural choice. I was just looking for a smart shortcut for creating common code, easy to edit and build on, not a new framework over a framework, or even a smart CMS.
Why Crestapps Code Generator
Laravel Code Generator (GitHub – Documentation) has the features and philosophy I was looking for. It’s quite recent and still actively developed.
In my project i needed features and assets provided by other great Laravel packages, namely:
- Internationalization and localization of the interface using Gettext, provided by https://github.com/Belphemur/laravel-gettext
- Multilingual Contents, using laravel Translatable https://github.com/dimsav/laravel-translatable
- A nice and ready admin template, https://github.com/jeroennoten/Laravel-AdminLTE
Integrating these features with other cms-like admin plugins would probably be quite complex and time consuming (countering the advantages and adding another layer of complexity). With CrestApps, it was easy to build from the boilerplate code generated. Integrating not trivial features was a breeze.
That’s just the first step: it was easy to customize the generator’s templates, so that the generated models, controllers, formRequests and views already included these additional features – or were ready to.
As a sidetone, the package author is also quite responsive, answering regularly to feature requests, bugs, and pull requests – it’s a pleasure to contribute with some basic issues and pull requests when a project is useful for your immediate needs, and the author actively listens.
Integrating other features
So, here we are. With everything installed as explained in each package’s Readme, and the vendor asset published, the first thing to do is copy the contents of /resources/codegenerator-templates/ default to a custom folder. This is where most integration will take place. A config setting allows us to tell CrestApps Generators to use our custom templates / stubs instead of his own default.
Integrating AdminLTE is just a matter of merging the bits you want from CrestApps’ layout with AdminLTE’s master view (assuming php artisan vendor:publish was already run at least once). I generated a dummy view from CreastApps for that purpose, then created a view/layout/app.blade.php layout for the app by merging the two.
Our custom generator view stubs (index, create, edit and view.blade.stub) will need to extend AdminLTE’s views/vendor/adminlte/page.blade.php (which has to be edited to extend our app’s layout, instead of his own master). Here is an example:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//index.blade.stub | |
@extends('adminlte::page') | |
@section('title') | |
{{ isset([% model_header %]) ? [% model_header %] : _i("[% model_name_plural_cap %]") }} | |
@endsection | |
@section('content_header') | |
{{ isset([% model_header %]) ? [% model_header %] : _i("[% model_name_plural_cap %]") }} | |
@endsection | |
@section('css') | |
@if(config('adminlte.plugins.datatables')) | |
<!-- DataTables --> | |
<link rel="stylesheet" href="//cdn.datatables.net/v/bs/dt-1.10.13/datatables.min.css"> | |
@endif | |
@endsection | |
@section('js') | |
@if(config('adminlte.plugins.datatables')) | |
<!-- DataTables --> | |
<script src="//cdn.datatables.net/v/bs/dt-1.10.13/datatables.min.js"></script> | |
@endif | |
@endsection | |
@section('content') | |
//[..] | |
@endsection |
(Laravel-AdminLTE also uses optional sections for page title, header, css and js)
The rest is just a matter of fixing the details, setting the plugins’ options, editing the layout’s menu, and so on.
Internationalization and localization
Internationalizing the interface is quite simple, using the Laravel-Gettext plugin. Just surrounding every text with the _i() function (in views and controllers mostly). Extracting and updating the language files is quick and easy with tools like poEdit.
(If you are not using gettext, and prefer laravel’s own localization system, crestapps generator has an option for this)
Full multi-language support for the dynamic contents is a little bit trickier. I use the Laravel-Translatable package, whose approach is clearly explained in its docs. We need to create an additional table for every entity to store the translations, and add the Translatable Trait to the models, specifying what fields are “translatable”.
So, I like to prepare my models by adding the required bits in the model stub (I’ll update the field list directly in the generated models), e. g.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
//extract from model.stub | |
use Dimsav\Translatable\Translatable; | |
use Illuminate\Database\Eloquent\Model; | |
[% use_soft_delete %] | |
class [% model_name_class %] extends Model | |
{ | |
use Translatable; | |
[% use_soft_delete_trait %] | |
[% time_stamps %] | |
/** | |
* The database table used by the model. | |
* | |
* @var string | |
*/ | |
protected $table = '[% table %]'; | |
[% primary_key %] | |
/** | |
* Attributes that should be mass-assignable. | |
* | |
* @var array | |
*/ | |
protected $fillable = [% fillable %]; | |
/** | |
* The attributes that should be mutated to dates. | |
* | |
* @var array | |
*/ | |
protected $dates = [% dates %]; | |
/** | |
* Property for Translatable - list of tranlsated fields | |
* | |
* @var array | |
*/ | |
public $translatedAttributes = ['name',]; | |
/** | |
* Static list of translatable fields (usually the same | |
* as $translatedAttributes) for use in Controller before | |
* instantiating the model | |
* | |
* @var array | |
*/ | |
public static $translatedFields = ['Name' =>'name']; | |
/** | |
* The relations to eager load on every query. | |
* | |
* @var array | |
*/ | |
// (optionally) | |
protected $with = ['translations']; |
In my custom controller stub I added various things.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace [% namespace %]; | |
use Session; | |
[% use_command_placeholder %] | |
use Illuminate\Support\Str; | |
class [% controller_name %] [% controller_extends %] | |
{ | |
protected $[% model_name %]; // Model Instance | |
[% constructor %] | |
/** | |
* Display a listing of the [% model_name_plural %] . | |
* | |
* @return Illuminate\View\View | |
*/ | |
public function index(Request $request) | |
{ | |
$[% model_name_plural %] = [% model_name_class %]::[% with_relations_for_index %] | |
search($request->get('s')) | |
->type('parameter', $request->get('parameter')) | |
->orderBy($request->get('o','id'),$request->get('d','desc')) | |
->paginate([% models_per_page %]); | |
Session::flash('backUrl', $request->fullUrl()); | |
return view('[% index_view_name %]'[% view_variables_for_index %]); | |
} | |
/** | |
* Show the form for creating a new {{modelName}}. | |
* | |
* @return Illuminate\View\View | |
*/ | |
public function create() | |
{ | |
[% relation_collections %] | |
if (Session::has('backUrl')) { Session::keep('backUrl');} | |
return view('[% create_view_name %]'[% view_variables_for_create %]); | |
} | |
/** | |
* Store a new [% model_name %] in the storage. | |
* | |
* @param [% request_fullname %][% request_variable %] | |
* | |
* @return Illuminate\Http\RedirectResponse | Illuminate\Routing\Redirector | |
*/ | |
public function store([% type_hinted_request_name %]) | |
{ | |
[% call_affirm %] | |
$[% data_variable %] = [% call_get_data %]; //$this->getRequestsData($request); | |
[% on_store_setter %] | |
$this->[% model_name %] = [% model_name_class %]::create( $[% data_variable %] ); | |
$[% data_variable %] = $this->requestDataTranslateOrNew ( $[% data_variable %] ); | |
$this->[% model_name %]->update($[% data_variable %]); | |
Session::flash('success_message', _i('[% model_name_class %] was added!') ); | |
Session::has ( 'backUrl' ) ? $url = Session::get('backUrl') : $url = route('[% index_route_name %]'); | |
return redirect($url); | |
} | |
/** | |
* Display the specified [% model_name %]. | |
* | |
* @param int $id | |
* | |
* @return Illuminate\View\View | |
*/ | |
public function show($id, Request $request) | |
{ | |
$[% model_name %] = [% model_name_class %]::[% with_relations_for_show %]findOrFail($id); | |
Session::flash('backUrl', $request->fullUrl()); | |
return view('[% show_view_name %]'[% view_variables_for_show %]); | |
} | |
/** | |
* Show the form for editing the specified [% model_name %]. | |
* | |
* @param int $id | |
* | |
* @return Illuminate\View\View | |
*/ | |
public function edit($id) | |
{ | |
$[% model_name %] = [% model_name_class %]::findOrFail($id); | |
[% relation_collections %] | |
if (Session::has('backUrl')) { Session::keep('backUrl'); } | |
return view('[% edit_view_name %]'[% view_variables_for_edit %]); | |
} | |
/** | |
* Update the specified [% model_name %] in the storage. | |
* | |
* @param int $id | |
* @param [% request_fullname %][% request_variable %] | |
* | |
* @return Illuminate\Http\RedirectResponse | Illuminate\Routing\Redirector | |
*/ | |
public function update($id, [% type_hinted_request_name %]) | |
{ | |
[% call_affirm %] | |
$[% data_variable %] = [% call_get_data %]; //$this->getRequestsData($request); | |
[% on_update_setter %] | |
$this->[% model_name %] = [% model_name_class %]::findOrFail($id); | |
$[% data_variable %] = $this->requestDataTranslateOrNew($[% data_variable %] ); | |
$this->[% model_name %]->update($[% data_variable %] ); | |
Session::flash('success_message', _i('[% model_name_class %] was updated!')); | |
Session::has ( 'backUrl' ) ? $url = Session::get('backUrl') : $url = route('[% index_route_name %]'); | |
return redirect($url); | |
} | |
/** | |
* Remove the specified [% model_name %] from the storage. | |
* | |
* @param int $id | |
* | |
* @return Illuminate\Http\RedirectResponse | Illuminate\Routing\Redirector | |
*/ | |
public function destroy($id) | |
{ | |
$[% model_name %] = [% model_name_class %]::findOrFail($id); | |
$[% model_name %]->delete(); | |
Session::flash('success_message', _i('[% model_name_class %] was deleted!') ); | |
Session::has ( 'backUrl' ) ? $url = Session::get('backUrl') : $url = route('[% index_route_name %]'); | |
return redirect($url); | |
} | |
[% affirm_method %] | |
[% get_data_method %] | |
[% upload_method %] | |
protected function getTraslatedRequestData(Request $request, $data ) { | |
foreach (config('app.app_languages') as $locale) { | |
foreach ([% model_name_class %]::$translatedFields as $field) { | |
$data[$field .'-'.$locale] = !empty($request->input($field .'-'.$locale)) ? $request->input($field .'-'.$locale) : null; | |
//repeat with other translatable fields | |
} | |
} | |
return $data; | |
} | |
protected function requestDataTranslateOrNew( $data ) { | |
foreach (config('app.app_languages') as $locale => $language) { | |
foreach ($this->[% model_name %]->translatedAttributes as $field) { | |
if(!is_null($data["$field-{$locale}"])) { | |
$this->[% model_name %]->translateOrNew ( $locale )->{$field} = $data["$field-{$locale}"]; | |
} | |
//repeat for other translatable fields | |
} | |
} | |
return $data; | |
} | |
} |
- The query in the index method has some additional scopes (defined in the model) for filtering (and sorting) the records.
- I use a Session flash variable (backUrl) in index, show, etc. for redirecting to where the user came from, instead of index, after creating, updating or deleting a record.
- I store the current model as a property in the controller, used when needed.
- Most changes are related to the Translatable package, like the last two methods, used to get the form data for translations, and store/update it using Translatable’s methods.
Validation happens in the FormRequest, where I added a stub method for altering the data BEFORE the validation (e.g. for creating a required slug from the title, leaving the “required” field optional in the form).
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
//form-request.stub | |
namespace [% app_name %]\Http\Requests; | |
use Illuminate\Foundation\Http\FormRequest; | |
class [% form_request_class %] extends FormRequest | |
{ | |
/** | |
* Determine if the user is authorized to make this request. | |
* | |
* @return bool | |
*/ | |
public function authorize() | |
{ | |
return true; | |
} | |
public function prepareForValidation () | |
{ | |
$data = $this->all (); | |
//Alter the field values as needed, i.e. $data['whatever'] = .. | |
//$data['slug_en'] = !empty($this->input('slug_en')) ? $this->input('slug_en') : Str::slug($data['title_en']); | |
$this->replace ($data); | |
} | |
/** | |
* Get the validation rules that apply to the request. | |
* | |
* @return array | |
*/ | |
public function rules() | |
{ | |
return [ | |
[% validation_rules %] | |
]; | |
} | |
[% get_data_method %] | |
[% upload_method %] | |
} |
I added some partials in my views directory to take care of the multilingual fields. My custom CrestApps generator templates use these partials to add the language relative parts (tabbed form fields in edit and create views, translations in show and index view, with some other addition).
Here are the form ones:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//form.blade.stub | |
[% form_fields_html %] | |
{{--Keep one of the following--}} | |
@include('admin.partials.single_field_translation_form_text_input', ['current_model' => isset($[% model_name %]) ? $[% model_name %] : null, 'field' => 'name']) | |
@include('admin.partials.multiple_fields_translation_form_text_input_tabs', | |
['current_model' => isset($[% model_name %]) ? $[% model_name %] : null, | |
'translatable_fields' => \App\Models\[% model_name_class %]::$translatedFields] | |
) | |
// views/admin/partials/multiple_fields_translation_form_text_input_tabs.blade.php | |
// (tabs for showing single language translation fields in a form). | |
// Whether fields are strings or texts is specified in a config file) | |
<div class="col-sm-8 col-sm-offset-2"> | |
<div class="nav-tabs-custom"> | |
<ul class="nav nav-tabs"> | |
<?php $count = 0; ?> | |
@foreach(config( 'app.app_languages' ) as $code => $lang) | |
<li class="@if(!$count)active @endif"> | |
<a href="#tab_{{ $code }}" data-toggle="tab" aria-expanded="@if(!$count++) true @else false @endif"> | |
<img src="/images/flags/{{ $code }}.png"> {{ $code }} | |
</a> | |
</li> | |
@endforeach | |
</ul> | |
<div class="tab-content"> | |
<?php $count = 0; ?> | |
@foreach(config( 'app.app_languages' ) as $code => $lang) | |
<div class="tab-pane @if(!$count++)active @endif" id="tab_{{ $code }}"> | |
<h4> {{ _i($lang) }} </h4> | |
@foreach($translatable_fields as $field) | |
@if( !in_array ( $field, config ('enums.textarea_fields') ) ) | |
<div class="form-group"> | |
<label for="{{$field}}-{{ $code }}" class="control-label col-md-2 col-sm-2 col-xs-12"> | |
{{ _i( \Illuminate\Support\Str::studly($field) ) }} <img src="/images/flags/{{ $code }}.png"> | |
</label> | |
<div class="col-md-10 col-sm-10 col-xs-12"> | |
<input id="{{$field}}-{{ $code }}" class="form-control col-md-7 col-xs-12" name="{{$field}}-{{ $code }}" type="text" value="{{ old($field.'-'.$code, isset($current_model) && isset($current_model->translate($code)->{$field}) ? $current_model->translate($code)->{$field} : null) }}"> | |
</div> | |
</div> | |
@else | |
<div class="form-group"> | |
<label for="{{$field}}-{{ $code }}" class="control-label col-md-2 col-sm-2 col-xs-12"> | |
{{ _i( \Illuminate\Support\Str::studly($field) ) }} <img src="/images/flags/{{ $code }}.png"> | |
</label> | |
<div class="col-md-10 col-sm-10 col-xs-12"> | |
<textarea class="form-control editor" name="{{ $field }}-{{ $code }}" cols="50" rows="10" id="{{ $field }}-{{ $code }}" placeholder="Description">{{ isset($current_model) && isset($current_model->translate($code)->{$field}) ? $current_model->translate($code)->{$field} : null }}</textarea> | |
</div> | |
</div> | |
@endif | |
@endforeach | |
</div> | |
@endforeach | |
</div> | |
</div> | |
</div> | |
// views/admin/partials/single_field_translation_form_text_input.blade.php | |
@foreach(config( 'app.app_languages' ) as $code => $lang) | |
<div class="form-group"> | |
<label for="{{$field}}-{{ $code }}" class="control-label col-md-2 col-sm-2 col-xs-12"> | |
{{ _i( \Illuminate\Support\Str::studly($field) ) }} {{ _i($lang) }} <img src="/images/flags/{{ $code }}.png"> | |
</label> | |
<div class="col-md-10 col-sm-10 col-xs-12"> | |
<input id="{{$field}}-{{ $code }}" class="form-control col-md-7 col-xs-12" name="{{$field}}-{{ $code }}" type="text" value="{{ old($field.'-'.$code, isset($current_model) && isset($current_model->translate($code)->name) ? $current_model->translate($code)->name : null) }}"> | |
</div> | |
</div> | |
@endforeach |
here I included both alternatives, a simple partial for simpler entities (where you only need to translate the name / title) and a tabs widget containing the translatable fields for each language. Most of the variables come from the relevant controller action, while some costant values are set in config files.
Here are the “show” parts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// in show.blade.stub | |
<div class="panel-body"> | |
<dl class="dl-horizontal"> | |
[% table_rows %] | |
{{--Keep one of the following--}} | |
@include('admin.partials.single_field_translation_show', ['current_model' => isset($[% model_name %]) ? $[% model_name %] : null, 'field_label' => 'Name', 'field' => 'name']) | |
@include('admin.partials.multiple_fields_translation_show', ['current_model' => isset($[% model_name %]) ? $[% model_name %] : null, 'translatable_fields' => \App\Models\[% model_name_class %]::$translatedFields ] ) | |
</dl> | |
</div> | |
// views/admin/partials/single_field_translation_show.blade.php | |
@foreach(config( 'app.app_languages' ) as $code => $lang) | |
<dt>{{ _i($field_label) }} {{ _i($lang) }} <img src="/images/flags/{{ $code }}.png"></dt> | |
<dd>{{ isset($current_model) && isset($current_model->translate($code)->{$field}) ? $current_model->translate($code)->{$field} : null }}</dd> | |
@endforeach | |
// views/admin/partials/multiple_fields_translation_show.blade.php | |
<div class="col-sm-10"> | |
<div class="nav-tabs-custom"> | |
<ul class="nav nav-tabs"> | |
<?php $count = 0; ?> | |
@foreach(config( 'app.app_languages' ) as $code => $lang) | |
<li class="@if(!$count)active @endif"> | |
<a href="#tab_{{ $code }}" data-toggle="tab" aria-expanded="@if(!$count++) true @else false @endif"> | |
<img src="/images/flags/{{ $code }}.png"> {{ $code }} | |
</a> | |
</li> | |
@endforeach | |
</ul> | |
<div class="tab-content"> | |
<?php $count = 0; ?> | |
@foreach(config( 'app.app_languages' ) as $code => $lang) | |
<div class="tab-pane @if(!$count++)active @endif" id="tab_{{ $code }}"> | |
<h4> {{ _i($lang) }} </h4> | |
@foreach($translatable_fields as $field_label => $field) | |
<dt>{{ _i($field_label) }} <img src="/images/flags/{{ $code }}.png"></dt> | |
@if( !in_array ( $field, config('enums.textarea_fields'))) | |
<dd>{{ isset($current_model) && isset($current_model->translate($code)->$field) ? $current_model->translate($code)->$field : null }}</dd> | |
@else | |
<dd>{!! isset($current_model) && isset($current_model->translate($code)->$field) ? $current_model->translate($code)->$field : null !!}</dd> | |
@endif | |
@endforeach | |
</div> | |
@endforeach | |
</div> | |
</div> | |
</div> |
And here the “index” ones
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//From index.blade.stub | |
<table class="table table-striped "> | |
<thead> | |
<tr> | |
[% header_cells %] | |
@include('admin.partials.single_field_translation_index_header') | |
<th></th> | |
</tr> | |
</thead> | |
<tbody> | |
@foreach($[% model_name_plural %] as $[% model_name %]) | |
<tr> | |
[% body_cells %] | |
@include( 'admin.partials.single_field_translation_index_cell', ['current_model' => $[% model_name %], 'field' => 'name'] ) | |
// index.header.cell.blade.php | |
// (add sorting feature to table headers) | |
<th><a href="{!! Request::fullUrlWithQuery ( [ 'o' => '[% field_name %]', 'd' => 'asc' ] ) !!}">{{ _i("[% field_title %]") }}</a></th> | |
// views/admin/partials/single_field_translation_index_header.blade.php | |
@foreach(config( 'app.app_languages' ) as $code => $lang) | |
<th><img src="/images/flags/{{ $code }}.png" alt="{{ strtoupper($code) }}"></th> | |
@endforeach | |
// views/admin/partials/single_field_translation_index_cell.blade.php | |
@foreach(config( 'app.app_languages' ) as $code => $lang) | |
<td> | |
@if($current_model->hasTranslation($code)) | |
{{ $current_model->translate($code)->{$field} }} | |
@endif | |
</td> | |
@endforeach |
That’s all, we can generate fully multilingual CRUD actions with a nice AdminLTE template.
Everything else is just the cherry on top – as an example, I added Select2 js, a couple optional rich text editors in place of textareas, some fancy checkbox alternatives, and so on. Using the “js” and “css” sections of the AdminLTE template, and some config file, it’s easy to add the required assets (and custom partials) conditionally in our generators templates too.
If you’d like to use a tool that saves you time while not getting in the way, Star CrestApps Laravel Code Generator on Github now!