Documentation
While this attempts to document most of the features of ActiveRecord, it may not be entirely complete. I’ve tried to create tests for all pieces of functionality that exist in ActiveRecord. To view and / or run these tests check out the devel/ branch in the Subversion repository. In other words, there may be some functionality that is not documented here but is used in the tests.
For example purposes, let’s pretend we’re building a blog. You’ll have model classes which are each the model of a database table. Each model class is in a separate file. The stubs of these files are automatically generated for you by generate.php. Every time you update your database schema, you’ll have to run generate.php again. It will not overwrite the files you’ve altered, but will overwrite the *Base.php files. Once you have the model stubs generated you can use them and work with the tables individually. However, in order to use the relationship specific abilities of ActiveRecord, you’ll need to specify the relationships in your models as outlined below in the Associations section.
Associations
In ActiveRecord we specify relationships between the tables in the model classes. There are 3 types of relationships, 1:1, 1:many, and many:many.
1:1
In our example, blog posts have a 1:1 relationship with slugs. Here’s how you’d specify that inside the Post and Slug classes.
-
/* inside Post.php */
-
-
/* inside Slug.php */
In a 1:1 relationship we must specify each side of the relationship slightly differently so that ActiveRecord knows the “direction” of the relationship. We use belongs_to for the model whose table contains the foreign key (post_id in this case). The other side of the relationship uses has_one. Since an object could have multiple 1:1 relationships, we use an array to allow for additional tables. Notice the singular use of slug and post. The code tries to read like English as much as possible, so later when we do 1:many relationships you’ll plural strings.
After you’ve specified this relationship you can do some extra things with your models. On every slug and post object you can now do →post and →slug to get its post and slug respectively as an ActiveRecord object. Also you set assign a slug or post using this mechanism. Furthermore, a save will cascade to the relationship.
-
$slug = Slug::find(‘first’); # SQL query to grab first slug
-
$slug->post; # an SQL query occurs behind the scenes to find the slug’s post
-
-
$p->slug; # no SQL query here because we already got this post’s slug in the SQL join in the previous line
-
-
$p = Post::find(‘first’);
-
$p->slug = $s; # assign a slug to this post
-
-
$p->slug->slug = ‘foobar’;
-
$p->save(); # cascading save (post and slug are saved)
1:many
In our example a post has many comments, but a comment only has one post. Here’s how you’d specify it in the Post and Comment classes.
-
/* inside Post.php */
-
-
/* inside Comment.php */
Notice, we used plural “comments” for the has_many and a singular “post” for belongs_to. Also notice how the comments table contains the foreign key (post_id) and therefore is a belongs_to relationship. Once we’ve done this Comment can do the same things as an 1:something relationship can (see 1:1).
Post now has some slight variations to the features added in a 1:1 relationship. Now when accessing the attribute comments you’d get an array of comment ActiveRecord objects that belong to this Post.
-
$p = Post::find(‘first’);
You can also get the list of comment ids that belong to this post by calling →comment_ids. You can set the ids in a similar fashion.
-
$p = Post::find(‘first’);
-
$foo = $p->comment_ids;
-
# foo is now an array of comment ids that belong to this post
-
-
$p->comment_ids = $foo;
-
/* this will remove the comment we popped off of foo
-
and add the comment we pushed onto foo to this post
-
*/
You can also push new objects onto the relationships.
-
$p->comments_push($c); # this call saves the new comment and associates with this post
In this example, we might want to have comments destroyed when their post is destroyed or when they are disassociated with their post. You can have this happen by specifying the relationship slightly differently. You can do this on any sort of relationship. Instead have the following in the Post model.
-
/* inside Post.php */
many:many
A many:many relationship will have an intermediate table (and therefore model) which ties two other tables together. In our example, there is a many:many relationship between posts and categories. Our intermediate table is categorizations. Here is how that is specified:
-
/* inside Categorization.php */
-
-
/* inside Post.php */
-
-
/* inside Category.php */
Since the categorizations table contains the foreign keys post_id and category_id, it has a belongs_to relationship with those. The Post model has a regular has_many relationship with categorizations and a special has_many relationship with categories. We specify which table that relationship goes through (categorizations), IOW which table is the intermediate table of that relationship. The category to post relationship is specified similarly.
Posts and categories can now use the special has_many methods documented in the 1:many relationship.
Working With Models
This section applies to all models regardless of any associations they may have.
Create
-
$p->save(); # saves this post to the table
-
-
$p2 = new Post();
-
$p2->title = "Second Post";
-
$p2->body = "This is the body of the second post";
-
$p2->save(); # save yet another post to the db
Retrieve
Retrieving data involves finding the rows you want to look at and subsequently grabbing the column data as needed. The first parameter for the find method should be one of the following:
- an id number
- an array of id numbers
- the string “first”
- the string “all”
When the first parameter is an id number or the string “first”, the result will be an ActiveRecord object. Otherwise, it will be an array of ActiveRecord objects.
The find method takes quite a few different options for its second parameter by using “named parameters” by accepting an array of key, value pairs. You can pass it the following keys with sane values:
- limit
- order
- group
- offset
- select
- conditions
- include (for associations)
-
$p = Post::find(1); # finds the post with an id = 1
-
$p->title; # title of this post
-
$p->body; # body of this post
-
-
# returns the 10 most recent posts in an array, assuming you have a column called "timestamp"
Update
-
$p = Post::find(1);
-
$p->title = "Some new title";
-
$p->save(); # saves the change to the post
-
-
# alternatively, the following is useful when a form submits an array
-
$p = Post::find(1);
-
$p->update_attributes($_POST[‘post’]); # saves the object with these attributes updated
Destroy
-
$p = Post::find(1);
-
$p->destroy();
Hooks
The following hooks are available, just define the method of the same name in the model that you want to use them:
- before_save
- before_create
- after_create
- before_update
- after_update
- after_save
- before_destroy
- after_destroy
Escaping Query Values
ActiveRecord will do proper escaping of query values passed to where possible. However, it can’t do proper quoting when you do something like the following.
Instead you can use the quote static method to quote that value like so.
-
$title = ActiveRecord::quote($_GET[‘title’]);
Manual Queries
Occasionally, though hopefully rarely, you may need to do specify some queries by hand. You can use the query static method. This returns an associative array with all the rows in it.
-
ActiveRecord::query("SELECT COUNT(*) FROM bar as b1, bar as b2 where b2.id != b1.id");
Table Structure For Example
-
–
-
– Table structure for table `categories`
-
–
-
-
CREATE TABLE `categories` (
-
`id` int(11) NOT NULL AUTO_INCREMENT,
-
`name` varchar(255) DEFAULT NULL,
-
PRIMARY KEY (`id`)
-
) TYPE=MyISAM;
-
-
–
-
– Table structure for table `categorizations`
-
–
-
-
CREATE TABLE `categorizations` (
-
`id` int(11) NOT NULL AUTO_INCREMENT,
-
`post_id` int(11) DEFAULT NULL,
-
`category_id` int(11) DEFAULT NULL,
-
PRIMARY KEY (`id`)
-
) TYPE=MyISAM;
-
-
–
-
– Table structure for table `comments`
-
–
-
-
CREATE TABLE `comments` (
-
`id` int(11) NOT NULL AUTO_INCREMENT,
-
`author` varchar(255) DEFAULT NULL,
-
`body` text,
-
`post_id` int(11) DEFAULT NULL,
-
PRIMARY KEY (`id`)
-
) TYPE=MyISAM;
-
-
–
-
– Table structure for table `posts`
-
–
-
-
CREATE TABLE `posts` (
-
`id` int(11) NOT NULL AUTO_INCREMENT,
-
`title` varchar(255) DEFAULT NULL,
-
`body` text,
-
PRIMARY KEY (`id`)
-
) TYPE=MyISAM;
-
-
–
-
– Table structure for table `slugs`
-
–
-
-
CREATE TABLE `slugs` (
-
`id` int(11) NOT NULL AUTO_INCREMENT,
-
`slug` varchar(255) DEFAULT NULL,
-
`post_id` int(11) NOT NULL,
-
PRIMARY KEY (`id`)
-
) TYPE=MyISAM;
Related Pages
21 Comments »
RSS feed for comments on this post · TrackBack URI
LifeFeel said,
July 27, 2008 @ 6:03 am
I think it’s very nice library for PHP.
Are you stopping to develop this project?
I really wanted to not to stop this project.
sean said,
October 9, 2008 @ 3:28 pm
Does this library support foreign keys other than “table_id” convention? I’d like to use it but am using a legacy database where foreign keys don’t follow the same conventions as rails. Didn’t see it in the docs so thought i’d ask
thanks!
Tanoor said,
October 28, 2008 @ 9:06 am
Hi this library seems very interesting. Is it a simple example or a mature project?
Tanoor.
Brian Yoder said,
March 9, 2009 @ 12:36 am
This all looks pretty cool. I hope this project keeps on going!
I am having some undefined variable errors croppoing up just like some of the others seem to. What’s the story with that? Am I doing something wrong? Here are the specific errors:
Notice: Undefined index: order in /var/www/html/www/includes/models/activerecord/ActiveRecord.php on line 341
Notice: Undefined index: group in /var/www/html/www/includes/models/activerecord/ActiveRecord.php on line 343
Notice: Undefined index: offset in /var/www/html/www/includes/models/activerecord/ActiveRecord.php on line 345
Notice: Undefined index: conditions in /var/www/html/www/includes/models/activerecord/ActiveRecord.php on line 356
Notice: Undefined index: select in /var/www/html/www/includes/models/activerecord/ActiveRecord.php on line 363
Notice: Undefined index: include in /var/www/html/www/includes/models/activerecord/ActiveRecord.php on line 367
Notice: Undefined variable: limit in /var/www/html/www/includes/models/activerecord/ActiveRecord.php on line 399
Notice: Undefined variable: offset in /var/www/html/www/includes/models/activerecord/ActiveRecord.php on line 400
Notice: Undefined variable: column_lookup in /var/www/html/www/includes/models/activerecord/ActiveRecord.php on line 401
Luke said,
March 12, 2009 @ 6:31 pm
Brian, you’re not doing anything wrong. The released version has those notices when PHP’s error_reporting is set to include E_NOTICE. The most recent version in trunk has the “Undefined variable” notices fixed.
akas said,
April 3, 2009 @ 2:41 pm
what is foreign key?
Devin said,
April 20, 2009 @ 4:11 pm
Luke,
I was having an issue accessing associated tables when they had camel-cased names. Example: the class IndustrialProductCategory has_many IndustrialProducts – but when you attempt to access $category->industrialproducts, you get an error with call_user_func_array, because it is attempting to call Industrialproducts::find instead of IndustrialProducts::find. So I made a quick hack that is working for me:
In inflector.php I added the following method:
/* try capitalizing different letters of the class name, with the first letter still capitalized
associations weren’t working with multi-word classes, since it passed, i.e. Industrialproducts instead of IndustrialProducts
*/
function try_class_variations($class_name)
{
$results = array();
/* loop trhough starting w/ 1 – we want to keep the first letter capitalized */
for ($i=1, $j = strlen($class_name); $i <= $j; $i++)
{
$string = strtoupper($class_name[0]); // just in case it wasn’t already capitalized
for ($k=1; $k INdustrialproducts [1] => InDustrialproducts [2] => IndUstrialproducts [3] => InduStrialproducts [4] => IndusTrialproducts [5] => IndustRialproducts [6] => IndustrIalproducts [7] => IndustriAlproducts [8] => IndustriaLproducts [9] => IndustrialProducts [10] => IndustrialpRoducts [11] => IndustrialprOducts [12] => IndustrialproDucts [13] => IndustrialprodUcts [14] => IndustrialproduCts [15] => IndustrialproducTs [16] => IndustrialproductS [17] => Industrialproducts )
then I added the following in HasMany::get, line 55 (right before the try/catch block):
if (!class_exists($this->dest_class))
{
/* try my awesome brute-force capitalization mechanism */
foreach (Inflector::try_class_variations($this->dest_class) as $attempt)
{
if (class_exists($attempt))
{
$this->dest_class = $attempt;
break;
}
}
}
Pretty inelegant, but it works fine for my purposes.
Devin said,
April 20, 2009 @ 4:11 pm
oops. sorry about the code formatting.
Devin said,
April 20, 2009 @ 4:12 pm
OK – maybe this is better
inflector.php
function try_class_variations($class_name)
{
$results = array();
/* loop trhough starting w/ 1 – we want to keep the first letter capitalized */
for ($i=1, $j = strlen($class_name); $i <= $j; $i++)
{
$string = strtoupper($class_name[0]); // just in case it wasn’t already capitalized
for ($k=1; $k dest_class))
{
/* try my awesome brute-force capitalization mechanism */
foreach (Inflector::try_class_variations($this->dest_class) as $attempt)
{
if (class_exists($attempt))
{
$this->dest_class = $attempt;
break;
}
}
}
Devin said,
April 20, 2009 @ 4:13 pm
OK, I give up
Brian Yoder said,
April 27, 2009 @ 7:32 am
I have been using your Active Record implementation and it seems to work OK but I get a huge number of errors along the lines of the messages below. Am I doing something wrong? Is there some fix to this?
–Brian
[27-Apr-2009 07:16:44] PHP Notice: Undefined index: order in /var/www/html/www/includes/models/activerecord/ActiveRecord.php on line 345
[27-Apr-2009 07:16:44] PHP Notice: Undefined index: group in /var/www/html/www/includes/models/activerecord/ActiveRecord.php on line 347
[27-Apr-2009 07:16:44] PHP Notice: Undefined index: offset in /var/www/html/www/includes/models/activerecord/ActiveRecord.php on line 349
[27-Apr-2009 07:16:44] PHP Notice: Undefined index: conditions in /var/www/html/www/includes/models/activerecord/ActiveRecord.php on line 360
[27-Apr-2009 07:16:44] PHP Notice: Undefined index: select in /var/www/html/www/includes/models/activerecord/ActiveRecord.php on line 367
[27-Apr-2009 07:16:44] PHP Notice: Undefined index: include in /var/www/html/www/includes/models/activerecord/ActiveRecord.php on line 371
[27-Apr-2009 07:16:44] PHP Notice: Undefined variable: limit in /var/www/html/www/includes/models/activerecord/ActiveRecord.php on line 403
[27-Apr-2009 07:16:44] PHP Notice: Undefined variable: offset in /var/www/html/www/includes/models/activerecord/ActiveRecord.php on line 404
[27-Apr-2009 07:16:44] PHP Notice: Undefined variable: column_lookup in /var/www/html/www/includes/models/activerecord/ActiveRecord.php on line 406
Brian Yoder said,
May 4, 2009 @ 3:21 am
So what is the idea behind generate.php generating no UI at all? Why not just have it say something like “Generation complete” or “52 models created.” or something?
Brian Yoder said,
May 7, 2009 @ 3:14 am
Maybe I am misunderstanding something here. If I have a many to many relationship between a couple of tables with a link table between them containing IDs of both tables in it, can I just read into the big object and say something like this?
echo $post->comments[$i]->comment_text.’ is my is the ith comment text!’;
And if I want to change a value can I just say something like this?
$post->comments[$i]->comment_text = $RevisedText;
Does it really save the value instantly when I do that? Or only when I do a $post->save(); ? Or is that only what happens when I do this?
$post->comment_push($mycomment);
And what if I want to disassociate one of the comments from the post (but leave it out there because this is a many to many relationship and some other post may share the same comment? Could I just clean out the array like this?
unset($post->comments);
Or could I pop all of the elements off like this?
foreach ($post->comments as $MyComment)
{
array_pop($MyComment);
}
Or perhaps I could remove the fourth comment from the array via:
$post->comments[3] = array();
I have played around with many of these and they don’t seem to work the way I am thinking they should. This makes me wonder just what kind of thing these arrays are and how you access them. If I can’t just put values in there, how can I really change them? You show a few specific calls I can make (to push and pop items from the list for example) but is that really all you can do to them? It would seem that there ought to be more operators than just those, are there? Or do I just have to compose all higher level access to them in terms of looking, pushing, and popping?
Sorry for asking so many questions, but the documentation page and the experiments I have been trying leave me with lot of questions.
Thanks for all your help!
Brian
Luke said,
May 8, 2009 @ 9:50 pm
Brian,
I agree it is not always self-evident what is happening. When I created this I based it off of the ActiveRecord that was shipping with Ruby on Rails at the time. Unfortunately, this PHP version hasn’t kept pace with the Ruby on Rails version, so I can’t simply point you to that documentation.
I’ll answer your last question first, as that might be helpful as I answer the others. The arrays that are returned by calls like $post->comments are your typical PHP arrays with nothing fancy. So, count($post->comments) should work. However, you won’t be able to do anything fancy like unset or pop an element of that array using the core PHP functions and have that affect your database.
If you want to add or remove elements you can use the $post->comments_push() and $post->comments_pop() functions. You can also get an array of the comment ids that belong to a post with the following call $post->comment_ids. Similarly you can add or remove comments by doing $post->comment_ids = array(1, 3, 4). That makes sure that the only comments that belong to that post have ids of 1, 3, or 4. $post->comment_ids = array(); would then remove all associated with that post. If that’s a “has_many through” relationship, then the comment will remain, but be no longer associated with the post referenced.
When you do something like:
$post->comments[$i]->comment_text = $RevisedText;
it does not save the revised text. You must issue a save() for that. You can either do $post->save() or $post->comments[$i]->save(). The reasoning behind this, is you that might want to update multiple attributes on the comment, but only issue one UPDATE or INSERT into the database.
When you do $post->comments_push($comment), the code expects that you want that saved to the database immediately.
If you want to dig in more, I’d encourage you to take a look at the development branch which has a myriad of tests. Examining those will give you some more info as to what should be happening. In fact, I went back to those tests to answer some of these questions, since it has been awhile. :-)
http://lukebaker.org/svn/repos/activerecord/branches/devel/
Luke
Fyodor said,
June 29, 2009 @ 6:47 am
For those seeking to :foreign_key and :class_name options for associations, you may modify following classes:
1. Association in Association.php:
…
function __construct($source, $dest, $options=null) {
$this->source_class = get_class($source);
if (!empty($options['foreign_key'])) {
$this->foreign_key = $options['foreign_key'];
}
if (!empty($options['class_name'])) {
$this->dest_class = $options['class_name'];
} else {
$this->dest_class = ActiveRecordInflector::classify($dest);
}
$this->options = $options;
}
…
2. BelongsTo in BelongsTo.php:
…
function __construct(&$source, $dest, $options=null) {
parent::__construct($source, $dest, $options);
if (!isset($this->foreign_key)) {
$this->foreign_key = ActiveRecordInflector::foreign_key($this->dest_class);
}
}
…
3. HasMany in HasMany.php, HasOne in HasOne.php:
…
function __construct(&$source, $dest, $options=null) {
parent::__construct($source, $dest, $options);
if (!isset($this->foreign_key)) {
$this->foreign_key = ActiveRecordInflector::foreign_key($this->source_class);
}
}
…
Example:
class Product extends ProductBase {
protected $belongs_to = array(array(‘color1′ => array(‘class_name’ => ‘Color’, ‘foreign_key’ => ‘color1′)), array(‘color2′ => array(‘class_name’ => ‘Color’, ‘foreign_key’ => ‘color2′)), array(‘color3′ => array(‘class_name’ => ‘Color’, ‘foreign_key’ => ‘color3′)));
}
Solution is not perfect, just quick patch.
Fyodor said,
June 29, 2009 @ 6:48 am
Sorry for code formatting and my bad English. ;)
Matthieu said,
January 6, 2010 @ 11:36 am
Little bug correction:
in HasMany.php, around line 58, change this:
else {
// TODO: $this->options['through'] is not necessarily the table name
$collection = call_user_func_array(array($this->dest_class, ‘find’),
array(‘all’,
array(‘include’ => $this->options['through'],
‘conditions’ => “{$this->options['through']}.{$this->foreign_key} = “.$source->{$source->get_primary_key()})));
}
to this :
else {
$through_table = ActiveRecordInflector::tableize($this->options['through']);
$collection = call_user_func_array(array($this->dest_class, ‘find’),
array(‘all’,
array(‘include’ => $this->options['through'],
‘conditions’ => “{$through_table}.{$this->foreign_key} = “.$source->{$source->get_primary_key()})));
}
And around line 144, modify this:
$join = “LEFT OUTER JOIN {$this->options['through']} ON ”
. “{$this->options['through']}.{$this->foreign_key} = $source_table.”.$source_inst->get_primary_key() .” ”
. “LEFT OUTER JOIN $dest_table ON ”
. “$dest_table.”.$dest_inst->get_primary_key() .” = {$this->options['through']}.” . ActiveRecordInflector::foreign_key($this->dest_class);
to this:
$through_table = ActiveRecordInflector::tableize($this->options['through']);
$join = “LEFT OUTER JOIN {$through_table} ON ”
. “{$through_table}.{$this->foreign_key} = $source_table.”.$source_inst->get_primary_key() .” ”
. “LEFT OUTER JOIN $dest_table ON ”
. “$dest_table.”.$dest_inst->get_primary_key() .” = {$through_table}.” . ActiveRecordInflector::foreign_key($this->dest_class);
Matthieu said,
January 6, 2010 @ 1:32 pm
Another bug i found:
in file AcitveRecord.php, on around 317, change this:
if ($table_name == ActiveRecordInflector::pluralize($assoc_name))
to this:
if ($table_name == ActiveRecordInflector::tableize($assoc_name))
Sebastian said,
January 11, 2010 @ 9:09 am
An additional function: count
Same parameters as find, but only returns the number of rows.
Code:
In ActiveRecord.php add:
static function count($class, $options=null) {
$class = str_replace(‘Base’, ”, $class);
$query = self::generate_count_query($class, $options);
$rows = self::query($query);
return $rows[0]['COUNT(*)'];
}
function generate_count_query($class_name, $options){
$item = new $class_name;
/* regex for limit, order, group */
$regex = ‘/^[A-Za-z0-9\-_ ,\(\)]+$/’;
if (!isset($options['limit']) || !preg_match($regex, $options['limit']))
$options['limit'] = ”;
if (!isset($options['order']) || !preg_match($regex, $options['order']))
$options['order'] = ”;
if (!isset($options['group']) || !preg_match($regex, $options['group']))
$options['group'] = ”;
if (!isset($options['offset']) || !is_numeric($options['offset']))
$options['offset'] = ”;
$select = ‘COUNT(*)’;
if (isset($options['conditions']))
$where = (isset($where) && $where) ? $where . ” AND (” . $options['conditions'] .”)” : $options['conditions'];
if ($options['offset'])
$offset = $options['offset'];
if ($options['limit'] && !isset($limit))
$limit = $options['limit'];
$joins = array();
$tables_to_columns = array();
$query = “SELECT $select FROM {$item->table_name}”;
$query .= (isset($where)) ? ” WHERE $where” : “”;
$query .= ($options['group']) ? ” GROUP BY {$options['group']}” : “”;
$query .= ($options['order']) ? ” ORDER BY {$options['order']}” : “”;
$query .= (isset($limit) && $limit) ? ” LIMIT $limit” : “”;
$query .= (isset($offset) && $offset) ? ” OFFSET $offset” : “”;
return $query;
}
In ModelBase.tpl add inside the Class definition:
static function count($options=null){
return parent::count(__CLASS__,$options);
}
Ben Arwin said,
March 11, 2010 @ 3:29 pm
I used Fyodor’s solution for adding custom foreign_key and class_name attributes in the $belongs_to and $has_many arrays, but I had to modify it slightly to get it to work.
Instead of using empty() to test if $options['foreign_key'] was set or not, I had to use isset(). E.g. if (!isset($options['foreign_key'])) { … }
So change all instances of “empty” with “isset” and you’re good.
Hopefully this will help others who are banging their heads against the wall ;)
By the way, this is a great project! This beats all of the other pre-PHP-5.3 ORM packages that I’ve tried, hands down. It’s still going to be a while before everyone’s supporting PHP 5.3, so IMHO it’s worth it to make this a “real” project with releases, user forums, and a public git repo. I’d be happy to help if the author is interested?
Cheers,
-Ben
Derek said,
July 6, 2010 @ 4:01 pm
Thank you Sebastian,
That count method did the trick for me.