cult3

Extending Eloquent in Laravel 4

Aug 05, 2013

Table of contents:

  1. Introducing Magniloquent
  2. What will Magniloquent do?
  3. Building Magniloquent
  4. Conclusion

One of the great things about Laravel 4 is how easy it is to extend. It is very easy to create your own package and replace a part of Laravel by using a ServiceProvider. This allows you to take an existing package from the PHP community and plug it straight in, or create a bespoke package that works exactly how you want it to.

In this post I’m going to look at extending Laravel’s Eloquent ORM so that I can create my own methods and add validation in to the Model. If you remember back to my post on Ardent, Ardent is simply extending Eloquent so it inherits all of the existing functionality. I’m going to do the same with my extension so I can build on top of a great foundation.

Hopefully this will be a nice and easy introduction to extending bits of Laravel 4!

Introducing Magniloquent

To extend Eloquent, all you have to do is create a new class which extends Eloquent. I’m calling my class Magniloquent and I’m saving it in the app/models directory:

class Magniloquent extends Eloquent
{
}

Because I’m extending Eloquent, Magniloquent will automatically have all of the methods and properties of Eloquent. This means I can add new stuff in very easily.

When you are doing something like this, you just have to be aware of how Eloquent works. I find it useful to have the GitHub page open whilst I’m working because it’s handy to have a reference as to what you are extending.

For example, the constructor must have an $attributes array set as default:

public function __construct($attributes = array())
{
    parent::__construct($attributes);
    $this->validationErrors = new MessageBag;
}

The parent::__construct($attributes); line simply calls the contractor on Eloquent (the parent class).

I’m also creating a new instance of Laravel’s MessageBag. This makes working with error messages a lot nicer.

What will Magniloquent do?

So what am I trying to achieve with Magniloquent? Well I have some strong opinions on how I want my Models to behave:

Action specific validation - One of the most important things is I want is to set specific validation rules on certain actions. For example, I want to ensure that a username is unique when a new user is created, but I don’t want that same rule to apply when the user is updating her profile information.

Auto hydrate - I want my Model to automatically hydrate without me having to write code.

Auto purge confirmations - I have no reason to store confirmation fields so I will get rid of these by default.

Auto hash passwords - I will always want to hash passwords so this will happen by default. If the password has not changed I won’t rehash the password again.

Building Magniloquent

Magniloquent is clearly heavily inspired by the great work of Ardent. If the changes I wanted to implement were only small, I could of just made a Pull Request to see if the author would be willing it implement them. However, because I want to make some dramatic changes, it doesn’t feel right to try and impose them on someone else’s package.

If you like the look of Ardent, you should definitely use it as it’s a great package.

However, here is how I went about creating Magniloquent.

Save

The first method I will look at is save(). When using Magniloquent, I want to keep the clean syntax of:

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

However, before I actually save the data to the database, first I need to run some other methods to validate and clean up the input.

The save() method in Magniloquent is where I will run through the procedures that I need before actually saving.

To save the data, I will create a new method called performSave():

private function performSave(array $options = array())
{
    return parent::save($options);
}

As you can see, this simply runs the save() method on the parent class.

Validation on actions

An important feature I want to implement with Magniloquent is the ability to have certain validations only on certain actions. For example, I want to ensure that the username and email fields are unique when the User is created, but not when the user is updating their details.

To implement this, first I reformatted my static rules array in the User Model to segment the rules:

/**
 * Validation rules
 */
protected static $rules = array(
    "save" => array(
        'username' => 'required',
        'email' => 'required|email',
        'password' => 'required|min:8'
    ),
    "create" => array(
        'username' => 'unique:users',
        'email' => 'unique:users',
        'password' => 'confirmed',
        'password_confirm' => 'min:8'
    ),
    "update" => array()
);

Here I’m simply saying that the save array is the default validations, and the create and update specific arrays should only be merged in on those actions.

Next, back in Magniloquent, I want to set the static $rules property by merging the required validation rules together:

// Merge the rules arrays into one array
// (in the save() method)
static::$rules = $this->mergeRules();

And here is the mergeRules() method for merging the rules into one array:

/**
 * Merge Rules
 *
 * Merge the rules arrays to form one set of rules
 */
private function mergeRules()
{
    if ($this->exists) {
        $merged = array_merge_recursive(static::$rules['save'], static::$rules['update']);
    }else{
        $merged = array_merge_recursive(static::$rules['save'], static::$rules['create']);
    }

    foreach ($merged as $field => $rules) {
        if (is_array($rules)) {
            $output[$field] = implode("|", $rules);
        }else{
            $output[$field] = $rules;
        }
    }
    return $output;
}

Validate

The validate() method simply attempts to validate the input and the merged rules. If the validation passes, the method returns true. However, if the validation does not pass, the method returns false and the errors are set on the $this->validationErrors property:

/**
 * Validate
 *
 * Validate input against merged rules
 */
private function validate($attributes)
{
    $validation = Validator::make($attributes, static::$rules);

    if ($validation->passes()) return true;

    $this->validationErrors = $validation->messages();

    return false;
}

Back in the save method, if the validation fails, we can just return false as there is no need to proceed:

// If the validation failed, return false
if (!$this->validate($this->attributes)) {
    return false;
}

Purge Redundant Fields

To purge redundant fields, I’m simply going to strip out any fields that end with _confirmation. This can easily be extended in the future based on other requirements, but there’s no point in trying to think up every possibility at this stage:

/**
 * Purge Redundant fields
 *
 * Get rid of '_confirmation' fields
 */
private function purgeRedundant($attributes)
{
    foreach ($attributes as $key => $value) {
        if (!Str::endsWith( $key, '_confirmation')) {
            $clean[$key] = $value;
        }
    }
    return $clean;
}

In the save() method, I will simply save the return from the purgeRedundant() method to the $this->attributes property:

// Purge Redundent fields
$this->attributes = $this->purgeRedundant($this->attributes);

Auto hash

Again, I’m very likely going to extend this method in the future to encompass additional fields that should be auto hashed by Magniloquent. But for the time being, this will do the job:

/**
 * Auto hash
 *
 * Auto hash passwords
 */
private function autoHash()
{
    if (isset($this->attributes['password']))
    {
        if ($this->attributes['password'] != $this->getOriginal('password')) {
            $this->attributes['password'] = Hash::make($this->attributes['password']);
        }
    }
    return $this->attributes;
}

And once again, I will simply save the output to the $this->attributes array:

// Auto hash passwords
$this->attributes = $this->autoHash();

Perform the save

Finally, I can save the Model using the performSave() method. I will also return the boolean return value to indicate whether the record save correctly or not:

return $this->performSave($options);

Conclusion

So as you can see, it is very easy to extend Laravel classes to add your own functionality. In this case, I’ve added some very specific functionality that I wanted without having to create my own ORM package from scratch.

After I wrote this tutorial I decided to separate Magniloquent into it’s own package so I could use it in other projects. Feel free to use Magniloquent in your projects if you want. However, it is still far from a 1.0 release and so the code will likely change up until that point. Some of the methods in this tutorial have already been rewritten as I have required it to work differently.

If you notice any bugs, or you want to make any suggestions, please feel free to open a Pull Request.

This is a series of posts on building an entire Open Source application called Cribbb. All of the tutorials will be free to web, and all of the code is available on GitHub.

Philip Brown

@philipbrown

© Yellow Flag Ltd 2024.