Drupal ORM Wrapper

In a previous blog post I briefly spoke about our custom ORM called RootsRecord. In this blog post, I am going to go into detail about RootsRecord.

Background

Neil Hastings
#Development | Posted

In a previous blog post I briefly spoke about our custom ORM called RootsRecord. In this blog post, I am going to go into detail about RootsRecord.

Background

Before I dive into the technical details, I wanted to talk about the motivation. While I was working with Ruby on Rails on a recent project, I enjoyed the use of ActiveRecord for interaction with data. ActiveRecord is how Ruby on Rails interacts with the databases. It's called an Object Relational Mapper or ORM (http://en.wikipedia.org/wiki/Object-relational_mapping).

An ORM allows you to abstract out your data interaction from your business logic. The concept is simple. In database terms: each class represents a table and each instance of the class (the object) represents a row in the table. The methods in the class interact with the data in a logical way without requiring developers having to actually write the SQL statements. In more complex cases, the logical object can interact with several database tables.

Do it in Drupal

So you might ask, doesn't Drupal already do it? The short answer, no. Drupal does abstract out the direct database interaction (especially in Drupal 7 with the next-generation Database API) but you still have to write the SQL (in the case of db_query) or define the tables/fields/etc (in the case of D7). If you are creating a new entity type in Drupal 7 then you should be using the Entity API module (http://drupal.org/project/entity), which has a built in ORM. Entities are great if you are creating new content, but if you want to act with configuration in custom tables, you still have to write your custom code.

The Technical Details

To make our ORM as easy to implement as possible, we created an abstract class. All we have to do to implement a new class is to extend the abstract class, add a few methods and properties and BAM! So let's walk through the abstract class.
Here is the full class.

// $ID:$
/**
 * @file
 * Record abstract class
 */

abstract class RootsRecord {
  // Internal variables.
  protected $data = array();
  protected $valid;
  protected $needsValidation = TRUE;
  protected $errors = array();
  // Run the unserialize
  protected $shouldUnserialize = FALSE;
  // Define the table this record is for
  abstract protected function table();
  /**
   * Public method to get the fields
   */

  public function getFields() {
    $schema = drupal_get_schema($this->getTable());
    return array_keys($schema['fields']);
  }
  /**
   * Public method to check if a field is serialized
   */

  public function isSerialized($field) {
    $schema = drupal_get_schema($this->getTable());
    return isset($schema['fields'][$field]['serialize']) ? $schema['fields'][$field]['serialize'] : FALSE;
  }
  /**
   * Public method to the table
   */

  public function getTable() {
    return $this->table();
  }
  /**
   * Constructor for Record objects.
   *
   * @param $param
   *   Can be either an id or an array.
   */

  public function __construct($param = NULL) {
    if (!empty($param)) {
      if (is_numeric($param)) {
        $this->load($param);
      }
      elseif (is_array($param)) {
        $this->loadFromArray($param);
      }
    }
  }
  /**
   * Load the record object from the database row identified by $id.
   */

  public function load($id) {
    $row = db_fetch_array(db_query('SELECT * FROM {'. $this->table() .'} WHERE id = %d', $id));
    if (!empty($row)) {
      $this->data = $row;
    }
    if ($this->shouldUnserialize) {
      $this->unserialize();
    }
  }
  /**
   * Unserialze all serilzed fields
   */

  protected function unserialize() {
    foreach ($this->data as $field_name => $field) {
      if ($this->isSerialized($field_name)) {
        $this->data[$field_name] = unserialize($field);
      }
    }
  }
  /**
   * Load the record object with data from an array.
   */

  public function loadFromArray($array) {
    $fields = $this->getFields();
    foreach ($array as $key => $value) {
      if (in_array($key, $fields)) {
        $this->data[$key] = $value;
      }
    }
    $this->invalidate();
  }
  /**
   * Is the record new?
   */

  public function isNew() {
    return (empty($this->id));
  }
  /**
   * Does the record pass validation?
   */

  public function isValid() {
    if ($this->needsValidation) {
      $this->resetErrors();
      $this->valid = $this->validate();
      $this->needsValidation = FALSE;
    }
    return $this->valid;
  }
  /**
   * Validation routine for the record. Should be overridden by sub-classes.
   */

  protected function validate() {
    return TRUE;
  }
  /**
   * Mark the record as needing validation.
   */

  protected function invalidate() {
    $this->needsValidation = TRUE;
  }
  /**
   * Returns the array of validation errors.
   */

  public function getErrors(#41; {
    return $this->errors;
  }
  /**
   * Used internally to set a validation error.
   */

  protected function setError($error, $field = 'all') {
    $this->errors[$field][] = $error;
  }
  /**
   * Resets the errors array.
   */

  protected function resetErrors() {
    $this->errors = array();
  }
  /**
   * Save the record to the database.
   */

  public function save() {
    //$current = $this->checkUniqueFields();
    if (!$this->isValid()) {
      return FALSE;
    }
    if ($this->isNew()) {
      $return = drupal_write_record($this->table(), $this);
      $this->data['id'] = db_last_insert_id($this->table(), 'id');
    }
    else {
      $return = drupal_write_record($this->table(), $this, array('id'));
    }
    $this->invalidate();
    return $this->saveCheck();
  }
  /**
   * Check to see if the record was loaded
   */

  protected function saveCheck() {
    if (empty($this->data['id'])) {
      $this->errors[] = t('Record Not Saved');
      return FALSE;
    }
    return TRUE;
  }
  /**
   * Delete the record from the database.
   */

  public function delete() {
    db_query('DELETE FROM {'. $this->table() .'} WHERE id = %d', $this->id);
    unset($this->data['id']);
  }
  /**
   * Property overloading for setting the value of a db column. The 'id' field
   * is not settable through the public interface.
   */

  public function __set($name, $value) {
    if (($name != 'id') && in_array($name, $this->getFields())) {
      $this->data[$name] = $value;
      $this->invalidate();
    }
  }
  /**
   * Property overloading for getting the value of a db column.
   */

  public function __get($name) {
    if (array_key_exists($name, $this->data)) {
      return $this->data[$name];
    }
    return NULL;
  }
  /**
   * Property overloading for testing is a db column field is set.
   */

  public function __isset($name) {
    return isset($this->data[$name]);
  }
  /**
   * Property overloading for unsetting a db column field.
   */

  public function __unset($name) {
    if (($name != 'id') && in_array($name, $this->getFields())) {
      unset($this->data[$name]);
      $this->invalidate();
    }
  }
  /**
   * Create new record
   *
   * @see __construct for more info
   */

  public static function create($param, $class) {
    //if ($class instanceof self) {
      $record = new $class($param);
      $record->save();
      return $record;
    //}
    //return 'Cool';
  }
}
/**
 * The Exception Class
 */

class RootsException extends Exception {}

First, let's go over the methods that you could implement in order to use RootsRecord.

  • table: This is the name of the table. This is the only required method needed to implement the ORM. The table can be defined in hook_schema() like normal but must contain a serial id field as the primary key.
  • validate: This function is run before the record is saved. It should return TRUE or FALSE and set all of the error in $errors.

Just implement these two methods and you have a fully functional ORM. Here is an example where we have a table named widgets with the fields id, type and name. The id is a serial id and the name and type fields are varchars. We want the the name to be required.

Here is the class.

class Widget extends RootsRecord {
    projected function table() {
        return 'widgets';
    }
   
    protected function validate() {
        if (empty($this->name)) {
            $this->setError('Name is required', 'name');
            return FALSE;
        }
       
        return TRUE;
    }
}

Here is how to use it.

/**
 * Simple example of using this class.
 */

function saving_a_widget() {
  $widget = new Widget;
  // This represents the 'type' field on your table.
  $widget->type = 'Simple';
  // This represents the name field on the table.
  $widget->name = 'Cool Widget';
  // This does nothing since id is always serial.
  $widget->id = 123;
  // The invalid_field field doesn't exist on your table so it will do nothing.
  $widget->invalid_field = 'foo';
  // This saves the record to the database.
  if (!$widget->save()) {
    // You can access any errors
    $errors = $widget->getErrors();
  }
}
/**
 * Loading a widget.
 */

function loading_a_widget() {
  // You can load a widget by an id if you know it
  $widget = new Widget($id);
  // You can pre populate the fields on the widget
  $fields = array(
    'type' => 'simple',
    'name' => 'Cool Widget',
  );
  $widget = new Widget($fields);
  // You can also create a widget and save it to the db in one step.
  // Pass in an array of your fields and the name of the class to return.
  $widget_in_the_database = RootsRecord::create($fields, 'widget');
}

Let's dive into the rest of the methods and properties.

Internal Properties

  • $data: This is an array of the data from the row of the table. The keys are the field names and the values are the values of the fields.
  • $valid: This is a boolean indicating that the record has been validated and can be saved.
  • $needsValidation: This is a boolean indicating that if the validation method has been run.
  • $errors: This is an array containing any errors from the validation. It's a common place to store error information.
  • $shouldUnserialize: A boolean property indicating if the class should look for serialization. While this can be found automatically from the schema, we specify this to save time from looking through each record. This is only used in the fetching of the record since drupal_write_record takes care of the unserializing.

Methods

  • __construct: The constructor is called when the object is first initiated. You can either pass in the id or an array of initial parameters. The object will attempt to load the record if it exists, if it does not, then it will prepopulate the fields of the object in preparation to save.
  • getFields: This method pull the fields from the defined schema. It returns an array of fields keyed by the name of the field. Each row of the array contains the schema information about the field.
  • isSerialized: Pass in the name of the field and return TRUE for FALSE if the field needs to be serialized.
  • getTable: This returns the name of the table.
  • load: Pass in the id of the record to load up the object with the fields from the database.
  • unserialize: This looks at every field to see if it need to be unserailzed and unserializes it. The method is run on the loading of an object if the $shouldUnserialize property is TRUE.
  • loadFromArray: Prepopulate the object with an array of fields in bulk.
  • isNew: Is the record saved in the db?
  • isValid: This checks if the object is valid according to the validation functions. It takes care of calling all of the necessary validation methods.
  • invalidate: When a field is updated, the object is no longer valid and we tell the object that validation needs to be run again before the object is saved.
  • getErrors: Return the validation errors
  • resetErrors: Reset all of the validation errors.
  • save: Save the record to the database
  • saveCheck: Checks to make sure the record was saved correctly. The method will return TRUE or FALSE.
  • delete: Delete this record from the database.
  • __set: This overrides the setting of a field. If you call $object->field = value then this method is called. We want to set the $data array for the field name.
  • __get: When your code calls $object->field then this function is run. It returns the value from the $data array.
  • __isset: Calling isset($this->field) tells you if the field is set in the $data property.
  • __unset: Calling unset($this->field) unsets the field in the $data property.
  • create: A static function that creates a new object and saves it to the database at the same time.

I hope this helps you as much as it has helped me! If you want to look at our static query class wrapper see the RootsFind class ( https://github.com/treehouseagency/roots/blob/master/examples/record.api... and https://github.com/treehouseagency/roots/blob/master/includes/record.inc .) Please let me know if you end up using this class and how you use it!

Neil Hastings