View Source

<ac:macro ac:name="unmigrated-inline-wiki-markup"><ac:plain-text-body><![CDATA[{zone-template-instance:ZFPROP:Proposal Zone Template}
{zone-data:component-name}
Zend\Db\ActiveRecord
{zone-data}

{zone-data:proposer-list}
[Arthur Bodera|mailto:abodera@gmail.com]
{zone-data}

{zone-data:revision}
0.2 - 19 July 2011: Use cases, description, requirements. It's looking good!
0.1 - 19 July 2011: Initial Draft.
{zone-data}

{zone-data:liaison}
TBD
{zone-data}

{zone-data:overview}
{info:title=Working classes and tests are at https://github.com/Thinkscape/zf2/branches/ActiveRecord}
This component is an easy to use [ActiveRecord pattern|http://martinfowler.com/eaaCatalog/activeRecord.html] implementation for ZF.

I believe it had a long time coming, as there were some small flame-wars around this subject. There was some confusion and there were people thinking that Zend_Table [was an ActiveRecord implementation|http://www.armando.ws/2008/03/using-activerecord-in-the-zend-framework/]. [Zend\Db\Table|http://framework.zend.com/manual/en/zend.db.table.html] implementation has been created specifically to implement [Table Data Gateway|http://www.martinfowler.com/eaaCatalog/tableDataGateway.html] and [Row Data Gateway|http://www.martinfowler.com/eaaCatalog/rowDataGateway.html] patterns, which are not the same as ActiveRecord. It [has been stated|http://karwin.blogspot.com/2008/05/activerecord-does-not-suck.html] several times that [ActiveRecord pattern|http://martinfowler.com/eaaCatalog/activeRecord.html] is not row or table gateway, nor [is it an ORM|http://kore-nordmann.de/blog/why_active_record_sucks.html].

As per [Martin Fowler summary|http://martinfowler.com/eaaCatalog/activeRecord.html]:
bq. *ActiveRecord* is an object that wraps a row in a database table or view, encapsulates the database access, and adds domain logic on that data.

It is a very convenient, easy to use, easy to maintain pattern, because:
bq. An object carries both data and behavior ... Active Record uses the most obvious approach, putting data access logic in the domain object. This way all people know how to read and write their data to and from the database.

A quick summary:
* ActiveRecord class contains both business logic and data
* ActiveRecord is designed to be easy to use and understand
* ActiveRecord is a simplification of ORM
* ActiveRecord is +not+ Table Data Gateway
* ActiveRecord is +not+ Row Data Gateway
* ActiveRecord is +not+ an ORM (it might be a subset)
* ActiveRecord is +not+ a model, but can be a base for one.

\\
\\
{info:title=This component was created for ZF2 only.

If you need a ZF1 version, vote below}
{vote:title=Should ActiveRecord be also available for ZF1 ?}Yes, please (ZF1 is important)
No need (ZF2 is the future)
I don't care
{vote}

{zone-data}

{zone-data:references}
* [Old (Archive) Zend_Db_ActiveRecord proposal|http://framework.zend.com/wiki/display/ZFPROP/Zend_Db_ActiveRecord]
* [PHPActiveRecord.org|http://www.phpactiverecord.org/] and [How to use it with ZF|http://www.edvanbeinum.com/how-to-use-php-activerecord-with-zend-framework]

* [Wikipedia: ActiveRecord|http://en.wikipedia.org/wiki/ActiveRecord]
* [Martin Fowler's Patterns of Enterprise Application Architecture|http://martinfowler.com/eaaCatalog/activeRecord.html]
* [Bill Carvin post about what is ActiveRecord about|http://karwin.blogspot.com/2008/05/activerecord-does-not-suck.html]
* [Kore Nordmann - Why ActiveRecord sucks|http://kore-nordmann.de/blog/why_active_record_sucks.html] - it does not :)
* [Zend Framework ORM|http://code.google.com/p/zend-framework-orm/] - seems dead since Sep 26, 2008
* [Wikipedia: ORM|http://en.wikipedia.org/wiki/Object-relational_mapping]
{zone-data}

{zone-data:requirements}
* This component *will* require PHP 5.3+
* This component *will* be lightweight, modular and extendable
* This component *will* be ease-of-use oriented (AKA fun to use ;) )
* This component *will* be performance oriented
** Lazy initializing {color:gray} (flywheel, deferred init){color}
** Lazy updates (and lazy metadata loading)
** Lazy loading {color:gray} ("load on demand", "deferred load"){color}
** Bulk loading {color:gray} ("range loading", "set loading"){color}
** Eager loading {color:gray} ("read-ahead", implicit join loading){color}
** Meta-data hinting
** Caching of data and meta-data

* This component *will* support dependencies (associations)
** one-to-many {color:gray}(has many of ... ){color}
** one-to-one {color:gray}(has one of ... ){color}
** many-to-many
** reverse one-to-many {color:gray}(belongs to ...){color}
** one-to-many of self {color:gray}(has many of self){color}

* This component *will* support persistence out-of-the-box
* This component *will* return query results in smart, efficient, iterable Collections of ActiveRecords.
* This component *will* be prepared for advanced setups:
** Database sharding {color:gray}(clustering, distributed db){color}
** Master-slave databases {color:gray}(read-only, write-only, etc.){color}
** Server load-balancing {color:gray}(round-robin, selection logic, etc.){color}
** Software database failover {color:gray}(if db goes down, connect to another in the pool){color}
** Multiple caches {color:gray}(read/write, local/remote etc.){color}

* This component *will* will use [Zend\Db|http://framework.zend.com/manual/en/zend.db.html] components for connecting and querying databases
* This component *will* be cross-platform and db brand indepenent (because of the above)
* This component *will NOT* conflict with developer-implemented business logic (a Model)
* This component *will NOT* use Zend\Db\Table.
* This component *will NOT* replace or augment Zend\Db\Table behavior.
* This component *will NOT* try to be a full blown ORM (use Propel or Doctrine for that)
* This component *will NOT* use code generation (it will instead depend on subclassing, configuration and developer-oriented efforts)
{zone-data}

{zone-data:dependencies}
* Zend\Cache\*
* Zend\Db\Adapter\*
* Zend\Db\Select\*
* Zend\Db\Statement\*
* Zend\Filter\*
{zone-data}

{zone-data:operation}
In order to use ActiveRecord, developer creates a subclass extending {{Zend\Db\ActiveRecord\AbstractActiveRecord}}. Newly created class will work out-of-the box with some defaults. There are number of configuration options available for subclasses, including _table name_, _dependencies_, _primary key_ column name, _auto table name_ resolver etc. In order to configure and fine-tune the class, one has to override protected static properties, such as {{$_dbTable}} or {{$_pk}}.

By default, AbstractActiveRecord determines db table name from subclass name (i.e. {{Application\Model\User}} becomes table _user_). It fetches default db adapter from {{Zend\Db\Table\Table::getDefaultAdapter()}} and loads column list from the database. One can create an object instance with standard constructor ({{$user = new Application\Model\User();}} and assign values ({{$user->name = "Bob";}}). By calling {{$user->save();}} a new database record is inserted.

Existing records can be fetched from database by using a {{factory()}} or {{findById()}} static methods. For example assuming _Bob_ has an id of 1, we can instantiate his user object with {{$user = Application\Model\User::factory(1);}}. At this stage nothing is loaded from the database, until we try fetch object properties i.e. {{echo $user->name;}}. If there is no such user with id 1, a {{NotFoundException}} will be thrown.
The second style of record fetching, {{findById()}}, will try to find the record in the database before instantiating. For example {{$user = Application\Model\User::findById(1);}} will send a {{SELECT ... WHERE id=1}} to the database. If no such record can be found, {{false}} is returned instead of throwing an exception.

All ActiveRecord-specific methods are protected and prefixed with _ to avoid conflicts with user-defined business logic. There is a minimal number of non-prefixed public methods that can be overridden as needed (for example {{save()}} or {{delete()}}).
{zone-data}

{zone-data:milestones}
* Milestone 1: {color:green}*\[DONE\]* Create first working version of the component {color}
* Milestone 2: {color:green}*\[DONE\]* Create test suite {color}
* Milestone 3: {color:green}*\[DONE\]* Create first version of a proposal{color}
* Milestone 4: Implement associations and create test suite for that
* Milestone 5: Write advanced examples (sharding, clustering, master-slave etc.)
* Milestone 6: Write a tutorial.
{zone-data}

{zone-data:class-list}
*namespace* Zend\Db\ActiveRecord
* AbstractActiveRecord
* Collection
* CollectionIterator
* Exception\Exception
* Exception\BadMethodCallException
* Exception\NotFoundException
* Exception\UndefinedPropertyException
{zone-data}

{zone-data:use-cases}
{composition-setup}
{info:title=Most use cases are already implemented! Download code from https://github.com/Thinkscape/zf2/branches/ActiveRecord}
There are additional features you can look at in the test suite: {{tests/Zend/Db/ActiveRecord/ActiveRecordTest.php}}

h3. Basic usage
{deck:id=ZendDbActiveRecordUC1}
{card:label=UC-01}
*Basic ActiveRecord class with no configuration*
{code}<?php
namespace Application\Model;
class User extends \Zend\Db\ActiveRecord\AbstractActiveRecord {}

$bob = new User();
$user->name = "Bob"; // DESCRIBE TABLE user
$user->save(); // INSERT INTO user SET name="Bob"

$frank = User::findById(35); //SELECT * FROM user WHERE id = 35;
echo "Frank's age is: " . $frank->age;
{code}
{card}

{card:label=UC-02}
*Explicitly setting table name*
{code}<?php
namespace Application\Model;
class User extends \Zend\Db\ActiveRecord\AbstractActiveRecord {
protected static $_dbTable = 'app_users';
}

$bob = new User();
$user->name = "Bob"; // DESCRIBE TABLE app_users
$user->save(); // INSERT INTO app_users SET name="Bob"
{code}
{card}

{card:label=UC-03}
*Automatic table names - table name inflection*
{code}
<?php
namespace Application\Model;
abstract class AbstractModel extends \Zend\Db\ActiveRecord\AbstractActiveRecord {
protected static _determineDbTable(){
$dbTable = strtolower(get_called_class()); // get current class name
$dbTable = substr( // strip namespace
$dbTable,
strripos($dbTable,'\\') + 1
);
$dbTable = "app_" . $dbTable ."s"; // add prefix and suffix
return $dbTable;
}
}

class User extends AbstractModel {};
$bob = User::findById(1); // SELECT * FROM app_users

class Department extends AbstractModel {}
$sales = new Department();
$sales->name = "Sales department"; // DESCRIBE TABLE app_departments
$sales->save(); // INSERT INTO app_departments SET name="Sales department"

class Client extends AbstractModel {
protected static $_dbTable = "app_users"; // this class has a hardcoded table name
}
$acme = Client::findById(72); // SELECT * FROM app_users WHERE id = 72;
{code}
{card}
{card:label=UC-04}
*Supplying default DB adapter for all ActiveRecords and subclass*
{code}
<?php
// create mysqli db adapter
$db = \Zend\Db\Db::factory('mysqli',array('host' => '127.0.0.1', 'dbname' => 'app'));

// set it as default for all active records
\Zend\Db\ActiveRecord\AbstractActiveRecord::setDefaultDb($db);

// create some classes
namespace Application\Model;
class User extends \Zend\Db\ActiveRecord\AbstractActiveRecord {}
class Department extends \Zend\Db\ActiveRecord\AbstractActiveRecord {}
class Client extends \Zend\Db\ActiveRecord\AbstractActiveRecord {}

// create another db adapter (second database)
$clientsDb = \Zend\Db\Db::factory('mysqli',array('host' => '10.0.22.1', 'dbname' => 'clients'));

// Client class will use the second database adapter
Client::setDefaultDb($clientsDb);
{code}
{card}

{card:label=UC-05}
*Setting default DB as string - fetching from Registry*
{code}
<?php
// create mysqli db adapter
$db = \Zend\Db\Db::factory('mysqli',array('host' => '127.0.0.1', 'dbname' => 'app'));

// store it in registry
\Zend\Registry::set('maindb', $db);

// create some classes
namespace Application\Model;
class User extends \Zend\Db\ActiveRecord\AbstractActiveRecord
{
protected static $_defaultDb = 'maindb';
}

// create another db adapter (second database) and store it in registr
$clientsDb = \Zend\Db\Db::factory('mysqli',array('host' => '10.0.22.1', 'dbname' => 'clients'));

// store it in registry
\Zend\Registry::set('clientsdb', $db);

// Create client class that will use the second database adapter
class Client extends \Zend\Db\ActiveRecord\AbstractActiveRecord {
protected static $_defaultDb = 'clientsdb';
}
{code}
{card}

{card:label=UC-06}
*ActiveRecord persistence*
{code}
<?php
namespace Application\Model;
class User extends \Zend\Db\ActiveRecord\AbstractActiveRecord
{
protected static $_persistent = true; // this is on by default
}

// create new user object
$user = new User();
$user->name = "Bob Unique";
$user->save(); // INSERT INTO user SET name="Bob Unique"
$id = $user->id;

// create another instance of the same user
$user2 = User::findById($id);
assert( $user2 === $user ); // true, this is the same object

// find all users named "Bob Unique"
$search = User::findByName("Bob Unique");
$user3 = $search[0];
assert( $user3 === $user ); // true, this is still the same object

// change bob's name
$user->name = "Bob Unique The Third";

// check if it equals across every variable
assert( $user->name, $user2->name ); // true
assert( $user->name, $user3->name ); // true

{code}
{card}


{deck}


h3. Finding records (queries)
{deck:id=ZendDbActiveRecordUC2}
{card:label=UC-Q01: basic}
{code}<?php
/*
* CREATE TABLE user ( id INT, name VARCHAR(255), age INT, salary DOUBLE )
*/
namespace Application\Model;
class User extends \Zend\Db\ActiveRecord\AbstractActiveRecord {}

// find all users
$allUsers = User::findAll(); // SELECT * FROM user

// find users named tom
$search = User::findAll(array( // SELECT * FROM user WHERE name = "Tom"
'name' => 'Tom'
));

// find users named Bob at the age of 25
$search = User::findAll(array( // SELECT * FROM user WHERE name = "Bob" AND age = 25
'name' => 'Bob',
'age' => 25
));

// find users with names Mary, Mark and John
$search = User::findAll(array( // SELECT * FROM user WHERE name IN ('Mary','Mark','John')
'name' => array('Mary','Mark','John'),
));
{code}
{card}


{card:label=UC-Q02: query expressions}
{code}<?php
/*
* CREATE TABLE user ( id INT, name VARCHAR(255), age INT, salary DOUBLE, departmentId INT NULL )
*/
namespace Application\Model;
class User extends \Zend\Db\ActiveRecord\AbstractActiveRecord {}

// find all users with names starting with letter A
$search = User::findAll(array( // SELECT * FROM user WHERE name LIKE 'A%'
array('name', 'like', 'A%')
));

// find all users with age above 50
$search = User::findAll(array( // SELECT * FROM user WHERE age > 50
array('age', 'gt', 50)
));

// find all users with salary less or equal to 1000
$search = User::findAll(array( // SELECT * FROM user WHERE salary <= 1000
array('salary', '<=', 1000)
));

// find all users with names not ending with "nes"
// and with no department assigned
$search = User::findAll(array( // SELECT * FROM user
array('name', 'notLike', '%nes'), // WHERE name NOT LIKE '%nes'
array('departmentId', 'isnull', null), // AND departmentId IS NULL
));

// find users with names Mary, Mark and John that do
// not have salary information on file
$search = User::findAll(array( // SELECT * FROM user
('name', 'eq', array('Mary','Mark','John') ), // WHERE name IN ('Mary','Mark','John')
('salary, 'isempty', ''), // AND (salary IS NULL OR salary = "" OR salary = 0)
));
{code}
{card}


{card:label=UC-Q03: SQL queries}
{code}<?php
/*
* CREATE TABLE user ( id INT, name VARCHAR(255), age INT, salary DOUBLE, departmentId INT NULL )
*/
namespace Application\Model;
class User extends \Zend\Db\ActiveRecord\AbstractActiveRecord {}

// find all users with names starting with letter A
$search = User::findAll(array( // SELECT * FROM user WHERE name LIKE 'A%'
'name LIKE ?' => 'A%'
));

// find all users with age above 50
$search = User::findAll(array( // SELECT * FROM user WHERE age > 50
'age > ? ' => 50
));

// find users with salary between 1000 and 2000
$search = User::findAll(array( // SELECT * FROM user WHERE salary BETWEEN 1000 AND 2000
'salary BETWEEN ? AND ?' => array(1000, 2000)
));

// find users with even id
$search = User::findAll(array( // SELECT * FROM user WHERE MOD(id,2) = 0
'MOD(id,2) = ?' => 0
));
{code}
{card}
{deck}


h3. Associations
{deck:id=ZendDbActiveRecordUC2}
{card:label=UC-AS01}
*one-to-many (using defaults and built-in inflection)*
{code}<?php
/*
* CREATE TABLE user (
* id INT,
* name VARCHAR(255),
* departmentId INT
* );
*
* CREATE TABLE department (
* id INT,
* name VARCHAR(255)
* );
*/
namespace Application\Model;
class User extends \Zend\Db\ActiveRecord\AbstractActiveRecord {}

class Department extends \Zend\Db\ActiveRecord\AbstractActiveRecord {
protected static $_hasMany = array(
'users' => array('User') // ->users is a collection of User objects which
); // are referenced by "departmentId" column
}

// load sales department
$sales = Department::findOneByName('sales'); // SELECT * FROM department WHERE name = "Sales"

// display all users from sales department
foreach($sales->users as $user){ // SELECT * FROM user WHERE departmentId = 15
echo $user->name . "\n";
}
{code}
{card}


{card:label=UC-AS02}
*one-to-many (custom field and class names)*
{code}<?php
/*
* CREATE TABLE user (
* id INT,
* name VARCHAR(255),
* assigned_to INT
* );
*
* CREATE TABLE team (
* id INT,
* name VARCHAR(255)
* );
*/
namespace Application\Model;
class User extends \Zend\Db\ActiveRecord\AbstractActiveRecord {}

class Team extends \Zend\Db\ActiveRecord\AbstractActiveRecord {
protected static $_hasMany = array(
'members' => array('User', 'assigned_to') // ->members is a collection of User objects, which
); // are referenced by "assigned_to" column.
}

// load team with name "innovative"
$team = Team::findOneByName('innovative'); // SELECT * FROM team WHERE name = "innovative"

// display all members names
foreach($team->members as $user){ // SELECT * FROM user WHERE assigned_to = 15
echo $user->name . "\n";
}
{code}
{card}


{card:label=UC-AS03}
*belongs to (reversed one-to-many)*
{code}<?php
/*
* CREATE TABLE user (
* id INT,
* name VARCHAR(255),
* departmentId INT
* );
*
* CREATE TABLE department (
* id INT,
* name VARCHAR(255)
* );
*/
namespace Application\Model;
class User extends \Zend\Db\ActiveRecord\AbstractActiveRecord {
protected static $_belongsTo = array(
'department' => array('Department', 'departmentId') // ->department is an associated object of class
); // Department with id matching "departmentId"
}

class Department extends \Zend\Db\ActiveRecord\AbstractActiveRecord {
protected static $_hasMany = array(
'users' => array('User') // ->users is a collection of User objects which
); // are referenced by "departmentId" column
}

// get all users
$users = User::findAll(); // SELECT * FROM user

// display user departments
foreach($user as $user){
echo "User name: " . $user->name . "\n";
echo "Department: ". $user->department->name ."\n"; // SELECT * FROM department WHERE id = ...
}
{code}
{card}

{card:label=UC-AS04}
*belongs to self (parent)*
{code}<?php
/*
* CREATE TABLE category (
* id INT,
* name VARCHAR(255),
* parentId INT NULL
* );
*/
namespace Application\Model;
class Category extends \Zend\Db\ActiveRecord\AbstractActiveRecord {
protected static $_belongsTo = array(
'parent' => array(__CLASS__, 'parentId') // ->parent points to parent Category class
);
protected static $_hasMany = array(
'children' => array(__CLASS__, 'parentId') // ->children points to a collection of Category
); // objects associated by "parentId" column
}

// fetch category with id 15
$category = Category::findById(15); // SELECT * FROM category WHERE id = 15

// display it's parent
echo "Category name: " . $category->name . "\n";
echo "Parent name: " . $category->parent->name . "\n"; // SELECT * FROM category WHERE id = 17


// get main categories (with no parents)
$mainCats = Category::findAll(array( // SELECT * FROM category WHERE parentId IS NULL
array('parentId', 'isNull', '')
));

// display main categories with their children
foreach($mainCats as $category){
echo "Main category: " . $category->name . "\n";
echo "Children: \n";

foreach($category->children as $cat){ // SELECT * FROM category WHERE parentId = ...
echo " - " . $cat->name . "\n";
}
}
{code}
{card}

{deck}

{zone-data}

{zone-data:skeletons}
_Skeletons are good for Halloween \;\-\)_
This component is alive and kicking. Download from [https://github.com/Thinkscape/zf2/branches/ActiveRecord]
{zone-data}

{zone-template-instance}]]></ac:plain-text-body></ac:macro>