We've been working hard to get this ready for people to start poking around in and we're happy to announce that it's now ready for public beta testing! You can grab it from http://github.com/kla/php-activerecord/. Play with it... break it... and give us your feedback to help us make a better library for everyone! We want to hear from you.

Quick Start

We'll start first with a bare bones example to show how little you need to get up and running. There's very little to configure. We've adhered to the convention over configuration philosophy so there are no code generators you need to run and no xml/yaml mapping files to maintain.

<?
// make sure the ActiveRecord project is in your current directory
// or your include_path
require_once 'ActiveRecord/ActiveRecord.php';
 
// assumes a table name of "books" with a primay key named "id"
class Book extends ActiveRecord\Model { }
 
// initialize ActiveRecord
ActiveRecord\Config::initialize(function($cfg)
{
    $cfg->set_model_directory('.');
    $cfg->set_connections(array(
        'development' => 'mysql://username:password@localhost/database_name'));
});
 
print_r(Book::first()->attributes());
?>

That's it! The code for this is located in the examples/simple directory. You can run it with "php examples/simple/simple.php". Make sure to modify the connection string to suit your system and run the simple.sql script appropriately.

Serious Business Time

Now for a not so completely trivial example. This example will simulate a very dumb ordering and payment model. See the examples/orders directory for the source. You can run this sample by executing "php examples/orders/orders.php". Again, make sure you modify the connection string to suit your system and be sure the schema in orders.sql has been created in your test database.

First, let's look at the models:

class Person extends ActiveRecord\Model
{
    // a person can have many orders and payments
    static $has_many = array(
        array('orders'),
        array('payments'));
 
    // must have a name and a state which has a custom error message
    static $validates_presence_of = array(
        array('name'), array('state', 'message' => 'Where do you live then?'));
}
class Order extends ActiveRecord\Model
{
    // order belongs to a person
    static $belongs_to = array(
        array('person'),
        array('order'));
 
    // order can have many payments by many people
    // the conditions is just there as an example as it makes no logical sense
    static $has_many = array(
        array('payments'),
        array('people',
            'through'    => 'payments',
            'select'     => 'people.*, payments.amount',
            'conditions' => 'payments.amount < 200'));
 
    // order must have a price and tax > 0
    static $validates_numericality_of = array(
        array('price', 'greater_than' => 0),
        array('tax',   'greater_than' => 0));
 
    // setup a callback to automatically apply a tax
    static $before_validation_on_create = array('apply_tax');
 
    public function apply_tax()
    {
        if ($this->person->state == 'VA')
            $tax = 0.045;
        elseif ($this->person->state == 'CA')
            $tax = 0.10;
        else
            $tax = 0.02;
 
        $this->tax = $this->price * $tax;
    }
}
class Payment extends ActiveRecord\Model
{
    // payment belongs to a person
    static $belongs_to = array(
        array('person'));
}

And here's the code that does everything:

<?
require_once dirname(__FILE__) . '/../../ActiveRecord.php';
 
// initialize ActiveRecord
ActiveRecord\Config::initialize(function($cfg)
{
    $cfg->set_model_directory(dirname(__FILE__) . '/models');
    $cfg->set_connections(array(
        'development' => 'mysql://test:test@127.0.0.1/orders_test'));
 
    // you can change the default connection with the below
    //$cfg->set_default_connection('production');
});
 
// create some people
$jax = new Person(array('name' => 'Jax', 'state' => 'CA'));
$jax->save();
 
// compact way to create and save a model
$tito = Person::create(array('name' => 'Tito', 'state' => 'VA'));
 
// place orders. tax is automatically applied in a callback
// create_orders will automatically place the created model into $tito->orders
// even if it failed validation
$pokemon = $tito->create_orders(array('item_name' => 'Live Pokemon', 'price' => 6999.99));
$coal    = $tito->create_orders(array('item_name' => 'Lump of Coal', 'price' => 100.00));
$freebie = $tito->create_orders(array('item_name' => 'Freebie', 'price' => -100.99));
 
if (count($freebie->errors) > 0)
{
    echo "[FAILED] saving order $freebie->item_name: " .
        join(',',$freebie->errors->full_messages()) . "\n\n";
}
 
// payments
$pokemon->create_payments(array('amount' => 1.99, 'person_id' => $tito->id));
$pokemon->create_payments(array('amount' => 4999.50, 'person_id' => $tito->id));
$pokemon->create_payments(array('amount' => 2.50, 'person_id' => $jax->id));
 
// reload since we don't want the freebie to show up (because it failed validation)
$tito->reload();
 
echo "$tito->name has " . count($tito->orders) . " orders for: " .
    join(', ',ActiveRecord\collect($tito->orders,'item_name')) . "\n\n";
 
// get all orders placed by Tito
foreach (Order::find_all_by_person_id($tito->id) as $order)
{
    echo "Order #$order->id for $order->item_name " .
        "($$order->price + $$order->tax tax) " .
        "ordered by " . $order->person->name . "\n";
 
    if (count($order->payments) > 0)
    {
        // display each payment for this order
        foreach ($order->payments as $payment)
        {
            echo "  payment #$payment->id of $$payment->amount by " .
            $payment->person->name . "\n";
        }
    }
    else
        echo "  no payments\n";
 
    echo "\n";
}
 
// display summary of all payments made by Tito and Jax
$conditions = array(
    'conditions' => array('id IN(?)',array($tito->id,$jax->id)),
    'order'      => 'name desc');
 
foreach (Person::all($conditions) as $person)
{
    $n = count($person->payments);
    $total = array_sum(ActiveRecord\collect($person->payments,'amount'));
    echo "$person->name made $n payments for a total of $$total\n\n";
}
 
// using order has_many people through payments with options
//
// array('people',
//    'through' => 'payments',
//    'select' => 'people.*, payments.amount',
//    'conditions' => 'payments.amount < 200'));
//
// this means our people in the loop below also has the payment information since
// it is part of an inner join we will only see 2 of the people instead of 3
// because 1 of the payments is greater than 200
$order = Order::find($pokemon->id);
echo "Order #$order->id for $order->item_name ($$order->price + $$order->tax tax)\n";
 
foreach ($order->people as $person)
    echo "  payment of $$person->amount by " . $person->name . "\n";
?>

The orders example should produce the following output:

[FAILED] saving order Freebie: Price must be greater than 0, Tax must be greater than 0

Tito has 2 orders for: Live Pokemon, Lump of Coal

Order #3 for Live Pokemon ($6999.99 + $315 tax) ordered by Tito
  payment #4 of $1.99 by Tito
  payment #5 of $4999.5 by Tito
  payment #6 of $2.5 by Jax

Order #4 for Lump of Coal ($100 + $4.5 tax) ordered by Tito
  no payments

Tito made 2 payments for a total of $5001.49

Jax made 1 payments for a total of $2.5

Order #3 for Live Pokemon ($6999.99 + $315 tax)
  payment of $2.50 by Jax
  payment of $1.99 by Tito

EXTENDED CONFIGURATION

Here's one more example that is basically the same as the first, but the point here is to display some of the ways to extend configuration (while remaining close to convention).

class Book extends ActiveRecord\Model
{
    // explicit table name since our table is not "books"
    static $table_name = 'simple_book';
 
    // explicit pk since our pk is not "id"
    static $primary_key = 'book_id';
 
    // explicit connection name since we always want production with this model
    static $connection = 'production';
 
    // explicit database name will generate sql like so => db.table_name
    static $db = 'test';
}
 
// ActiveRecord allows the use of multiple connections
$connections = array(
    'development' => 'mysql://test:test@127.0.0.1/development',
    'production' => 'mysql://test:test@127.0.0.1/production'
);
 
// initialize ActiveRecord
ActiveRecord\Config::initialize(function($cfg) use ($connections)
{
    $cfg->set_model_directory('.');
    $cfg->set_connections($connections);
});
 
print_r(Book::first()->attributes());
 

Simplicity

From the configuration, all the way to a serious-business example, ActiveRecord takes care of the heavy-lifting and the gritty details for you. This allows you, the developer, to focus more on business logic and complex code instead of composing custom SQL queries or designing ways to handle your data. Because we have embraced a convention over configuration philosophy, using our library is not painful. The conventions are easy to remember which will also contribute to stream-lining your productivity as a developer.

Where can you find ActiveRecord?

Again, the code is hosted on http://github.com/kla/php-activerecord/. We also have a website which will be available in the near future. We hope to have all the necessary content such as: tutorials, screencasts, and documentation.