Saturday, January 12, 2013

Build Your First Admin Bundle for Laravel

Build Your First Admin Bundle for Laravel:
It's hard to deny the fact that the PHP community is excited for Laravel 4. Among other things, the framework leverages the power of Composer, which means it's able to utilize any package or script from Packagist.
In the meantime, Laravel offers "Bundles", which allow us to modularize code for use in future projects. The bundle directory is full of excellent scripts and packages that you can use in your applications. In this lesson, I’ll show you how to build one from scratch!


Wait, What's a Bundle?

Bundles offer an easy way to group related code. If you’re familiar with CodeIgniter, bundles are quite similar to "Sparks". This is apparent when you take a look at the folder structure.
Folder Structure
Creating a bundle is fairly simple. To illustrate the process, we’ll build an admin panel boilerplate that we can use within future projects. Firstly, we need to create an 'admin' directory within our 'bundles' folder. Try to replicate the folder structure from the image above.
Before we begin adding anything to our bundle, we need to first register it with Laravel. This is done in your application's bundles.php file. Once you open this file, you should see an array being returned; we simply need to add our bundle and define a handle. This will become the URI in which we access our admin panel.
'admin' => array('handles' => 'admin')
Here, I've named mine, “admin,” but feel free to call yours whatever you wish.
Once we've got that setup, we need to create a start.php file. Here, we're going to set up a few things, such as our namespaces. If you're not bothered by this, then you don't actually need a start file for your bundle to work, as expected.
Laravel's autoloader class allows us to do a couple of things: map our base controller, and autoload namespaces.
Autoloader::map(array(
    'Admin_Base_Controller' => Bundle::path('admin').'controllers/base.php',
));
Autoloader::namespaces(array(
    'Admin\Models' => Bundle::path('admin').'models',
    'Admin\Libraries' => Bundle::path('admin').'libraries',
));
Namespacing will ensure that we don't conflict with any other models or libraries already included in our application. You'll notice that we haven't opted to not namespace our controllers to make things a little easier.

Publishing Assets

For the admin panel, we'll take advantage of Twitter's Bootstrap, so go grab a copy. We can pop this into a public folder inside our bundle in order to publish to our application later on.
When you're ready to publish them, just run the following command through artisan.
php artisan bundle:publish admin
This will copy the folder structure and files to the bundles directory in our public folder, within the root of the Laravel installation. We can then use this in our bundle's base controller.

Setting up the Base Controller

It's always a smart idea to setup a base controller, and extend from there. Here, we can setup restful controllers, define the layout, and include any assets. We just need to call this file, base.php, and pop it into our controller’s directory.
Firstly, let's get some housekeeping out of the way. We'll of course want to use Laravel's restful controllers.
public $restful = true;
And we'll specify a layout that we'll create shortly. If you're not used to controller layouts, then you're in for a treat.
public $layout = 'admin::layouts.main';
The bundle name, followed by two colons, is a paradigm in Laravel we'll be seeing more of in the future, so keep an eye out.
When handling assets within our bundle, we can do things as expected and specify the path from the root of the public folder. Thankfully, Laravel is there to make our lives easier. In our construct, we need to specify the bundle, before adding to our asset containers.
Asset::container('header')->bundle('admin');
Asset::container('footer')->bundle('admin');
If you're unfamiliar with asset containers, don't worry; they're merely sections of a page where you want to house your assets. Here, we'll be including stylesheets in the header, and scripts in the footer.
Now, with that out of the way, we can include our bootstrap styles and scripts easily. Our completed base controller should look similar to:
class Admin_Base_Controller extends Controller {
    public $restful = true;
    public $layout = 'admin::layouts.main';
    public function __construct(){
        parent::__construct();
        Asset::container('header')->bundle('admin');
        Asset::container('header')->add('bootstrap', 'css/bootstrap.min.css');
        Asset::container('footer')->bundle('admin');
        Asset::container('footer')->add('jquery', 'http://code.jquery.com/jquery-latest.min.js');
        Asset::container('footer')->add('bootstrapjs', 'js/bootstrap.min.js');
    }
    /**
     * Catch-all method for requests that can't be matched.
     *
     * @param  string    $method
     * @param  array     $parameters
     * @return Response
     */
    public function __call($method, $parameters){
        return Response::error('404');
    }
}
We've also brought across the catch-all request from the application's base controller to return a 404 response, should a page not be found.
Before we do anything else, let's make the file for that layout, views/layout/main.blade.php, so we don't encounter any errors later on.

Securing the Bundle

As we're building an admin panel, we're going to want to keep people out. Thankfully, we can use Laravel's built in Auth class to accomplish this with ease..
First, we need to create our table; I'm going to be using 'admins' as my table name, but you can change it, if you wish. Artisan will generate a migration, and pop it into our bundle's migrations directory. Just run the following in the command line.
php artisan migrate:make admin::create_admins_table

Building the Schema

If you're unfamiliar with the schema builder, I recommend that you take a glance at the documentation. We're going to include a few columns:
  • id – This will auto-increment and become our primary key
  • name
  • username
  • password
  • email
  • role – We won't be taking advantage of this today, but it will allow you to extend the bundle later on
We'll also include the default timestamps, in order to follow best practices.
/**
 * Make changes to the database.
 *
 * @return void
 */
public function up()
{
    Schema::create('admins', function($table)
    {
        $table->increments('id');
        $table->string('name', 200);
        $table->string('username', 32)->unique();
        $table->string('password', 64);
        $table->string('email', 320)->unique();
        $table->string('role', 32);
        $table->timestamps();
    });
}
/**
 * Revert the changes to the database.
 *
 * @return void
 */
public function down()
{
    Schema::drop('admins');
}
Now that we've got our database structure in place, we need to create an associated model for the table. This process is essentially identical to how we might accomplish this in our main application. We create the file and model, based on the singular form of our table name – but we do need to ensure that we namespace correctly.
namespace Admin\Models;
use \Laravel\Database\Eloquent\Model as Eloquent;
class Admin extends Eloquent {
}
Above, we've ensured that we're using the namespace that we defined in start.php. Also, so we can reference Eloquent correctly, we create an alias.

Extending Auth

To keep our bundle entirely self contained, we’ll need to extend auth. This will allow us to define a table just to login to our admin panel, and not interfere with the main application.
Before we create our custom driver, we'll create a configuration file, where you can choose if you'd like to use the username or email columns from the database table.
return array(
    'username' => 'username',
    'password' => 'password',
);
If you want to alter the columns that we'll be using, simply adjust the values here.
We next need to create the driver. Let's call it, “AdminAuth,” and include it in our libraries folder. Since we're extending Auth, we only need to overwrite a couple of methods to get everything working, as we intended.
namespace Admin\Libraries;
use Admin\Models\Admin as Admin, Laravel\Auth\Drivers\Eloquent as Eloquent, Laravel\Hash, Laravel\Config;
class AdminAuth extends Eloquent {
/**
 * Get the current user of the application.
 *
 * If the user is a guest, null should be returned.
 *
 * @param  int|object  $token
 * @return mixed|null
 */
public function retrieve($token)
{
    // We return an object here either if the passed token is an integer (ID)
    // or if we are passed a model object of the correct type
    if (filter_var($token, FILTER_VALIDATE_INT) !== false)
    {
        return $this->model()->find($token);
    }
    else if (get_class($token) == new Admin)
    {
        return $token;
    }
}
/**
 * Attempt to log a user into the application.
 *
 * @param  array $arguments
 * @return void
 */
public function attempt($arguments = array())
{
    $user = $this->model()->where(function($query) use($arguments)
    {
        $username = Config::get('admin::auth.username');
        $query->where($username, '=', $arguments['username']);
        foreach(array_except($arguments, array('username', 'password', 'remember')) as $column => $val)
        {
            $query->where($column, '=', $val);
        }
    })->first();
    // If the credentials match what is in the database, we will just
    // log the user into the application and remember them if asked.
    $password = $arguments['password'];
    $password_field = Config::get('admin::auth.password', 'password');
    if ( ! is_null($user) and Hash::check($password, $user->{$password_field}))
    {
        return $this->login($user->get_key(), array_get($arguments, 'remember'));
    }
    return false;
}
protected function model(){
    return new Admin;
}
}
Now that we've created the driver, we need to let Laravel know. We can use Auth's extend method to do this in our start.php file.
Auth::extend('adminauth', function() {
    return new Admin\Libraries\AdminAuth;
});
One final thing that we need to do is configure Auth to use this at runtime. We can do this in our base controller's constructor with the following.
Config::set('auth.driver', 'adminauth');

Routes & Controllers

Before we can route to anything, we need to create a controller. Let's create our dashboard controller, which is what we'll see after logging in.
As we'll want this to show up at the root of our bundle (i.e. the handle we defined earlier), we'll need to call this home.php. Laravel uses the 'home' keyword to establish what you want to show up at the root of your application or bundle.
Extend your base controller, and create an index view. For now, simply return 'Hello World' so we can ensure that everything is working okay.
class Admin_Home_Controller extends Admin_Base_Controller {
    public function get_index(){
        return 'Hello World';
    }
}
Now that our controller is setup, we can route to it. Create a routes.php within your bundle, if you haven't already. Similar to our main application, each bundle can have its own routes file that works identically.
Route::controller(array(
    'admin::home',
));
Here, I've registered the home controller, which Laravel will automatically assign to /. Later , we'll add our login controller to the array.
If you head to /admin (or whatever handle you defined earlier) in your browser, then you should see 'Hello World'.

Building the Login Form

Let’s create the login controller, however, rather than extending the base controller, we’ll instead extend Laravel's main controller. The reason behind this decision will become apparent shortly.
Because we're not extending, we need to set a couple of things up before beginning – namely restful layouts, the correct auth driver, and our assets.
class Admin_Login_Controller extends Controller {
    public $restful = true;
    public function __construct(){
        parent::__construct();
        Config::set('auth.driver', 'adminauth');
        Asset::container('header')->bundle('admin');
        Asset::container('header')->add('bootstrap', 'css/bootstrap.min.css');
    }
}
Let's also create our view. We're going to be using Blade – Laravel's templating engine – to speed things up a bit. Within your bundles views directory, create a 'login' directory and an 'index.blade.php' file within it.
We'll pop in a standard HTML page structure and echo the assets.
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <title>Login</title>
    {{Asset::container('header')->styles()}}
    <!--[if lt IE 9]>
    <script src="http://html5shim.googlecode.com/svn/trunk/html5.js"></script>
    <![endif]-->
</head>
<body>
</body>
</html>
Now, let's make sure that the view is being created in the controller. As we're using restful controllers, we can take advantage of the 'get' verb in our method.
public function get_index(){
    return View::make('admin::login.index');
}
Awesome! We're now good to start building our form, which we can create with the Form class.
{{Form::open()}}
{{Form::label('username', 'Username')}}
{{Form::text('username')}}
{{Form::label('password', 'Password')}}
{{Form::password('password')}}
{{Form::submit('Login', array('class' => 'btn btn-success'))}}
{{Form::token()}}
{{Form::close()}}
Login Form
Above, we created a form that will post to itself (exactly what we want), along with various form elements and labels to go with it. The next step is to process the form.
As we're posting the form to itself and using restful controllers, we just need to create the post_index method and use this to process our login. If you've never used Auth before, then go and have a peek at the documentation before moving on.
public function post_index(){
    $creds = array(
        'username' => Input::get('username'),
        'password' => Input::get('password'),
    );
    if (Auth::attempt($creds)) {
        return Redirect::to(URL::to_action('admin::home@index'));
    } else {
        return Redirect::back()->with('error', true);
    }
}
If the credentials are correct, the user will be redirected to the dashboard. Otherwise, they'll be redirected back with an error that we can check for in the login view. As this is just session data, and not validation errors, we only need to implement a simple check.
@if(Session::get('error'))
    Sorry, your username or password was incorrect.
@endif
We'll also need to log users out; so let's create a get_logout method, and add the following. This will log users out, and then redirect them when visiting /admin/login/logout.
public function get_logout(){
    Auth::logout();
    return Redirect::to(URL::to_action('admin::home@index'));
}
The last thing we should do is add the login controller to our routes file.
Route::controller(array(
    'admin::home',
    'admin::login',
));

Filtering routes

To stop people from bypassing our login screen, we need to filter our routes to determine if they're authorized users. We can create the filter in our routes.php, and attach it to our base controller, to filter before the route is displayed.
Route::filter('auth', function() {
    if (Auth::guest()) return Redirect::to(URL::to_action('admin::login'));
});
At this point, all that's left to do is call this in our base controller's constructor. If we extended our login controller from our base, then we'd have an infinite loop that would eventually time out.
$this->filter('before', 'auth');

Setting up the Views

Earlier, we created our main.blade.php layout; now, we’re going to do something with it. Let's get an HTML page and our assets being brought in.
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>{{$title}}</title>
    {{Asset::container('header')->styles()}}
    <!--[if lt IE 9]>
    <script src="http://html5shim.googlecode.com/svn/trunk/html5.js"></script>
    <![endif]-->
  </head>
  <body>
    <div class="container">
        {{$content}}
    </div>
  {{Asset::container('footer')->scripts()}}
  </body>
  </html>
You'll notice that I've also echoed out a couple of variables: $title and $content. We'll be able to use magic methods from our controller to pass data through to these. I've also popped $content inside the container div that Bootstrap will provide the styling for.
Next, let's create the view for our dashboard. As we'll be nesting this, we only need to put the content we want to put into our container.
<h1>Hello</h1>
<p class="lead">This is our dashboard view</p>
Save this as index.blade.php within the views/dashboard directory inside of your bundle.
We now need to set our controller to take advantage of the layout and view files that we just created. Within the get_index method that we created earlier, add the following.
$this->layout->title = 'Dashboard';
$this->layout->nest('content', 'admin::dashboard.index');
title is a magic method that we can then echo out as a variable in our layout. By using nest, we're able to include a view inside the layout straight from our controller.

Creating a Task

In order to speed things up, Laravel provides us with an easy way to execute code from the command line. These are called "Tasks"; it's a good idea to create one to add a new user to the database easily.
We simply need to ensure that the file takes on the name of our task, and pop it into our bundle's tasks directory. I'm going to call this setup.php, as we'll use it just after installing the bundle.
use Laravel\CLI\Command as Command;
use Admin\Models\Admin as Admin;
class Admin_Setup_Task {
public function run($arguments){
    if(empty($arguments) || count($arguments) < 5){
        die("Error: Please enter first name, last name, username, email address and password\n");
    }
    Command::run(array('bundle:publish', 'admin'));
    $role = (!isset($arguments[5])) ? 'admin' : $arguments[5];
    $data = array(
        'name' => $arguments[0].' '.$arguments[1],
        'username' => $arguments[2],
        'email' => $arguments[3],
        'password' => Hash::make($arguments[4]),
        'role' => $role,
    );
    $user = Admin::create($data);
    echo ($user) ? 'Admin created successfully!' : 'Error creating admin!';
}
}
Laravel will pass through an array of arguments; we can count these to ensure that we're getting exactly what we want. If not, we'll echo out an error. You'll also notice that we're using the Command class to run bundle:publish. This will allow you to run any command line task built into Laravel inside your application or bundle.
The main thing this task does is grab the arguments passed through to it, hash the password, and insert a new admin into the Admins table. To run this, we need to use the following in the command line.
php artisan admin::setup firstname lastname username email@address.com password

What Now?

In this tutorial, we created an boilerplate admin panel that is quite easy to extend. For example, the roles column that we created could allow you to limit what your clients are able to see.
A bundle can be anything from an admin panel, like we built today, to Markdown parsers – or even the entire Zend Framework (I'm not kidding). Everything that we covered here will set you on your way to writing awesome Laravel bundles, which can be published to Laravel's bundle directory.
Learn more about creating Laravel bundles here on Nettuts+.

No comments:

Post a Comment