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.
Thank you for publishing these tools! This project has been a huge help to our development efforts. We’ve been doing seo website design and development at industryforge.com for a long time, and love the efficiencies we gain from using your tools. We now have this code running in multiple production environments. We would love to contribute if desired.
That’s good to hear. You’re welcome to submit patches by forking the project on github or just emailing us.
First and foremost – great work guys. I’ve been using this in a production environment (with a few headaches), but there’s a few things I thought I might offer in terms of what I think the library should/shouldn’t have.
I think you’ve made a blooper with the design decision to force standards in PHP. With a library like Rails, it’s easy to do because developers are either starting new projects with it, or are working on projects that already use it. I’ve had quite a few problems trying to get the library working with one of our legacy systems. I got there – but not without some changes. First and foremost is PHPActiveRecord’s use of converting field names – this is a problem as all our field names have a capslocked suffix (don’t ask).
Secondly, this was also a problem when reporting errors – as the library tried to humanize the field names, so where we had fields like price_ADV, in the error – it would show up as “price a d v”, as it stuck to it’s own naming conventions.
I’m sure for most cases, the library isn’t going to be an issue, but I think considering that it’s a new library, and a lot of people are going to want to drop it in as a solution to easy database connectivity, forcing standards may not work out as you had hoped.
Keep up the good work
PS – is there any way for us to contribute directly?
Hey Kirk, if you could provide more specifics on the headaches you mentioned I can see if I can address them since more than likely other people have run across the same things and I’d like to get it fixed. Just drop me an email.
We would definitely like for people to contribute. Just fork the project on github or you can email patches to me.