PHP ActiveRecord with PHP 5.3

May 14, 2009

Update!

Find the latest here.

PHP 5.3 gets ActiveRecord!

php-med-trans-lightA quick search to find an implementation of active record for php on google is discouraging when one considers the state of ActiveRecord for Ruby on Rails. The reader will notice that the top results are from very old posts and the rest of the results preview minimial implementations. Of course, eventually, PHP will see a robust active record similar to RoR. Fortunately, that time is now, thanks to PHP 5.3 and the beneficial new features: closures, late static binding, and namespaces.

My friend Kien and I have improved upon an earlier version of an ORM that he had written prior to PHP 5.3. The ActiveRecord we have created is inspired by Ruby on Rails and we have tried to maintain their conventions while deviating mainly because of convenience or necessity. Our main goal for this project has been to allow PHP developers tackle larger projects with greater agility. However, we also hope that use of this resource will push the PHP community further by learning the wonderful benefits of the Ruby on Rails stack. Enough with the rambling, let’s get to the interesting piece!

Overview

Allow me to reiterate the fact that we have tried to maintain similarity between our implementation and rails’ ActiveRecord to avoid headaches and increase programmer bandwidth. Keeping this similarity in mind, we have tried to re-produce many of the features. Here is a list of those features:

There are other features such as named scopes, additional adapters, transactions (something we want sooner than later), and a few others we hope to add in the future, but we believe this is a great start. We are hoping to have the code hosted on launchpad or the like within a week or two and a website with documentation. Be sure to check back here shortly for updates!

Configuration

Setup is very easy and straight-forward. There are essentially only two configuration points you must concern yourself with:

  • Setting the model auto_load directory.
  • Configuring your database connections

Examples:

ActiveRecord\Config::initialize(function($cfg)
{
    $cfg->set_model_directory('/path/to/your/model_directory');
    $cfg->set_connections(array('development' =>
       'mysql://username:password@localhost/database_name'));
});

#Alternatively (w/o the 5.3 closure):
$cfg = ActiveRecord\Config::instance();
$cfg->set_model_directory('/path/to/your/model_directory');
$cfg->set_connections(array('development' =>
    'mysql://username:password@localhost/database_name'));

Once you have configured these two settings you are done. ActiveRecord takes care of the rest for you. It does not require that you map your table schema to yaml/xml files. It will query the database for this information and cache it so that it does not make multiple calls to the database for a single schema.

Finder methods

ActiveRecord supports a number of methods by which you can find records either by primary key or you can construct your own complex conditions array with other options such as: order, limit, select, group.

#find by primary key
Author::find(3);

#same as above but expecting multiple results
Author::find(1,2,3);

#find the first record with limit
Author::first();

#find last record by order and limit
Author::last();

Author::all();

#this may be evil - but you can pass your raw sql to this method
Author::find_by_sql();


## you can also pass many options to finder methods

#sql => ORDER BY name
Author::find(3,array('order' => 'name'));

#sql => WHERE author_id IN (1,2,3)
#find all with conditions as array
Author::find('all',
  array('conditions' => array('author_id IN(?)',array(1,2,3))));

#sql => WHERE author_id = 3
#find with conditions as string
Author::find('first',array('conditions' => 'author_id=3'));

#sql => GROUP BY name
Author::find(3, array('group' => 'name'));

#sql => LIMIT 0,3
Author::find('all', array('limit' => 3));

#sql => select * from `author` INNER JOIN etc...
Author::find('all', array('joins' =>
  'INNER JOIN book on (book.author_id = author.id)'));

#sql => SELECT name FROM table
Author::first(array('select' => 'name'));

#these methods do not return records
#return true/false
Author::exists(1);

#return integer
Author::count(array('conditions' => array('name = ?', 'John')));

# access is self-evident
$book = Book::first();
echo $book->title;
echo $book->author_id;

#returns an array
$books = Book::all();
echo $books[0]->title;

Dynamic finder methods

ActiveRecord within rails makes clever use of finders by allowing you to dynamically create methods based on the attribute names. This means you can easily make a “find_by_attribute_name” query without having to explicitly define it in the class. We also make use of this feature by using a new PHP 5.3 magic method: __callStatic().

Author::find_by_name('George Bush');
#you can make use of and/or
Author::find_by_name_or_author_id('George Bush', 2);
Person::find_by_first_name_and_last_name('Obama', 'Mama');
#also have find_all_by
Author::find_all_by_name('Tito');
Author::find_all_by_name(array('Tito','Bill Clinton'));

Writer methods

What good is it to have an object that encapsulates a record from the database if you can’t do anything with it?

#call to SQL insert since it knows that it is a new record
$book = new Artist(array('name' => 'Tito'));
$book->save();

## updates
#only update 'dirty' attributes meaning the sql would only update
#fields that have been changed
$book = Book::find(1);
$book->title = 'new title!';
$book->save();

#this will automatically call save
$book = Book::find(1);
$book->update_attributes(array('title' => 'new title!',
  'price' => 5.00));

#will also make call to save
$book = Book::find(1);
$book->update_attribute('title', 'some new title');

$book = new Book;
$book->title = 'new title!';
$book->author_id = 5;
$book->save();
$book->created_at # we also support created_at/updated_at timestamps where applicable
echo $book->id; #id is also set to the auto increment value from the db

#delete
$author = Author::find(4);
$author->delete();

#you can pass readonly on the find so that you cannot save a model
$book = Book::first(array('readonly' => true));
$book->title = 'new'; # or you could set it here by doing $book->readonly(true);
#this will throw an ActiveRecord\ReadonlyException
$book->save();

Relationships

Associations are the complex piece of ActiveRecord. They accept many of the same options as with rails.

#relationships are declared with a static var
class Book extends ActiveRecord\Model {
  static $belongs_to = array(
    array('publisher'),
    array('author', 'readonly' => true, 'select' => 'name', 'conditions' => "name != 'jax'"),
    array('another', 'class_name' => 'SomeModel')
  );
  #has_many accepts select/conditions/limit/readonly/group/primary_key
  #has_many also takes a through option which you can use with source
  #to clarify the class
  static $has_many = array(
    array('revisions'),
    array('editors', 'through' => 'revisions')
  );

  static $has_one = array(
    array('something')
  );
}

#the 0 index declaration array of each association is the "attribute_name"
#which you use to access on the model like so:
$book = Book::first();
echo $book->publisher->name;
echo $book->author->name; # we only have name as an attribute b/c of the select opt
#below will throw a readonlyException due to the option -- see the writer
#methods section
$book->author->save();

#has_many
echo $book->revisions[0]->id;
#has_many through
echo $book->editors[0]->name;

Validations

This is rather self-explanatory. Before save/update/insert, your validations will be called for each declaration you have made and will save the model if all validations have passed. Otherwise, you will have access to an errors attribute which you can use to get back validation error messages.

class Book extends ActiveRecord\Model
{
  static $validates_format_of = array(
    array('title', 'with' => '/^[a-zW]*$/', 'allow_blank' => true)
  );
  static $validates_exclusion_of = array(
    array('title', 'in' => array('blah', 'alpha', 'bravo'))
  );
  static $validates_inclusion_of = array(
    array('title', 'within' => array('tragedy of dubya', 'sharks wit laserz'))
  );
  static $validates_length_of = array(
    array('title', 'within' => array(1, 5)),
    array('attribute2', 'in' => array(1,2)),
    array('attribute3', 'is' => 4, 'allow_null' => true)
  );
  # same as above since it is just an alias
  static $validates_size_of = array();
  static $validates_numericality_of = array(
    array('title')
  );
  static $validates_presence_of = array(
    array('title')
  );
};

$book = new Book;
$book->title = 'this is not part of the inclusion';
if (!$book->save())
  print_r($book->errors->on('title'));

Callbacks

Callbacks allow you to take command of your model before/after certain events during its lifecycle. You can define methods in your model that will occur as callbacks before or after other methods are invoked on the model. Unfortunately, even though PHP 5.3 has closures, you cannot use them in a static var declaration so you must define/use methods.

#below are the possible declarations that you can make
#if your callback returns false for a before_* then it will cancel the
#action and the rest of the callbacks
class Book extends ActiveRecordModel{
  static $after_construct;
  static $before_save = array('do_something_before_save');
  static $after_save;
  static $before_create;
  static $after_create;
  static $before_update;
  static $after_update;
  static $before_validation;
  static $after_validation;
  static $before_validation_on_create;
  static $after_validation_on_create;
  static $before_validation_on_update;
  static $after_validation_on_update;
  static $before_destroy;
  static $after_destroy;

  #this will be called directly before save()
  public function do_something_before_save() {}
}

Serializations

class Book extends ActiveRecord\Model{
  public function upper_title(){
    return strtoupper($this->title);
  }
}

#produces: {title: 'sharks wit lazers', author_id: 2}
$book = Book::find(1);
$book->to_json();

#produces: {title: 'sharks wit lazers'}
$book->to_json(array('except' => 'author_id'));

#produces:  {upper_title: 'SHARKS WIT LAZERS'}
#make methods an array of methods and it will call them all
$book->to_json(array('methods' => 'upper_title', 'only' => 'upper_title'));

#produces {title: 'sharks wit lazers', author_id: 2}
$book->to_json(array('include' => array('author')));

#also support xml w/ the same options but need more tests =)
$book->to_xml();

Support for multiple adapters

Currently, there exists support only for MySQL (also through mysqli) and sqlite3. Right now there are only two contributors to the code base; however, we hope to attract more coders to the project that can help support additional adapters such as postgres and oracle. The connection/adapter piece of the code has been sufficiently abstracted so that it should not be difficult to create more adapters when the time comes.

Miscellaneous Options

When declaring a model you can also specify the primary_key and table_name. Protected/accessible declarations are available so that you can avoid mass assignment problems. Attributes can be aliased so that you may access them via different names.

class Book extends ActiveRecord\Model{
  static $primary_key = 'book_id';
  static $table_name = 'book_table';
  static $attr_accessible = array('author_id');
  static $attr_protected = array('book_id');
  static $alias_attribute = array(
    'new_alias' => 'actual_attribute',
    'new_alias_two' => 'other_actual_attribute'
  );
}

The Future

As I stated previously, very shortly the code will be available on launchpad. We are also in the works of putting up a website to host tutorials and documentation for the code. I will make additional posts once those milestones have been reached. Thanks for reading.

14 Responses to “PHP ActiveRecord with PHP 5.3”


  1. […] here:  Derivante » PHP ActiveRecord with PHP 5.3 Maio 14th, 2009 | Tags: active, considers-the-state, convenience, discover-the-convenience, market, […]


  2. […] View original post here: Derivante » PHP ActiveRecord with PHP 5.3 […]

  3. Oliver Says:

    I can’t wait for this!

  4. Thomas Says:

    Wow, very nice implementation of ActiveRecord. You’ve pulled all of the great feature across from RoR’s implemetation of the ActiveRecord pattern.

    Would love to help out with development and testing. Possibly building a wrapper class for integration into the Zend Framework.

  5. Logan Says:

    Looks amazing, can’t wait to use it.

  6. Brian Says:

    Looking forward to this as well!

  7. monk.e.boy Says:

    Looks nice. Would like to see lots of complex join examples. Say I have a DB with
    – people
    – orders
    – payments
    – companies

    and I’d like to see who has an order with what company. How many payments have been made and who they went to… etc…

    Nice work, interesting to see how the new PHP5 namepaces look 🙂 (a bit grim)

  8. Shawn Says:

    One word: Doctrine.
    phpdoctrine.org

    It’s essentially the same thing but its supported by a huge community.

  9. Kien La Says:

    I’d argue they are not the same. The end results might be the same but I think you’ll find the path to get there is far simpler with an activerecord approach.

    With activerecord and convention over configuration you do not have to manage mapping files or use any code generators. Few lines of code and bam you can do pretty much all the typical operations you need.


  10. I found your blog on google and read a few of your other posts. I just added you to my Google News Reader. Keep up the good work. Look forward to reading more from you in the future.

  11. Felipe Says:

    Quite interesting, and I can also implement other frameworks, because it doesn’t “lock” me in a specific file structure.
    But, what happens if I have a multiple primary Key? Would that PK become an Array?


  12. Felipe, right now we are not supporting composite primary keys, although, it is most certainly on our to-do list for the future. We have tried to structure our code so that there isn’t a significant amount of work required to implement it, but we simply did not consider it high priority for the first release.


  13. […] brings me to what I’m really excited about. Recently we pushed out PHP ActiveRecord for everyone! This tutorial goes over the steps necessary to build your environment. You’ll be able […]


  14. I have to use this in my development.

    I propose full replace existing validation syntax. Many items – it’s bad.

    I rewrite previous example to next:

    static $validates = array(
    ‘title’ => ‘with:”^[a-zW]*$”, blank:1, len:1-5, exclude: “blah,alpha,bravo”, within: “tragedy of dubya,sharks wit laserz”, null: 1’,
    ‘column2’ => ‘len: 10-50’
    );

    P.S. Please see next propositions, and if you have – contact with me by email.


Leave a comment