Home » Code » Extending Eloquent in Laravel 4

Extending Eloquent in Laravel 4

Posted by on August 5th, 2013

Extending Eloquent in Laravel 4
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:

<?php
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
 */
public 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->purgeRedundent($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.

To view a full listing of the tutorials in this series, click here.

Philip Brown

Hey, I'm Philip Brown, a designer and developer from Durham, England. I create websites and web based applications from the ground up. In 2011 I founded a company called Yellow Flag. If you want to find out more about me, you can follow me on Twitter or Google Plus.

Join the Culttt

Become an insider and join the Culttt

  • Mateusz Tracz

    You forgot about tests :)

    • http://culttt.com/ Philip Brown

      Haha I know :p

      Hopefully I should have the GitHub repo updated soon.

  • flackjap

    Something I noticed and don’t know if its intentional or you already know that – This post isn’t showing in Code category. Was looking to link it to someone and found it displayed only at homepage.

    • http://culttt.com/ Philip Brown

      Hmm, that’s weird, it is in the Code category. Perhaps it was just WordPress having a derp moment.

      Thank you for taking the time to let me know, and thank you for sharing it :) Much appreciated!

  • Juukie14

    Nice way of handling the differences in validation rules. Little question: should custom validation messages be added to the translation file?

    • http://culttt.com/ Philip Brown

      Hmm, I guess so. I’ve not really looked into customer validation messages to be honest.

      If you think of a good way of handling it, feel free to submit a pull request :)

  • snapey

    Thanks for this article.
    In not checking the uniqueness of certain fields on update, did you consider the scenario where a user changes a field to match another existing record?

    • http://culttt.com/ Philip Brown

      I would probably separate the logic out. So when a user updates their profile details, the whole model can be updated. But when they want to update their username or email address, I would use a different method to ensure it doesn’t match an existing user.

      To be honest when I come to implement that logic, I will probably just look at other people’s patterns to see how they handled it.

      Good question though! I hadn’t really thought about that at all.

      • kabir

        I don’t get the point if model has unique property set all the time it would also check uniqueness while updating too so I don’t get why are you changing it in first place

        • http://culttt.com/ Philip Brown

          When a user updates their username, you don’t want the validation to fail because their email is already in the database. In this case, you don’t want to check that the email is unique when you update because it will find the current record and fail.

    • http://mikeritteroline.com Mike Ritter

      This is where I check if the field’s value has changed then validate.

      Something like this

      $rules = array();
      if(Input::get(‘email’)!=User::find($id)->email)
      {
      array_push($rules,”email”=>”unique:users”);
      }

  • Etienne Wilhelm Marais

    Thank you for the great article. I really enjoy learning laravel 4 and you provide us with such great examples to write proper code.

    • http://culttt.com/ Philip Brown

      Thank you :) Glad you are finding them useful! :D

  • itrulia

    Models are not reponsible for validating…

    • http://culttt.com/ Philip Brown

      It’s pretty subjective…

      • itrulia

        not in MVC where the models are only the holder of the data, validation comes before inserting

        • Daniel Hollands

          Do you care to qualify that with any evidence?

          To my mind, seeing as the model is responsible for the data, surely it should also be responsible that the data it handles is correct?

  • Dels

    Do you recommend to use Magniloquent over Ardent for production site?

    • http://culttt.com/ Philip Brown

      I would use Ardent as Magniloquent is still subject to change

  • Sergey Telshevsky

    Auto hashing password works only when creating an instance and doesn’t trigger when updating, if I wan’t to use a mutator for that, it works great for updates, but for creates it hashes passwords twice, not a big issue, but performance suffers a bit

    • http://culttt.com/ Philip Brown

      Yeah, I dare say there will be many weird little issues like this as I start to actually use it more in a proper application. I think I will probably just tackle them as I discover them to be honest.

      If you could open up an issue on the GitHub repo that would be awesome? :D

  • Borislav Borisov

    Philip i got an error -> Cannot redeclare non static MagniloquentMagniloquentMagniloquent::$rules as static User::$rules .. when i submit the register form. when i remove static from the property $rules in usermodel another error occurs. Any idea? Thank you in advance

  • Daniel Barnhart

    I’m trying to create my own ORM similar to yours, but with some changes. I’m trying to put it in a separate lib directory (applibORMMyORM.php). For some reason it cannot find the class in the model I created. I have the applib folder in the autoload classmap. I have the MyORM.php file in the namespace ORM, and in the model I have the line “use ORMMyORM;”, but still I get the error that it cannot find the class.

    I understand that I am straying from your original tutorial by trying to put it in a separate folder, but I would really appreciate your help! Thank you!

    • http://culttt.com/ Philip Brown

      If you’re 100% sure you’ve got the namespaces and directories right, try running composer dump-autoload and php artisan dump-autoload.

      • Daniel Barnhart

        I think it was the php artisan dump that did it! Thanks! I was going insane over here trying to figure it out.

        • http://culttt.com/ Philip Brown

          No problem, it happens to me all the time :)

  • Ley

    Hey PB, im getting Target [CribbbStorageUserUserRepository] is not instantiable. …

    • http://culttt.com/ Philip Brown

      You need to bind it to an implementation in your Service Provider. You can instantiate an interface.

  • Pingback: Advanced Validation as a Service for Laravel 4 | Culttt

  • Pingback: Extending the Laravel 4 Validator | Culttt

Supported by