Aug 05, 2013
Table of contents:
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!
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.
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.
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.
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.
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;
}
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;
}
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);
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();
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);
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.