Случилось так, что осваивая Zend Framework я решил написать компонент Zend_ActiveRecord по функциональности как можно более похожий на Rails. Подобное предложение возникало на комьюнити зенда, но давно не обновлялось, да при этом требовало php 5.3 из-за его __callStatic(). Меня этот факт не устроил, но необходимость вызова динамических методов класса модели как статичных по прежнему остается очень актуальной темой, я же попробовал разобраться с этим как раз на примере моей собственной ActiveRecord для зенда
Допустим у нас есть модель Post, которая представляет таблицу (либо сервис) записей в блоге.
Логичным и удобным было бы производить поиск как
Тем самым мы избавляемся от необходимости инстанцировать модель каждый раз как нам понадобится доступ к поисковым методам. На первый взгляд кажется что достаточно описать этот метод как статический, но! При вызове статического метода не вызывается конструктор, а именно в нем по логике должны быть реализованы основные функции для приведения модели в рабочее состояние. Например, одним из основных преимуществ паттерна ActiveRecord является целостность и гибкое использование зависимых моделей, i.e. ассоциаций, поэтому попробуем добавить, например, комментарии к постам и произведем поиск всех постов И связанных с ними комментариев. В Rails это бы выглядело так:
has_many и belongs_to в Rails представляют собой методы регистрации ассоциаций, по аналогии на php это должно выглядеть так:
Первое что бросается в глаза, так это то что мы должны использовать статические методы для объявления ассоциаций. Это необходимо делать потому, что на этапе вызова статического find у нас нет экземпляра класса, и вызов инициалайзера должен производится проводится вручную из find() а не из конструктора по умолчанию, что согласитесь неудобно, особенно если в теле инициализации использовать другие функции, которые по умолчанию подразумевают наличие экземпляра класса. Например
При статическом вызове это вызовет fatal error.
Существуют также ситуации когда нужно вызывать find из уже созданного экземпляра класса
Все это приводит к тому что в разные моменты жизненного цикла модели необходимо знать как был осуществлен вызов метода, как статический либо как то иначе. Выход был найден: подразумевать метод find как НЕ СТАТИЧЕСКИЙ по умолчанию, что не помешает нам вызывать его статически, а при статическом вызове создавать временный экземпляр класса. PHP сам подсказал удобный способ в одну строчку.
либо можно использовать обвязку
При таком вызове мы просто эмулируем в самом псевдостатическом методе действия, которые было бы неудобно использовать в остальном коде. Т.е. в данном случае вызов
и
абсолютно идентичны, но в последнем случае нам не требуется каждый раз производить создание экземпляра класса
Допустим у нас есть модель Post, которая представляет таблицу (либо сервис) записей в блоге.
class Post extends Zend_ActiveRecord_Abstract {}
Логичным и удобным было бы производить поиск как
Post::find('all');
Тем самым мы избавляемся от необходимости инстанцировать модель каждый раз как нам понадобится доступ к поисковым методам. На первый взгляд кажется что достаточно описать этот метод как статический, но! При вызове статического метода не вызывается конструктор, а именно в нем по логике должны быть реализованы основные функции для приведения модели в рабочее состояние. Например, одним из основных преимуществ паттерна ActiveRecord является целостность и гибкое использование зависимых моделей, i.e. ассоциаций, поэтому попробуем добавить, например, комментарии к постам и произведем поиск всех постов И связанных с ними комментариев. В Rails это бы выглядело так:
class Post < ActiveRecord::Base
has_many :comments
end
class Comment < ActiveRecord::Base
belongs_to :post
end
Post.find :all, :include=>:comments
has_many и belongs_to в Rails представляют собой методы регистрации ассоциаций, по аналогии на php это должно выглядеть так:
class Post extends Zend_ActiveRecord_Abstract {
// выделяем отдельную функцию для инициализации, т.к. в php
// нет дефолтного конструктора при статических вызовах
protected static function initialize() {
// comments — в множественном числе, как в Rails,
// этот параметр обрабатывается инфлектором, так что не смущаемся
self::has_many('comments');
}
}
class Comment extends Zend_ActiveRecord_Abstract {
protected static function initialize() {
self::belongs_to('post');
}
}
// производим поиск
Post::find(array('all','include'=>'comments'));
Первое что бросается в глаза, так это то что мы должны использовать статические методы для объявления ассоциаций. Это необходимо делать потому, что на этапе вызова статического find у нас нет экземпляра класса, и вызов инициалайзера должен производится проводится вручную из find() а не из конструктора по умолчанию, что согласитесь неудобно, особенно если в теле инициализации использовать другие функции, которые по умолчанию подразумевают наличие экземпляра класса. Например
class Post extends Zend_ActiveRecord_Abstract {
function initialize{
self::has_many('comments');
// для примера поставим дату инициализации объекта
$this->set_attribute('initialized_at',timestamp());
}
}
При статическом вызове это вызовет fatal error.
Существуют также ситуации когда нужно вызывать find из уже созданного экземпляра класса
// подгузка поста с id=1
$post = new Post(1);
// вызываем поиск чего-нибудь
$something = $post->find(array('all','conditions'=>array(......));
// либо подгрузка комментариев к посту, вызывается метод find модели Comment, уже созданной по умолчанию
$otherthing = $post->comments->find(array('all'));
Все это приводит к тому что в разные моменты жизненного цикла модели необходимо знать как был осуществлен вызов метода, как статический либо как то иначе. Выход был найден: подразумевать метод find как НЕ СТАТИЧЕСКИЙ по умолчанию, что не помешает нам вызывать его статически, а при статическом вызове создавать временный экземпляр класса. PHP сам подсказал удобный способ в одну строчку.
class Zend_ActiveRecord_Abstract {
public function find($options = array()) {
// определим тип вызова по наличию $this. В статическом контексте переменная не определена
if (!isset($this)) $This = new self(); else $This = $this; // $this и $This — это разные переменные
// далее работаем с объектом не думая о последствиях, но используя $This вместо $this
$This->mehod();
…
}
}
либо можно использовать обвязку
class Zend_ActiveRecord_Abstract {
public static funtion get_instance() {
$obj = new self();
return $obj;
}
public funtion find($options = array()) {
if (!isset($this)) return self::get_instance()->find($options);
// далее работаем как обычно
}
}
При таком вызове мы просто эмулируем в самом псевдостатическом методе действия, которые было бы неудобно использовать в остальном коде. Т.е. в данном случае вызов
$post_instance = new Post();
$post_instance->find();
и
Post::find();
абсолютно идентичны, но в последнем случае нам не требуется каждый раз производить создание экземпляра класса