Lazy Loading in Model layer (cakePHP style)

CakePHP is not supporting Lazy Loading yet. Before if starts, I asked myself “how would I do that?”. My solution is based on PHP5, so I imagine that supporting PHP4 in 1.x branch is the reason of no-Lazy-Loading. Anyway – I wanted to share my ideas.

Let’s start with basics. The opposite of lazy loading is eager loading, which means “Load anything that can be needed. Ever.” In that approach model layer in Cake could look like that (it’s very simplified, the only relationship is belongsTo):

Eager Loading


class Model {
	function find(){

		$r = new ReflectionClass($this);
		print "calling ".$r->getName()."::find($conditions)";

		$query = "Select * from `".strtolower($r->getName())."s` ".
					"as `".$r->getName()."`";

		var_dump($query);

		//return query result
	}

	function __construct(){
		$r = new ReflectionClass($this);
		print("creating ".$r->getName()."
"); if(isset($this->belongsTo) && is_array($this->belongsTo)){ foreach($this->belongsTo as $related){ $this->{$related} = new $related(); } } } } class User extends Model{ protected $belongsTo = array("Group"); } class Group extends Model{ } $user = new User(); $user->find(); echo "
"; $user->Group->find();

One could see that Group object was initialized long before it is used:

creating User
creating Group
calling User::find()

string 'Select * from `users` as `User`' (length=31)


calling Group::find()

string 'Select * from `groups` as `Group`' (length=33)

Even more – it’s being initialized even if we don’t call any method in this object (comment $user->Group->find(); to see that).

Lazy Loading

class Model {
	function find(){

		$r = new ReflectionClass($this);
		print "calling ".$r->getName()."::find($conditions)";

		$query = "Select * from `".strtolower($r->getName())."s` ".
					"as `".$r->getName()."`";

		var_dump($query);

		//return query result
	}

	function __construct(){
		$r = new ReflectionClass($this);
		print("creating ".$r->getName()."
"); } function __get($property){ if(!isset($this->{$property})) { $this->{$property} = new $property(); } if(in_array($property, $this->belongsTo)){ return $this->{$property}; }else{ $r = new ReflectionClass($this); throw new Exception( "$property not related with {$r->getName()}" ); } } } class User extends Model{ protected $belongsTo = array("Group"); } class Group extends Model{ } $user = new User(); $user->find(); echo "
"; $user->Group->find();

When using Lazy Loading pattern, Group object isn’t created untill we need it. Magic method __get() is used to make that possible. __get() is called whenever we try to access an attribute in object that doesn’t exists.

But there’s question of searching in model, so it returns appropriate data, ie. var_dump($user->find());

array
  0 => 
    array
      'User' => 
        array
          'id' => int 1
          'name' => string 'Frank' (length=6)
      'Group' => 
        array
          'id' => int 1
          'name' => string 'Admin' (length=5)
  1 => 
    array
      'User' => 
        array
          'id' => int 2
          'name' => string 'John' (length=5)
      'Group' => 
        array
          'id' => int 10
          'name' => string 'User' (length=4)

We know that belongsTo means that User should have field qroup_id, so the query could look like this:

Select User.id, User.name, Group.id, Group.name from users as User
Inner Join groups as Group on (User.group_id = Group.id)

(Now CakePHP, since 1.2 AFAIR, has Containable behavior, but it depends on loaded models).

Lets keep cake’s convention and assume, that we’ll pass informations about related elements through $contain variable.

function find($contain=null)

Now I want to say: object $user, please return me all users from database and groups they’re in:

$user =  new User();

$user->find(
	array(
		"belongsTo" => array($user->Group)
	)
);

Lets update find() method:

	function find($contain=null){

		$r = new ReflectionClass($this);
		print "calling ".$r->getName()."::find($conditions)";

		$query = "Select * from `".strtolower($r->getName())."s` ".
					"as `".$r->getName()."` \n";


		if(!empty($contain) && is_array($contain["belongsTo"])){
			foreach($contain["belongsTo"] as $belongsToModel){
				$query .= $belongsToModel->getBelongsToJoin($this);
			}
		}


		var_dump($query);

		//return query result
	}

which now assumes that model has method, which can create the join part of query:

	function getBelongsToJoin($obj){
		$owner_reflection = new ReflectionObject($this);
		$obj_reflection = new ReflectionObject($obj);
		return "INNER JOIN `".strtolower($owner_reflection->getName())."s` 
					as `".$owner_reflection->getName()."`
						ON (`".$owner_reflection->getName()."`.`id` = `".$obj_reflection->getName()."`.`".strtolower($owner_reflection->getName())."_id`)
					";
	}

it is ugly because we keep using reflections on and on. To make that prettier we could implements getName() method in Model class.

Anyway. Even in case of find which considers relations, we can use Lazy Loading pattern.

As a bonus, code sample with more related models and example of use (and small refactorin – when I write about Lazy Loading it looked silly when I was loading ReflectionClasss fifteen times ;)):


class ModelException extends Exception{}

class Model {

	protected $reflection = null;

	function getName(){
		if(is_null($this->reflection)){
			$this->reflection= new ReflectionClass($this);
		}
		return $this->reflection->getName();
	}

	function getTableName(){
		return strtolower( $this->getName() . "s" );
	}

	function getFKName(){
		return strtolower( $this->getName() . "_id");
	}

	function getBelongsToJoin($obj){
		$owner_reflection = new ReflectionObject($this);
		$obj_reflection = new ReflectionObject($obj);
		return "INNER JOIN `".$this->getTableName()."` 
					as `".$this->getName()."`
						ON (`".$this->getName()."`.`id` = `".$obj->getName()."`.`".$this->getFKName()."`)
					";
	}

	function find($contain=null){

		$r = new ReflectionClass($this);
		print "calling ".$r->getName()."::find($conditions)";

		$query = "\nSelect * from `".strtolower($r->getName())."s` ".
					"as `".$r->getName()."` \n";


		if(!empty($contain) && is_array($contain["belongsTo"])){
			foreach($contain["belongsTo"] as $belongsToModel){
				$query .= $belongsToModel->getBelongsToJoin($this);
			}
		}


		var_dump($query);

		//return query result
	}

	function __construct(){
		$r = new ReflectionClass($this);
		print("creating ".$r->getName()."
"); } function __get($property){ if(!isset($this->{$property})) { $this->{$property} = new $property(); } if(in_array($property, $this->belongsTo)){ return $this->{$property}; }else{ $r = new ReflectionClass($this); throw new ModelException( "$property not related with {$r->getName()}" ); } } } class User extends Model{ protected $belongsTo = array("Group", "Unit", "Company"); } class Group extends Model{ } class Unit extends Model{} class Company extends Model{} $user = new User(); $user->find( array( "belongsTo" => array($user->Unit, $user->Company) ) );

What You think about that?

Share Button

One thought on “Lazy Loading in Model layer (cakePHP style)

Leave a Reply

Your email address will not be published. Required fields are marked *