Ardent: Laravel Models on Steroids

Francesco Malatesta
Share

One of the (few) things I don’t like about Laravel is that you cannot move the validation code from your controller to your models easily. When I write software I like to apply the “fat models, skinny controllers” principle. So, for me, writing the validation code in the controller is not a good thing.

To solve this, I’d like to introduce Ardent, a great package for Laravel 4. To be more precise, Ardent presents itself as “Self-validating smart models for Laravel Framework 4’s Eloquent ORM.” In other words: exactly what we need!

As you can imagine, it’s an extension of the Eloquent Model class, basically. This package comes with some new functionality, utilities and methods dedicated to input validation and other little things.

Our Test Application

For a better understanding of the advantages you can enjoy while using Ardent, we will set up a little test application. Nothing complicated: a simple To-Do List app.

Of course, I’m not going to implement a complete application: I just want to explain some principles, so I will make some controllers and models – no views. After that, I will “translate” the code using Ardent.

Our To-Do List will count two different entities:

  • User

    • id
    • first_name
    • last_name
    • email
    • password
  • Task

    • id
    • name
    • status (done / not done)

A really basic project. However, if you don’t want to write code, don’t worry: I have already prepared a migration that you can use to generate the database. Use it!

Create the migration file with the command

php artisan migrate:make todo_setup

and then, fill the file with this code:

<?php

    use Illuminate\Database\Schema\Blueprint;
    use Illuminate\Database\Migrations\Migration;

    class TodoSetup extends Migration {

        /**
         * Run the migrations.
         *
         * @return void
         */
        public function up()
        {
            Schema::create('users', function(Blueprint $table)
            {
                $table->increments('id')->unsigned();

                $table->string('first_name');
                $table->string('last_name');
                $table->string('email');
                $table->string('password', 60);

                $table->timestamps();
            });

            Schema::create('tasks', function(Blueprint $table)
            {
                $table->increments('id');

                $table->string('name');
                $table->boolean('status');

                $table->integer('user_id')->unsigned();

                $table->timestamps();

                $table->index('user_id');
            });
        }

        /**
         * Reverse the migrations.
         *
         * @return void
         */
        public function down()
        {
            Schema::dropIfExists('users');
            Schema::dropIfExists('tasks');
        }

    }

Now we have our tables. It’s time to create our models. Even here we have very few lines to write. Here’s the User model (that is also the default one).

<?php

    use Illuminate\Auth\UserTrait;
    use Illuminate\Auth\UserInterface;
    use Illuminate\Auth\Reminders\RemindableTrait;
    use Illuminate\Auth\Reminders\RemindableInterface;

    class User extends Eloquent implements UserInterface, RemindableInterface {

        use UserTrait, RemindableTrait;

        /**
         * The database table used by the model.
         *
         * @var string
         */
        protected $table = 'users';

        /**
         * The attributes excluded from the model's JSON form.
         *
         * @var array
         */
        protected $hidden = array('password', 'remember_token');

        public function tasks()
        {
            return $this->hasMany('Task');
        }

    }

I just added the tasks method to describe the relationship with the Task model.

<?php

    class Task extends \Eloquent {
        protected $fillable = [];

        public function user()
        {
            return $this->belongsTo('User');
        }
    }

We just made our starting point. From now on, right after the installation, we will see two different situations: first, the “normal” version of the code without Ardent. Right after that, we will make a comparison with the “improved” version. You will notice the difference, trust me.

Let’s start!

Installing Ardent

Installing Ardent is very easy with Composer. Just add the dependency to the composer.json file of your project.

{
        "require": {
            "laravelbook/ardent": "2.*"
        }
    }

Then, after the update, you just have to extend the Ardent class in your models like so:

<?php
    class User extends \LaravelBook\Ardent\Ardent {
        // model code here!
    }

… and you are ready to go!

Data Validation

The first thing to do is to analyze how Ardent makes our life easier in implementing validation. We already know how to do it with Laravel. Let’s make a classic example: the POST method that will handle data coming in from the form.

In a “normal” situation we would make something like this:

<?php

    public function postSignup()
    {
        $rules = array(
            'first_name' => 'required',
            'last_name' => 'required',
            'email' => 'required|email|unique:users',
            'password' => 'required|min:8'
        );

        $messages = array(
            'first_name.required' => 'First name is required.',
            'last_name.required' => 'Last name is required.',
            'email.required' => 'Email is required.',
            'password.required' => 'Password is required.',

            'email.email' => 'Use a real email address!',
            'email.unique' => 'This email address already exists!',
            'password.min' => 'Password must be at least 8 character long.'
        );

        $validator = Validator::make(Input::all(), $rules, $messages);

        if($validator->fails())
        {
            return Redirect::to('user/signup')->with('errors', $validator->messages());
        }

        $user = new User;

        $user->first_name = Input::get('first_name');
        $user->last_name = Input::get('last_name');
        $user->email = Input::get('email');
        $user->password = Hash::make(Input::get('password'));

        if($user->save())
        {
            $status = 1;
        }
        else
        {
            $status = 0;
        }

        return Redirect::to('user/signup')->with('status', $status);
    }

… what about with Ardent?

With Ardent, things change a little bit. First of all, as you can easily imagine, the validation rules are going to be moved directly into the model: it is here that we will start our “restyle”. So, open the model file and change it like so:

<?php

    use Illuminate\Auth\UserTrait;
    use Illuminate\Auth\UserInterface;
    use Illuminate\Auth\Reminders\RemindableTrait;
    use Illuminate\Auth\Reminders\RemindableInterface;

    class User extends \LaravelBook\Ardent\Ardent implements UserInterface, RemindableInterface {

        public static $rules = array(
            'first_name' => 'required',
            'last_name' => 'required',
            'email' => 'required|email|unique:users',
            'password' => 'required|min:8'
        );

        public static $customMessages = array(
            'first_name.required' => 'First name is required.',
            'last_name.required' => 'Last name is required.',
            'email.required' => 'Email is required.',
            'password.required' => 'Password is required.',

            'email.email' => 'Use a real email address!',
            'email.unique' => 'This email address already exists!',
            'password.min' => 'Password must be at least 8 character long.'
        );

        use UserTrait, RemindableTrait;

        /**
         * The database table used by the model.
         *
         * @var string
         */
        protected $table = 'users';

        /**
         * The attributes excluded from the model's JSON form.
         *
         * @var array
         */
        protected $hidden = array('password', 'remember_token');

        public function tasks()
        {
            return $this->hasMany('Task');
        }

    }

What happened? Not much: we changed the base class of our model (from Eloquent to Ardent) and we moved (using the same syntax, just a copy) the validation rules here, with the custom error messages.

So, the question now is: what are we going to write in our controller?

Let’s check it out:

<?php

    class UserController extends \BaseController {

        public function postSignup()
        {
            $user = new User;

            $user->first_name = Input::get('first_name');
            $user->last_name = Input::get('last_name');
            $user->email = Input::get('email');
            $user->password = Hash::make(Input::get('password'));

            if(!$user->save())
            {
                return Redirect::to('user/signup')->with('errors', $user->errors()->all());
            }

            return Redirect::to('user/signup')->with('status', 1);
        }

    }

No more validation instructions here. They all disappeared. It’s not just a “moving”, however: the $user->save() method, now, will return false if there are some problems in the validation phase. Then, you will be able to retrieve errors with the $user->errors()->all() method. No strange classes: the returned object will be a classic MessageBag that you maybe already met while working with Laravel.

If you prefer, however, you can also use the $user->validationErrors property. In that case, or if you want to retrieve field-specific errors just use$user->validationErrors->get('field_name').

Now, it is very likely that you are thinking “OK, but what is the real advantage, beyond fewer lines of code?”

Let’s start with the most important: better code organization means better project maintainability. In simple applications you can’t feel it as a priority, but when it comes to bigger projects things can be easily messed up with a single wrong decision. Let me make a real-world situation example. We developed our awesome To-Do List application, and it is growing really quickly. We definitely need a RESTful API for the mobile application. Using the controller in the “normal” way will mean writing another signup routine for the API. Two different blocks with the same code! This is no good. Where is the good old DRY (Don’t Repeat Yourself) principle?

So, the best practice would be writing a signup() method in the model that handles everything. Using Ardent means handling really everything: from validation to the save procedure. Without it, we could not accomplish the first phase.

A second advantage is a more practical one: model auto-hydrate. Let’s discover it together.

Model Auto-Hydrate

Our postSignup() method counts exactly thirteen lines of code. Even if it seems difficult, Ardent can lower that number further. Take a look at this example:

<?php

    class UserController extends \BaseController {

        public function postSignup()
        {
            $user = new User;
            if(!$user->save())
            {
                return Redirect::to('user/signup')->with('errors', $user->errors()->all());
            }

            return Redirect::to('user/signup')->with('status', 1);
        }

    }

No errors here. You probably already understand what happened.

Ardent has a model auto-hydrate feature. This means that when you create the object and call the $user->save() method, every field is filled automatically with the Input object data. Of course, you will have to give the right name at every form field accordingly.

So, this code:

$user = new User;

    $user->first_name = Input::get('first_name');
    $user->last_name = Input::get('last_name');
    $user->email = Input::get('email');
    $user->password = Hash::make(Input::get('password'));

    $user->save();

will have the same effect as

$user = new User;
    $user->save();

From thirtheen lines we just went down to seven for an entire signup procedure.

To use this feature, you will have to activate it. Just switch the $autoHydrateEntityFromInput in your model to true, like this:

<?php
    ...
    class User extends \LaravelBook\Ardent\Ardent {
        ...
        public $autoHydrateEntityFromInput = true;
        ...
    }

Done!

You will also often have some redundant data that you will not need for the business logic. Think about _confirmation fields or the CSRF tokens. Well, we can discard them with the model’s $autoPurgeRedundantAttributes property. As before, just switch it to true.

<?php
    ...
    class User extends \LaravelBook\Ardent\Ardent {
        ...
        public $autoHydrateEntityFromInput = true;
        public $autoPurgeRedundantAttributes = true;
        ...
    }

Now the procedure is cleaner than before.

Model Hooks

Another good feature worth mentioning is the introduction of model hooks. They are, essentially, a list of methods that, if implemented, are called in certain execution moments. So, to make an example, the afterUpdate() method will be called before every update() call. The beforeValidate() method will be called before every validation, and so on.

Here’s a list of all these methods:

  • beforeCreate()
  • afterCreate()
  • beforeSave()
  • afterSave()
  • beforeUpdate()
  • afterUpdate()
  • beforeDelete()
  • afterDelete()
  • beforeValidate()
  • afterValidate()

A classic example could be some data elaboration before the save procedure. Like this:

<?php
    public function beforeSave() {
        $this->slug = Str::slug($this->title);
        return true;
    }

Every “before” method has a boolean return value. If true, the following operation is executed normally. If the method returns false, the operation is going to be stopped. In the method above, we generated a slug (and filled the proper field) with the beforeSave() method, right after the validation.

There is also a specific tip about beforeSave() and afterSave(): you can declare them at run-time. Look:

$user->save(array(), array(), array(),
        function ($model) {
            // beforeSave() code here...
            return true;
        },
        function ($model) {
            // afterSave() code here...
        }
    );

Let’s think about a couple of possible uses of these methods in our application.

Maybe a beforeSave() to deal with password hashing?

<?php

    public function beforeSave()
    {
        $this->password = Hash::make($this->password);
    }

Or a cleaning hook right before the user delete procedure?

<?php

    public function beforeDelete()
    {
        $this->tasks()->delete();
    }

There are many possibilities.

Defining Relationships (the Ardent way)

With Ardent, you can also define relationships in a shorter way than before. Let’s see how we actually define our relationships between models: the following example shows the tasks() method in the User model.

<?php

    public function tasks()
    {
        return $this->hasMany('Task');
    }

Using Ardent to define relationships means defining a simple array, called $relationsData.

<?php

    class User extends \LaravelBook\Ardent\Ardent {

        ...

        public static $relationsData = array(
            'tasks'  => array(self::HAS_MANY, 'Task')
        );

        ...

    }

This has the exact same effect. Ardent uses the same naming convention to bind names and methods, without having to write them one by one.

However, there are many customizations that you can do:

<?php

    class User extends \LaravelBook\Ardent\Ardent {
        public static $relationsData = array(
            'books'  => array(self::HAS_MANY, 'Book', 'table' => 'users_have_books')
        );
    }

Every element in $relationsData has a key (yes, the relation’s method name) and an array with some parameters.

  • the first parameter (without a key, it’s just the first) describes the relation type (hasOne, hasMany, belongsTo, belongsToMany, morphTo, morphOne, morphMany).
  • the second parameter (without a key, it’s just the second) defines the destination model of the current relation;
  • further parameters don’t have a specific position, but a key. They could be:
    • foreignKey: optional, used for hasOne, hasMany, belongsTo and belongsToMany;
    • table, otherKey, timestamps and pivotKeys: optional, used for belongsToMany;
    • name, type and id: used with morphTo, morphOne and morphMany;

Conclusion

With a big pack of advantages (also 126k downloads and frequent updates) it’s very difficult to find a reason to not use Ardent in your next app. It is suitable for every kind of project, and being an extension of Eloquent’s Model, it achieves full compatibility with Laravel projects.

I definitely recommend it. Do you? Have you tried it out? Let us know in the comments below!