PHP ActiveRecord Available for Beta Testing

by Kien La on May 19, 2009

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.

35 comments

Hello..
I am impressed to see this library. I have downloaded that too.
but I am facing one problem.
when I try to execute “orders.php” it throws me an error like..

Warning: Unexpected character in input: ‘\’ (ASCII=92) state=1 in C:\wamp\www\kla-php-activerecord-9dda73da60e1751ad524f8dfe0b8cd675b45530f\kla-php-activerecord-9dda73da60e1751ad524f8dfe0b8cd675b45530f\examples\orders\orders.php on line 5

can any one help me?

by hussain on July 11, 2009 at 8:48 am. Reply #

Looks like it’s erroring out on the namespace character. Are you running PHP 5.3? This only works on 5.3

by Kien La on July 11, 2009 at 4:00 pm. #

Nice work!

by mp on June 20, 2009 at 5:46 pm. Reply #

George, thanks for looking at our project. Let me address some of your concerns.

I’m not sure what you mean by the lambda function/closure piece. What you are passing to Config::initialize is a closure. If you look at the config class it looks like this:
Config::initialize(Closure $initializer)

Also, if you take a look at our README on github, it shows that you can configure ActiveRecord w/o the 5.3 closure by doing:
$cfg = Config::instance();
$cfg->set_model_directory()…etc

We may consider switching to PDO as we are currently doing some performance profiling.

Finally, a print_r(Order::all()) would not result in an infinite loop. the all() function tells it to return all records from the database in an array of Order objects. The print piece would just print out that array.

by Jacques Fuentes on May 26, 2009 at 12:09 pm. Reply #

Overall seems like a solid idea, few points I do not personally agree on.

I don’t like the lambda function you use to initialize the connection, not very straight forward and not actually needed since you could feed it into a closure yourself in config::initialize.

By using PDO you could have skipped writing adapters ^_^;

Without having extensively checkout out the whole thingie, just a quick question, in your orders example, wouldn’t print_r(Order::all()); result in an infinite loop?

by George Antoniadis on May 25, 2009 at 6:32 pm. Reply #

Hi,

This looks great, if it’s stable/good enough I’ll be using this in my next project!

But one small thing, ActiveRecord.php starts with a short tag(<?) which isn’t enabled in all PHP configurations, please fix that ;)

by Jurpie on May 23, 2009 at 6:56 pm. Reply #

Jurpie, I thought I had went back and edited all of our files to not use the short open tag. I will make sure that none of our files use them.

by Jacques Fuentes on May 24, 2009 at 1:55 pm. #

looking great.

i’ve noticed a couple of 5.3 specific activerecord projects recently (FuzzyRecord is one of them) but this looks best to me, in terms of naming and coding conventions. will definitely take this for a spin.

best wishes for the project!

by aleks discodust on May 21, 2009 at 2:40 pm. Reply #

I did a ActiveResource class in php to consume restful rails resources that you may find interesting..

A bit of hacky code to get static method inheritance happening for PHP 5.1+, i haven’t got time to get the association working for resources yet but check it out

http://http://github.com/speedmax/activeresource-php

by taylor luk on May 21, 2009 at 5:44 am. Reply #

Yes, absolutely. The database specific code is all abstracted so its relatively easy to add support for new database types.

by Kien La on May 20, 2009 at 2:57 pm. Reply #

It looks good! Do you also plan to make use database abstraction ?

I mean, by letting the developer using mysql, postgre or oracle ?

by ben on May 20, 2009 at 1:47 pm. Reply #

[...] Find the latest here. [...]

by Derivante » PHP ActiveRecord with PHP 5.3 on May 20, 2009 at 2:34 am. Reply #

Leave your comment

Required.

Required. Not published.

If you have one.