Update!
Find the latest here.
PHP 5.3 gets ActiveRecord!
A 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:
- Finder methods
- Dynamic finder methods
- Writer methods
- Relationships
- Validations
- Callbacks
- Serializations
- Support for multiple adapters
- Miscellaneous options
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.
I will not debate with your decisions because I think you’re right on the money! You have put together a consistent case for your sentiments and now I know more about this unusual topic. Gives Thanks for this amazing post and i will come back for more.
I am not new to blogging and genuinely value your site. There is much original message that peaks my interest. I am going to bookmark your internet site and keep checking you out.
What about many-many polymorphic relationships?
Is there going to be an IRC channel? I’d love to have somewhere to talk theory and design!
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.