on Tutorials

Laravel 4 tutorial – simple website with backend – Part 2

61 comments
Getting started with Laravel
Tweet about this on TwitterShare on FacebookShare on Google+Share on LinkedInShare on RedditShare on StumbleUpon

One month ago, we started this Laravel 4 tutorial series with the first part in which the basics were explained. This time we’re continuing on that, so go ahead and read the first part if you didn’t already.

There is a part 3 of the series available on Laravel 4 – Validation and frontend

As said earlier, this tutorial is not for beginners, so some knowledge is expected from the friendly reader/developer. All source files are available on GitHub.

Laravel 4 tutorial – Layouts and views

The first thing we need is a simple layout for our backend so we need to create the following file:

app/views/admin/_layouts/defaut.blade.php

<!doctype html>
<html lang="en">
<head>
        <meta charset="utf-8">
        <title>L4 Site</title>

        @include('admin._partials.assets')
</head>
<body>
<div class="container">
        <div class="navbar navbar-inverse navbar-fixed-top">
        <div class="navbar-inner">
                <div class="container">
                        <a class="brand" href="{{ URL::route('admin.pages.index') }}">L4 Site</a>

                        @include('admin._partials.navigation')
                </div>
        </div>
</div>

<hr>

        @yield('main')
</div>
</body>
</html>

This will be our main layout that will load our views through the yield() function.

Laravel 4 Authentication

Every backend should have some kind of authentication and this Laravel 4 tutorial will help you build one.

Laravel support basic HTTP authentication, which is great for quick protection, but we’ll create our own login page.

We’re leveraging the great Sentry package, which I mentioned in the first part of the tutorial. So if you haven’t installed it, do so now. Just add it to your composer.json file and run composer update.

We need to create our authentication controller. I prefer to do this with a controller, while another way to go would be to create the auth routes with closures. It’s just a matter of preference.
We have our layout defined but we also need a view for the login form:

app/views/admin/auth/login.blade.php

@extends('admin._layouts.default')

@section('main')

        <div id="login" class="login">
                {{ Form::open() }}

                        @if ($errors->has('login'))
                                <div class="alert alert-error">{{ $errors->first('login', ':message') }}</div>
                        @endif

                        <div class="control-group">
                                {{ Form::label('email', 'Email') }}
                                <div class="controls">
                                        {{ Form::text('email') }}
                                </div>
                        </div>

                        <div class="control-group">
                                {{ Form::label('password', 'Password') }}
                                <div class="controls">
                                        {{ Form::password('password') }}
                                </div>
                        </div>

                        <div class="form-actions">
                                {{ Form::submit('Login', array('class' => 'btn btn-inverse btn-login')) }}
                        </div>

                {{ Form::close() }}
        </div>

@stop

And the controller:

app/controllers/admin/AuthController.php

<?php namespace App\Controllers\Admin;

use Auth, BaseController, Form, Input, Redirect, Sentry, View;

class AuthController extends BaseController {

        /**
         * Display the login page
         * @return View
         */
        public function getLogin()
        {
                return View::make('admin.auth.login');
        }

        /**
         * Login action
         * @return Redirect
         */
        public function postLogin()
        {
                $credentials = array(
                        'email' => Input::get('email'),
                        'password' => Input::get('password')
                );

                try
                {
                        $user = Sentry::authenticate($credentials, false);

                        if ($user)
                        {
                                return Redirect::route('admin.pages.index');
                        }
                }
                catch(\Exception $e)
                {
                        return Redirect::route('admin.login')->withErrors(array('login' => $e->getMessage()));
                }
        }

        /**
         * Logout action
         * @return Redirect
         */
        public function getLogout()
        {
                Sentry::logout();

                return Redirect::route('admin.login');
        }

}

Ok, now we have a default layout, an authentication controller and a login view. The only thing missing is a route that will connect those 3. I like to create these routes like this:

app/routes.php

Route::get('admin/logout', array('as' => 'admin.logout', 'uses' => 'App\Controllers\Admin\AuthController@getLogout'));
Route::get('admin/login', array('as' => 'admin.login', 'uses' => 'App\Controllers\Admin\AuthController@getLogin'));
Route::post('admin/login', array('as' => 'admin.login.post', 'uses' => 'App\Controllers\Admin\AuthController@postLogin'));

You might have noticed in our auth controller that upon a successful login we redirect the user to the route admin.pages.index. This route doesn’t exist so we’ll create it also, and we also add the entire group of routes for our admin controllers that we still need to create:

app/routes.php

Route::group(array('prefix' => 'admin', 'before' => 'auth.admin'), function()
{
        Route::any('/', 'App\Controllers\Admin\PagesController@index');
        Route::resource('articles', 'App\Controllers\Admin\ArticlesController');
        Route::resource('pages', 'App\Controllers\Admin\PagesController');
});

You can always check out your defined routes with the command php artisan routes.

So these routes need to be protected by our authentication logic. This is done via the ‘before’ filter which is set to auth.admin. This filter needs to be defined in our app/filters.php file.

You already have some filters predefined, but we need to create our own since we’re using the Sentry package. It’s actually really simple, just a few lines of code:

app/filters.php

Route::filter('auth.admin', function()
{
        if ( ! Sentry::check())
        {
                return Redirect::route('admin.login');
        }
});

Now just fire up your server. I like to use the PHP built in server and I just run php artisan serve and the site can be accessed through http://localhost:8000.

What we’re interested in right now is the login page which you can access through http://localhost:8000/admin/login. If you did everything correctly you should see your login view inside the default layout.

It should be styled by Twitter Bootstrap. Just try entering some dummy login data, and you should also see that we have error messages displayed. For now the message is simply the exception message that Sentry throws.

We created a database seed for an admin user in the last part, so we can login with the email admin@admin.com, and the password admin. Try that and you should be redirected to the route admin.pages.index. The route exists but we still need to create the controller and views.

Good job. Have a cup of tea or something, then come back.

Creating our controllers

In Laravel 4 you can route the requests in many different ways. I personally like to use controllers, and perhaps sometimes closures for simple actions.

For our backend panel we need a couple of controllers for now:

app/controllers/admin/ArticlesController.php

As you may remember from earlier, we used resourceful routes for the controllers. This means that Laravel 4 has created routes for all the CRUD actions for our resources. You can read more on this in the docs: http://four.laravel.com/docs/controllers#resource-controllers

This basically means you now have predefined routes that are mapped to specific methods in your controller. Eg. the “Pages” resource is mapped to the controller class “App\Controllers\Admin\PagesController”.

Laravel 4 makes use of the HTTP methods for each of the CRUD action. So for example a GET request to /pages will list all pages by calling the index() method in the PagesController class. For more details just check the docs, but for now let me show you the PagesController:

app/controllers/admin/PagesController.php

<?php namespace App\Controllers\Admin;

use App\Models\Page;
use Input, Redirect, Sentry, Str;

class PagesController extends \BaseController {

        public function index()
        {
                return \View::make('admin.pages.index')->with('pages', Page::all());
        }

        public function show($id)
        {
                return \View::make('admin.pages.show')->with('page', Page::find($id));
        }

        public function create()
        {
                return \View::make('admin.pages.create');
        }

        public function store()
        {
                $page = new Page;
                $page->title = Input::get('title');
                $page->slug = Str::slug(Input::get('title'));
                $page->body = Input::get('body');
                $page->user_id = Sentry::getUser()->id;
                $page->save();

                return Redirect::route('admin.pages.edit', $page->id);
        }

        public function edit($id)
        {
                return \View::make('admin.pages.edit')->with('page', Page::find($id));
        }

        public function update($id)
        {
                $page = Page::find($id);
                $page->title = Input::get('title');
                $page->slug = Str::slug(Input::get('title'));
                $page->body = Input::get('body');
                $page->user_id = Sentry::getUser()->id;
                $page->save();

                return Redirect::route('admin.pages.edit', $page->id);
        }

        public function destroy($id)
        {
                $page = Page::find($id);
                $page->delete();

                return Redirect::route('admin.pages.index');
        }

}

As you can see I already mapped some of the methods to views, and I’m also passing data to the views through the Page model using Eloquent methods. All of this is pretty simple, but this is fine for now.
Our index() method should list the pages from our database. The view looks something like this:

app/views/admin/pages/index.blade.php

@extends('admin._layouts.default')

@section('main')

        <table class="table table-striped">
                <thead>
                        <tr>
                                <th>#</th>
                                <th>Title</th>
                                <th>When</th>
                                <th><i class="icon-cog"></i></th>
                        </tr>
                </thead>
                <tbody>
                        @foreach ($pages as $page)
                                <tr>
                                        <td>{{ $page->id }}</td>
                                        <td><a href="{{ URL::route('admin.pages.show', $page->id) }}">{{ $page->title }}</a></td>
                                        <td>{{ $page->created_at }}</td>
                                        <td>
                                                <a href="{{ URL::route('admin.pages.edit', $page->id) }}" class="btn btn-success btn-mini pull-left">Edit</a>

                                                {{ Form::open(array('route' => array('admin.pages.destroy', $page->id), 'method' => 'delete')) }}
                                                        <button type="submit" href="{{ URL::route('admin.pages.destroy', $page->id) }}" class="btn btn-danger btn-mini">Delete</button>
                                                {{ Form::close() }}
                                        </td>
                                </tr>
                        @endforeach
                </tbody>
        </table>

@stop

You may notice that the delete button is inside a form. The reason for this is that the destroy() method from our controller needs a DELETE request, and this can be done in this way. If the button was a simple link, the request would be sent via the GET method, and we wouldn’t call the destroy() method.

We’re also missing the actual actions that would manipulate our data in the database. We’ll do this first without validation, and later on we’ll setup some validation rules in our models. So for now our store() method should create a new page, and this looks something like this:

app/controllers/admin/PagesController.php

public function store()
        {
                $page = new Page;
                $page->title = Input::get('title');
                $page->slug = Str::slug(Input::get('title'));
                $page->body = Input::get('body');
                $page->user_id = Sentry::getUser()->id;
                $page->save();

                return Redirect::route('admin.pages.edit', $page->id);
        }

But before this method works we also need a view to display the form for creating new pages. This view we’ll be shown when the method create() is called:

app/views/admin/pages/create.blade.php

@extends('admin._layouts.default')

@section('main')

        <h2>Create new page</h2>

        {{ Form::open(array('route' => 'admin.pages.store')) }}

                <div class="control-group">
                        {{ Form::label('title', 'Title') }}
                        <div class="controls">
                                {{ Form::text('title') }}
                        </div>
                </div>

                <div class="control-group">
                        {{ Form::label('body', 'Content') }}
                        <div class="controls">
                                {{ Form::textarea('body') }}
                        </div>
                </div>

                <div class="form-actions">
                        {{ Form::submit('Save', array('class' => 'btn btn-success btn-save btn-large')) }}
                        <a href="{{ URL::route('admin.pages.index') }}" class="btn btn-large">Cancel</a>
                </div>

        {{ Form::close() }}

@stop

The form will submit to the route admin.pages.store, and we don’t need to define a HTTP method, since POST is the default method, and POST is linked to the store() method mentioned above.

After the page is stored there’s a redirect to the screen where we can edit the entry. The method is defined but we’re still missing the view which is pretty similar to the create() view:

app/views/admin/pages/edit.blade.php

@extends('admin._layouts.default')

@section('main')

        <h2>Edit page</h2>

        {{ Form::model($page, array('method' => 'put', 'route' => array('admin.pages.update', $page->id))) }}

                <div class="control-group">
                        {{ Form::label('title', 'Title') }}
                        <div class="controls">
                                {{ Form::text('title') }}
                        </div>
                </div>

                <div class="control-group">
                        {{ Form::label('body', 'Content') }}
                        <div class="controls">
                                {{ Form::textarea('body') }}
                        </div>
                </div>

                <div class="form-actions">
                        {{ Form::submit('Save', array('class' => 'btn btn-success btn-save btn-large')) }}
                        <a href="{{ URL::route('admin.pages.index') }}" class="btn btn-large">Cancel</a>
                </div>

        {{ Form::close() }}

@stop

In this view we’re leveraging the Form::model() method, which simply fills the input fields inside the form with data from the Page model that is passed to the view by the controller. It also handles submitted data if the validation fails, but for now there’s no validation so more on this subject later.

The method that stores the changes to the page is update(). This method is called when the controller receives a PUT request, and this is also handled via the Form::model() helper. The actual method is almost the same as the create method:

app/controllers/admin/PagesController.php

public function update($id)
        {
                $validation = new PageValidator;

                if ($validation->passes())
                {
                        $page = Page::find($id);
                        $page->title = Input::get('title');
                        $page->slug = Str::slug(Input::get('title'));
                        $page->body = Input::get('body');
                        $page->user_id = Sentry::getUser()->id;
                        $page->save();

                        Notification::success('The page was saved.');

                        return Redirect::route('admin.pages.edit', $page->id);
                }

                return Redirect::back()->withInput()->withErrors($validation->errors);
        }

The last method needed is the destroy method:

app/controllers/admin/PagesController.php

public function update($id)
        {
                $page = Page::find($id);
                $page->title = Input::get('title');
                $page->slug = Str::slug(Input::get('title'));
                $page->body = Input::get('body');
                $page->user_id = Sentry::getUser()->id;
                $page->save();

                return Redirect::route('admin.pages.edit', $page->id);
        }

There’s also the show() method which could be useful for previewing the content:

public function show($id)
        {
                return \View::make('admin.pages.show')->with('page', Page::find($id));
        }

We also have another resource for our articles, but at this point we’ll just copy almost everything to this resource.

I know this might seem like bad practice, but eventually we’ll expand each resource. Eg. the pages resource well be able to have parent pages, and the articles will be categorised.

Custom commands

You may remember our app:refresh command from part 1. Well, now we can play with our pages and articles as much as we want, and if we want to go back to the starting point, simply run the command. This is extremely useful if for any reason the data in the DB becomes corrupted during development. Awesome…

Notifications and modals

When we perform those CRUD actions on our data, we don’t have any feedback when something happens, and I find this bad. You should always display some feedback, so we’re going to implement a simple notification system.

We could write a simple system ourselves, but it would be too much for this tutorial, so we’re use the force of Laravel 4 community again. I’m going to use this notification package: https://github.com/edvinaskrucas/notification

To install the package just add it to your composer.json file:

"edvinaskrucas/notification": "1.*"

Run composer update, and add the service provider and facade to your app configuration:

'Krucas\Notification\NotificationServiceProvider'

and

'Notification' => 'Krucas\Notification\Facades\Notification'

So basically every time we perform an action and redirect back, we need to add a notification. To keep thing simple for now we’re only gonna show success messages, since we don’t even have any validation in place …
Also keep in mind that if you want to use the Notification class, you need to either reference it with a backslash, or the way I do it add a use statement on the top:

...
use Notification;
...

So eg. in out update() method in the pages controller, before redirecting, we add the notification, and our method should look like this:

app/controllers/admin/PagesController.php

public function update($id)
        {
                $validation = new PageValidator;

                if ($validation->passes())
                {
                        $page = Page::find($id);
                        $page->title = Input::get('title');
                        $page->slug = Str::slug(Input::get('title'));
                        $page->body = Input::get('body');
                        $page->user_id = Sentry::getUser()->id;
                        $page->save();

                        Notification::success('The page was saved.');

                        return Redirect::route('admin.pages.edit', $page->id);
                }

                return Redirect::back()->withInput()->withErrors($validation->errors);
        }

To display the notification, we need to place the code inside our view. Since we redirect to the edit view after saving the page, I’ll just put the code right beneath the h2 tag:

app/views/admin/pages/edit.blade.php

{{ Notification::showAll() }}

@if ($errors->any())
        <div class="alert alert-error">
                {{ implode('<br>', $errors->all()) }}
        </div>
@endif

And this is all you need. Just add notification in all the places you want. You can check out my code at the repo: https://github.com/bstrahija/l4-site-tutorial

Another thing I like to add to my apps is confirmation when deleting resources. So to demonstrate this we’ll add a simple JavaScript confirmation dialog on our delete buttons in the index view.

So we had a form wrapped around the button because of the required HTTP delete method. One way to do this is to intercept the submit action of the form, and the way I like to do this is by adding a HTML5 data parameter to the form:

app/views/admin/pages/index.blade.php

{{ Form::open(array('route' => array('admin.pages.destroy', $page->id), 'method' => 'delete', 'data-confirm' => 'Are you sure?')) }}
      <button type="submit" href="{{ URL::route('admin.pages.destroy', $page->id) }}" class="btn btn-danger btn-mini">Delete</button>
{{ Form::close() }}

As you can see the parameter data-confirm holds the message that we’ll show to the user. To actually display the message we need a simple JQuery script. So we’ll create the following file and include it in our main layout:

public/assets/js/script.js

$(function() {

        // Confirm deleting resources
        $("form[data-confirm]").submit(function() {
                if ( ! confirm($(this).attr("data-confirm"))) {
                        return false;
                }
        });

});

So this script find all form tags with a data-confirm attribute, takes the value from the attribute and display a confirmation dialog. If the user clicks Ok, the form is submitted.

What’s next?

This concludes the second part of Laravel 4 tutorial where we covered authentication and some basic CRUD actions, so by now you should have a foundation for your web app.

In the next part we’ll get busy with some validation, and we’ll also jump into the front end part of the web, and routing requests to our pages and articles.

There is a part 3 of the series available on Laravel 4 – Validation and frontend

Tweet about this on TwitterShare on FacebookShare on Google+Share on LinkedInShare on RedditShare on StumbleUpon



61 Responses to “Laravel 4 tutorial – simple website with backend – Part 2”