In Part 1 of this series I covered developing a simple application that exercised the 3 essential components of the Laravel eco-system, that being Models, Views and Controllers. In the process I skipped over some finer details so that the big picture of a working app could be shown. In this article I will delve deeper into each section, starting with "Views".
In Laravel, a "View" is essentially where the output of your application is generated. A large commercial application might contain 100's of views or more correctly, 100's of Laravel Blade templates that render the "View". The blade template system is a powerful, elegant method of rendering your content. It can be enhanced, 'dumbed down' or modified as needed but to develop well maintainable applications its best to ensure some basic points are followed when crafting views:
- No Business Logic – Leave that in the controller.
- Avoid invoking Models inside the template – again, invoke these in the controller.
- Limit PHP tags where possible – use Laravel's template implementation where possible.
- Use master templates to form the outer "shell" of your application – keeps views small and easier to maintain.
Lets look at a Blade Template that renders a list of users and see where these 4 points fit. Then we will re-factor the code to just have the pure template at work:
@extends('master-users') @section('title','Add User') @section('content') <?php $AID = Session::get('AID'); $UID = Session::get('UID'); $Users = new AppUsers(); $rows = $Users->getByID($UID); $role = ''; foreach($rows as $ur) { $role = $ur->usr_type; } $Utilities = new AppUtilities(); ?> <div class='container'> <div class="row"> <div class="col-lg-12"><h3 class="page-header">Existing Users</h3></div> </div> @if(Session::has('flash_error')) <div class="alert alert-danger">{{ Session::get('flash_error') }}</div> @endif @if(Session::has('flash_message')) <div class="alert alert-success">{{ Session::get('flash_message') }}</div> @endif <div class="row"> <div class='col-lg-12'> <table class="table table-hover"> <thead> <tr> <th>E-Mail</th> <th>First Name</th> <th>Last Name</th> <th>Status</th> <th>Role</th> <th>Created</th> <th>Last Accessed</th> <th>Actions</th> </tr> </thead> <tbody> @foreach($acusers as $user) <tr> <!-- onclick='edituser({{ $user->id }})'--> <td>{{ $user->usr_email }}</td> <td>{{ $user->usr_first_name }}</td> <td>{{ $user->usr_last_name }}</td> <td><?php $Utilities->DisplayUserStatus( $user->usr_status ); ?></td> <td>{{ $user->usr_type }}</td> <td>{{ $user->usr_join_date }}</td> <td>{{ $user->usr_last_access_date }}</td> <td> <a href='/users/edit/{{ $user->id }}'><i class='fa fa-pencil-square-o'></i> Edit</a> <a class="deleteuser" data-toggle="modal" data-id="{{ $user->id }}" href="#confirmmodal"><i class='fa fa-trash'> Delete</i></a> </td> </tr> @endforeach </tbody> </table> </div> </div> <?php if($role=='ULAD') { ?> <div class="row"> <div class="col-lg-12"><h4 class="page-header">Add New User</h4></div> <div class="col-lg-12 col-md-12"> <form class='form-horizontal' id='newuser' name='newuser' method='post'> <?php $Users->InjectForm('U'); ?> {!! Form::token() !!} <div class="form-group"> <label class="control-label col-xs-2"> </label> <div class="col-xs-4"> <button type="button" id="btnsave" class="btn btn-primary">Save</button> <button type="button" id="btncancel" class="btn btn-warning">Cancel</button> </div> </div> </form> </div> </div> <?php } ?> </div> <script> .... </script>
The view is rendered by a controller with the line:
return view('Users.Admin.adduser',['acusers'=>$rows]);
This file has all four items mentioned above, it includes a master template, (master-users.blade.php in the resources/views directory), it has PHP code invoking a model, and a mix of PHP and template tags. Its messy and will cause us issues down the track. Lets first look at the Laravel specific parts, these are the directives that start with "@" and the variable output which uses {{ and }} to surround a variable for output. Ive already refactored the main loop to use @foreach and @endforeach.
Avoid getting into the habit of using PHP tags, like this:
<?php echo $variable; ?>
Instead use the blade templating tags:
{{ $variable }}
Avoid using <?php if(x==true) { ?>
Instead, use the @if @elsif and @endif constructions, so our code:
<?php if($role=='ULAD') { ?>
becomes:
@if($role=='ULAD')
and it ends with a @endif statement, so we get:
<?php } ?> ... Becomes .... @endif
At the beginning of the file we have a block of PHP code which can be refactored, essentially it gets the user's "role" in order to enable the block of code at the bottom of the form.
<?php $AID = Session::get('AID'); $UID = Session::get('UID'); $Users = new AppUsers(); $rows = $Users->getByID($UID); $role = ''; foreach($rows as $ur) { $role = $ur->usr_type; } $Utilities = new AppUtilities(); ?>
This can be moved into the controller as all we need to pass into the view is the $role variable. So by moving that code into the controller the call to the view becomes:
return view('Users.Admin.adduser',['acusers'=>$rows, 'role'=>$role]);
Our new code now looks neater but there is still a lot more to do. Lets review what we now have:
@extends('master-users') @section('title','Add User') @section('content') <?php $Utilities = new AppUtilities(); ?> <div class='container'> <div class="row"> <div class="col-lg-12"><h3 class="page-header">Existing Users</h3></div> </div> @if(Session::has('flash_error')) <div class="alert alert-danger">{{ Session::get('flash_error') }}</div> @endif @if(Session::has('flash_message')) <div class="alert alert-success">{{ Session::get('flash_message') }}</div> @endif <div class="row"> <div class='col-lg-12'> <table class="table table-hover"> <thead> <tr> <th>E-Mail</th> <th>First Name</th> <th>Last Name</th> <th>Status</th> <th>Role</th> <th>Created</th> <th>Last Accessed</th> <th>Actions</th> </tr> </thead> <tbody> @foreach($acusers as $user) <tr> <!-- onclick='edituser({{ $user->id }})'--> <td>{{ $user->usr_email }}</td> <td>{{ $user->usr_first_name }}</td> <td>{{ $user->usr_last_name }}</td> <td><?php $Utilities->DisplayUserStatus( $user->usr_status ); ?></td> <td>{{ $user->usr_type }}</td> <td>{{ $user->usr_join_date }}</td> <td>{{ $user->usr_last_access_date }}</td> <td> <a href='/users/edit/{{ $user->id }}'><i class='fa fa-pencil-square-o'></i> Edit</a> <a class="deleteuser" data-toggle="modal" data-id="{{ $user->id }}" href="#confirmmodal"><i class='fa fa-trash'> Delete</i></a> </td> </tr> @endforeach </tbody> </table> </div> </div> @if($role=='ULAD') <div class="row"> <div class="col-lg-12"><h4 class="page-header">Add New User</h4></div> <div class="col-lg-12 col-md-12"> <form class='form-horizontal' id='newuser' name='newuser' method='post'> <?php $Users->InjectForm('U'); ?> {!! Form::token() !!} <div class="form-group"> <label class="control-label col-xs-2"> </label> <div class="col-xs-4"> <button type="button" id="btnsave" class="btn btn-primary">Save</button> <button type="button" id="btncancel" class="btn btn-warning">Cancel</button> </div> </div> </form> </div> </div> @endif </div> <script> .... </script>
We still have a few issues with this code, it includes the following lines:
<?php $Utilities = new AppUtilities(); ?> ...... <?php $Utilities->DisplayUserStatus( $user->usr_status ); ?>
and the line:
<?php $Users->InjectForm('U'); ?>
While its great to have a model that can render the status of the user consistently through the application, we don't want to invoke a model inside our view if we can avoid it. Since we deleted the $Users object we need to remove or replace the code injecting the form. The code in the view that renders the status can be replaced with a simple key-value array returned from the controller, so our display line becomes:
<td>{{ $status[ $user->usr_status ] }}</td>
We need to make sure "$status" is passed in the view render call from the controller as such:
return view('Users.Admin.adduser',['acusers'=>$rows, 'role'=>$role, 'status'=>$status]);
We are now on the final leg of our refactor, and it's not hard to see what is next. The last part of the form contains the decision code to include a user submitted form to add a new user, again the HTML code for the form is in a support class as it is used in a few places. While we cleaned it up a bit using the @if tag, we deduce from the code that the view actually does two things, lists users and allows a User Administrator to add a new user. Ideally the view should do one primary thing, so splitting the user add forms to a second view is a cleaner and more elegant option.
I'm going to copy this PHP blade file to listusers.blade.php and change the route to invoke two controller methods, one for adding a new user and one for listing existing users, so we get two files: adduser.blade.php and listusers.blade.php
Our new "List Users" view (listusers.blade.php) is much more elegant and since only the administration user can actually invoke it, I can change the view to simply include a button at the bottom of the list to give the User Administrator the option to add another user. So the code now looks like:
@extends('master-users') @section('title','List Users') @section('content') <div class='container'> <div class="row"> <div class="col-lg-12"><h3 class="page-header">Existing Users</h3></div> </div> @if(Session::has('flash_error')) <div class="alert alert-danger">{{ Session::get('flash_error') }}</div> @endif @if(Session::has('flash_message')) <div class="alert alert-success">{{ Session::get('flash_message') }}</div> @endif <div class="row"> <div class='col-lg-12'> <table class="table table-hover"> <thead> <tr> <th>E-Mail</th> <th>First Name</th> <th>Last Name</th> <th>Status</th> <th>Role</th> <th>Created</th> <th>Last Accessed</th> <th>Actions</th> </tr> </thead> <tbody> @foreach($acusers as $user) <tr> <!-- onclick='edituser({{ $user->id }})'--> <td>{{ $user->usr_email }}</td> <td>{{ $user->usr_first_name }}</td> <td>{{ $user->usr_last_name }}</td> <td>{{ $status[ $user->usr_status] }}</td> <td>{{ $user->usr_type }}</td> <td>{{ $user->usr_join_date }}</td> <td>{{ $user->usr_last_access_date }}</td> <td> <a href='/users/edit/{{ $user->id }}'><i class='fa fa-pencil-square-o'></i> Edit</a> <a class="deleteuser" data-toggle="modal" data-id="{{ $user->id }}" href="#confirmmodal"><i class='fa fa-trash'> Delete</i></a> </td> </tr> @endforeach </tbody> </table> </div> </div> <div class="row"> <div class='col-lg-12'> <button type="button" id='btnaddnew' class="btn btn-success">Add New User</button> </div> </div> </div> <script> ..... </script>
What is not shown is the Javascript for the button and the action links, the JQuery code for the "Add New User" button will look like:
$('#btnaddnew').click(function() { var url = '/users/admin/add/'; window.location.href = url; });
We still need to add a route (the routes are in app/Http/routes.php) so that pressing the "Add New User" button calls the correct method in our UserController.php to show the Add User Page:
Route::get('/users/admin/add', 'UserController@ShowAddUserPage');
We also need to change the existing route to point to our renamed method that better reflects the views real purpose:
Route::get('/users/admin/list', 'UserController@ShowListUserPage');
What's Next?
The list users blade can be extended to support pagination as the controller uses the paginate() method to get the rows for display. Here is the code sample from the UserController.php file.
public function ShowListUserPage() { if($this->isAdminUser()==true) { $Utilities = new AppUtilities(); $status = $Utilities->getStatusArray(); # $status will look something like -> $status = array('A'=>'Active', 'C'=>'Closed','S'=>'Suspended','X'=>'Deleted'); $rows = DB::table('users')->paginate($this->pagination_count); return view('Users.Admin.listusers',['acusers'=>$rows,'role'=>$role,'status'=>$status]); } return view('/securityviolation'); }
In the view, we can include the code to do the pagination using:
</table> {!! $users->render() !!} </div>
The adduser.blade.php file now has some changes, below is the edited version without the Javascript shown:
@extends('master-users') @section('title','Add User') @section('content') <div class='container'> <div class="row"> <div class="col-lg-12"><h4 class="page-header">Add New User</h4></div> <div class="col-lg-12 col-md-12"> <form class='form-horizontal' id='newuser' name='newuser' method='post'> {{ $Users->InjectForm('U') }} {!! Form::token() !!} <div class="form-group"> <label class="control-label col-xs-2"> </label> <div class="col-xs-4"> <button type="button" id="btnsave" class="btn btn-primary">Save</button> <button type="button" id="btncancel" class="btn btn-warning">Cancel</button> </div> </div> </form> </div> </div> </div> <script> .... </script>
What we can see is there is no Object for "$Users" created in the file, its returned from the controller as such:
public function ShowAddUserPage() { if($this->isAdminUser()==true) { $Users = new AppUsers(); return view('Users.Admin.adduser',['Users'=>$Users]); } return view('/securityviolation'); }
Wrapping Up
We have now changed our original view into two files and logically partitioned the activity of each, we also removed calls to models and PHP tags and implemented the blade template tags. We also added functionality to our controller and we can now remove un-needed parameters like "role" from the view call in our controller as $role is not used in either view now. The last point I want to touch on is to conditional blocks of code in our listusers blade. These are:
@if(Session::has('flash_error')) <div class="alert alert-danger">{{ Session::get('flash_error') }}</div> @endif @if(Session::has('flash_message')) <div class="alert alert-success">{{ Session::get('flash_message') }}</div> @endif
The purpose of this code is to display a status message from the controller, either a success or fail message depending on the action of the code. So when we successfully add a new user using the add form, we should redirect back to the listusers.blade.php View and by using:
Session::flash('flash_message','User successfully added!'); OR Session::flash('flash_error','ERROR - No Email so user cannot be updated!');
We can inform the user of the result using the template tags to render the appropriate div element block based on the temporary Session status.
Tag List
Below is a list of tags valid at time of writing, no doubt newer versions will include more tags and you can add you own.
- {{ $var }} – Echo content
- {{ $var or 'default' }} – Echo content with a default value
- {{{ $var }}} – Echo escaped content
- {{– Comment –}} – A Blade comment
- @extends('layout') – Extends a template with a layout
- @if(condition) – Starts an if block
- @else – Starts an else block
- @elseif(condition) – Start a elseif block
- @endif – Ends a if block
- @foreach($list as $key => $val) – Starts a foreach block
- @endforeach – Ends a foreach block
- @for($i = 0; $i < 10; $i++) – Starts a for block
- @endfor – Ends a for block
- @while(condition) – Starts a while block
- @endwhile – Ends a while block
- @unless(condition) – Starts an unless block
- @endunless – Ends an unless block
- @include(file) – Includes another template
- @include(file, ['var' => $val,…]) – Includes a template, passing new variables.
- @each('file',$list,'item') – Renders a template on a collection
- @each('file',$list,'item','empty') – Renders a template on a collection or a different template if collection is empty.
- @yield('section') – Yields content of a section.
- @show – Ends section and yields its content
- @lang('message') – Outputs message from translation table
- @choice('message', $count) – Outputs message with language pluralization
- @section('name') – Starts a section
- @stop – Ends section
- @endsection – Ends section
- @append – Ends section and appends it to existing of section of same name
- @overwrite – Ends section, overwriting previous section of same name
Models
We will cover Models in Part 3 which wil be coming soon.
References
- Blade Templates – https://laravel.com/docs/5.2/blade
- Developing application with Laravel 5 – Part 1 – https://www.conetix.com.au/blog/developing-application-laravel-5