CakePHP nie wspiera aktualnie leniwego ładowania obiektów. W prawdzie są jakieś skrypty, ale jeszcze żaden z nich nie chciał mi zadziałać. Zacząłem się zastanawiać: “Gdybym miał zaimplementować Lazy Loading w warstwie modelu, to jak bym to zrobił?” To co mi wyszło korzysta z rozwiązań PHP5, więc może wsparcie gałęzi 1.x Cake’a dla PHP4 implikuje brak tego wzorca?
Tak czy siak, podzielę się z Wami moimi pomysłami.
Zacznijmy od początków. Przeciwieństwem leniwego ładowania jest ładowanie gorliwe (lub zachłanne), to znaczy ładowanie na zapas wszystkiego co może okazać się potrzebne. Warstwa modelu w cake’u mogła by wyglądać tak (duże uproszeczenie, jedyna możliwa relacja to 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();
Widzimy, że obiekt Group jest inicjowany długo przed użyciem:
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)
Mało tego, ładuje się nawet jeśli nigdy nie wywołamy metody w tym obiekcie (zakomentuj $user->Group->find(); żeby się o tym przekonać).
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();
W tym rozwiązaniu obiekt Group nie zostanie stworzony, dopóki nie będziemy go potrzebować. Wykorzystana jest do tego magiczna metoda __get, która jest wywoływana wtedy, gdy próbujemy się odnieść do atrybutu obiektu, który nie istnieje.
ale pozostaje kwestia wyszukiwania w modelu tak, żeby zwrócił powiązane dane
np.: var_dump($user->find());
array
0 =>
array
'User' =>
array
'id' => int 1
'name' => string 'Franek' (length=6)
'Group' =>
array
'id' => int 1
'name' => string 'Admin' (length=5)
1 =>
array
'User' =>
array
'id' => int 2
'name' => string 'Janek' (length=5)
'Group' =>
array
'id' => int 10
'name' => string 'User' (length=4)
Wiemy, że belongsTo oznacza, że User powinien mieć pole (group_id), zatem query powinno wyglądać mniej więcej tak:
Select User.id, User.name, Group.id, Group.name from users as User
Inner Join groups as Group on (User.group_id = Group.id)
W tej chwili cakephp zawiera behavior containable, jednak on polega na załadowanych już modelach.
Zostańmy przy terminologii cake’a i przyjmijmy, że będziemy przekazywać informacje na temat powiązanych elementów w zmiennej $contain
function find($contain=null)
Teraz chciałbym powiedzieć: obiekcie $user zwróć mi wszystkich użytkowników z bazy wraz z informacją w jakich są grupach:
$user = new User();
$user->find(
array(
"belongsTo" => array($user->Group)
)
);
Zatem zaktualizuję metodę find:
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
}
która od teraz zakłada, że model posiada metodę, która potrafi utworzyć odpowiedniego joina:
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`)
";
}
to jest oczywiście bardzo brzydkie ze względu na refleksje. Metodę getName() można jednak zaimplementować w klasie Model.
Tak czy siak, w tym wypadku nawet w find()’zie uwzględniającym relacje można stosować Lazy Loading
Na koniec całość kodu, z większą ilością relacji i przykładem działania (i małym refactoringiem – skoro już mowa o lazy loading to głupio było ładować piętnaście razy ReflectionClass):
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)
)
);