Skip to content
Boris Strahija edited this page May 24, 2013 · 1 revision

Simple website with backend in Laravel 4 - Part 2

Layout 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>

	<link href="//netdna.bootstrapcdn.com/twitter-bootstrap/2.3.1/css/bootstrap-combined.min.css" rel="stylesheet">
	<link href="//netdna.bootstrapcdn.com/twitter-bootstrap/2.3.1/css/bootstrap-responsive.min.css" rel="stylesheet">
	<link href="//netdna.bootstrapcdn.com/font-awesome/3.0.2/css/font-awesome.css" rel="stylesheet">
	<link href="{{ URL::asset('assets/css/main.css') }}" rel="stylesheet">

	<script src="//code.jquery.com/jquery-1.9.1.min.js"></script>
	<script src="//netdna.bootstrapcdn.com/twitter-bootstrap/2.3.1/js/bootstrap.min.js"></script>
</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>

				@if (Sentry::check())
					<ul class="nav">
						<li><a href="{{ URL::route('admin.pages.index') }}"><i class="icon-book"></i> Pages</a></li>
						<li><a href="{{ URL::route('admin.pages.index') }}"><i class="icon-edit"></i> Articles</a></li>
						<li><a href="{{ URL::route('admin.logout') }}"><i class="icon-lock"></i> Logout</a></li>
					</ul>
				@endif
			</div>
		</div>
	</div>

	<hr>

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

Just a simple layout that will load our views through the yield() function.

Authentication

Every backend should have some kind of authentication. 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">
		@if ($errors->has('login'))
			<div class="alert alert-error">{{ $errors->first('login', ':message') }}</div>
		@endif

		{{ Form::open('auth.login') }}

			<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-primary')) }}
			</div>

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

@stop

app/controllers/admin/AuthController.php

<?php namespace App\Controllers\Admin;

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

	class AuthController extends BaseController {

		public function getLogin()
		{
			return View::make('admin.auth.login');
		}

		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()));
			}
		}

		public function getLogout()
		{
			Sentry::logout();

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

	}

Ok, now we have a default layout, an auth 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.

Creating our controllers

In L4 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

app/controllers/admin/PagesController.php

As you may remember from earlier, we used resourceful routes for the controllers. This means that L4 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 basicly 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". L4 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()
	{
		return 'Store';
	}

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

	public function update($id)
	{
		return 'Update';
	}

	public function destroy($id)
	{
		return 'Destroy';
	}

}

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')

	<h1>
		Pages <a href="{{ URL::route('admin.pages.create') }}" class="btn btn-success"><i class="icon-plus-sign"></i> Add new page</a>
	</h1>

	<hr>

	<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</butfon>
						{{ 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 were 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)
{
	$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);
}

The last method needed is the destroy method:

app/controllers/admin/PagesController.php

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

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

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

app/controllers/admin/PagesController.php

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 migh 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 categorized.

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.

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 not good. You should always display some feedback, so we're gonna implement a simple notification system. We could write a simple system ourselves, but it would be too much for this tutorial, so we're gonna leverage the community again. I'm gonna use this notification package: https://github.com/edvinaskrucas/notification

To install the package just add it to your compose.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'

For more info on using this package check out the docs on https://github.com/edvinaskrucas/notification

So basicly everytime 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 ;)

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)
{
	$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);
}

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

...
<h2>Edit page</h2>
{{ Notification::showAll() }}
...

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</butfon>
{{ Form::close() }}
...

As you can see the parameter data-confirm holds the message that we're gonna display 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:

app/public/assets/js/script.js

$(function() {
	$("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?

We covered authentication and some basic CRUD actions in this part of the tutorial, 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.