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.
Sorry about the previous posts–I just found the answer: PHP 5.3 added namespaces (http://us.php.net/manual/en/language.namespaces.rationale.php) and uses the backslash character as the namespace separator. You must have PHP 5.3 (I should have read more carefully) to use PHP ActiveRecord.
Somehow, the line of code in question were removed when I posted previously. Here’s the line throwing the error:
ActiveRecord\Config::initialize(function($cfg)
Any idea why the ActiveRecord\Config is causing problems for me? I thought it might have something to do with magic_quotes, but I turned them off in production env with no luck. Everything works fine on my local system (php 5.3.0), but when I upload to the GoDaddy hosting env (php 5.2.8) I get an error. Here’s the error I get:
Parse error: syntax error, unexpected T_STRING in /my_php_script.php on line 3
And here’s the code:
set_model_directory(’classes’);
$cfg->set_connections(array(’development’ => ‘mysql://’.DB_USER.’:’.DB_PASS.’@’.DB_HOST.’/’.DB_NAME));
});
?>
This implementation of ActiveRecord requires PHP 5.3 and above. Namespaces are only available since that release, amongst other things, like anonymous functions.
Just wanted to let you know, yes, the field information for each table is cached in the Table class.
what about caching the column meta data from “SHOW COLUMNS FROM ” ?
I mean caching column meta on server using apc memory functions: apc_store and apc_fetch. That’s what I’m doing in my “Active RecordLite” slimmed down version of your Active Record.
You’d have to use the conditions option:
Foo::all(array(’conditions’ => array(”field_a=? and (field_b=? or field_c=?)”, ‘abc’, 0, 1)));
or a slightly simpler form if you don’t have a need to bind any parameters:
Foo::all(”field_a=’abc’ and (field_b=0 or field_c=1)”);
Hope that helps.
Hello,
I took a look at your examples, but I can’t find something like this.
How do you solve a sql query like this:
select *
from table_foo
where field_a = ‘abc’
and ( field_b = 0
or field_c = 1 );
Thanks and greetings,
Urkman
This is great! Very much like an AR implementation I’m working on lately.
Some of the lifecycle is identical Although I “emulate” static constructors instead of using hashtables so that I can add logic during the calls to add columns & define relationships.
I’ll be watching this project very closely.
Ohhh. I’ve seen, no PostgreSQL
So, no AR for me…
Urkman
Oh, yeah we will have postgres eventually but it might be a couple of months before we can get to it. We’ve just been working on documentation and the new site for it so we haven’t been adding new features lately.
To answer your other question. It’s unlikely we’re gonna have any tools to generate models. The goal is not to have a need for things like that.
Hello Kien,
thanks for your answer. I think I will give it a try
But you did not answer ny second question:
How about a Model build tool from an existing DB? Do you have something like this in your To-Do? Like the doctrine build tools.
Thanks again,
Urkman
The whole point of php-activerecord is that you don’t NEED to generate models. You write one simple file with a class extending ActiveRecord\Model and the name of the class automatically inflects from say “Book” to the table name “books”. (And you can explicitly set the table name if you don’t like inflection.) Also, I assume it automatically pulls the describe table information (cacheable?) so you don’t need any configuration file with columns or anything.
Just like Rails’ ActiveRecord::Base
BTW guys behind php-activerecord, I think you’re doing a really good thing here. I absolutely hate Zend_Db, and I hope to one day replace it with your model