Заметка: будут использоваться примеры кода из minishop2 от bezumkin. Пусть никто не ищет подвоха, его здесь нет. Просто материал предметный, а проверить примеры может всякий, скачав его из репозитория.
Кто захочет поиграться с примерами, лучше устанавливайте Console.
В свое время у нас уже был очень сложный диалог по поводу xPDO::addDerivativeCriteria(), и вот только сейчас я разобрался со всей этой системой на 100%, и могу четко все разложить по полочкам и описать.
В статье рассматривается поведение xPDO, когда у вас в одной таблице, в которой есть колонка class_key, хранятся записи нескольких производных классов. Эта ситуация широко известна. К примеру в таблице site_content хранятся записи производных modResource классов modDocument, modWebLink и т.п. Подробней обо всем этом можно прочитать здесь.
Коротко: когда в таблице есть колонка class_key, то xPDO, получая записи из такой таблицы методами getObject(), getCollection() и т.п., возвращает конечные объекты именно тех классов, имена которых указаны в этих колонках. То есть, к примеру, сделав выборку трех записей из таблицы site_content, значения class_key которых будут modDocument, modWebLink и modSymLink, на выходе мы получим не 3 объекта modResource, а три уникальных объекта от соответствующих классов.
Собственно, на этом и строятся CRC. Мы добавляем свой собственный класс, расширяя modResource, и при выборке $modx->getCollection('modResource'), вместе со всеми прочими документами, получаем и собственные объекты (чисто локальный пример).
Но здесь есть очень много всяких тонкостей. И чтобы было проще их понять, рассмотрим на реальных примерах с использованием кастомного класса msProduct из пакета minishop2.
Сразу уточню, что у меня документ с id=100 — это как раз документ с class_key=msProduct. Это важно запомнить, чтобы четко понимать результаты примеров.
Пример
$q = $modx->newQuery('msProduct', 100);
$docs = $modx->getCollection('msProduct', $q);
Вот здесь у нас с выполнением никаких проблем не возникло. Результирующий объект — инстанс класса msProduct, и все ОК.
Теперь уберем id из условия, и выполним запрос, который вернет нам все документы.
$q = $modx->newQuery('msProduct');
$docs = $modx->getCollection('msProduct', $q);
И получим вот такие ошибки в лог:
Instantiated a derived class modDocument that is not a subclass of the requested class msProduct
Что это за ошибки и почему они происходят?
Для этого стоит изучить метод xPDOObject::_loadInstance()
.......
$instance= $xpdo->newObject($actualClass);
if (is_object($instance) && $instance instanceof xPDOObject) {
.......
$parentClass = $className;
$isSubPackage = strpos($className,'.');
if ($isSubPackage !== false) {
$parentClass = substr($className,$isSubPackage+1);
}
if (!$instance instanceof $parentClass) {
$xpdo->log(xPDO::LOG_LEVEL_ERROR,
"Instantiated a derived class {$actualClass} that
is not a subclass of the requested class {$className}");
}
То есть, если реальный инстанс полученного объекта не является инстансом запрашиваемого в getCollection() класса, то xPDO пишет ошибку.
Следует отметить, что он все равно вернет полученный объект, но логи будут расти.
То есть если мы заменим msProduct на modResource, то этих ошибок не будет.
$q = $modx->newQuery('msProduct');
$docs = $modx->getCollection('modResource', $q);
В данном случае мы запросили коллекцию объектов modResource, и так как все они — производные от modResource, то и ошибки нет. Но это еще совсем не все. Как в мультике — «Стрижка только началась»…
Вот здесь стоит рассмотреть более внимательно код, и еще жду ответа от Джейсона Коварда о возможном баге. Собственно, ответ уже есть. Да, это бага. Тикет создан и он его обещал пофиксить прямо сейчас. Ну да ладно, баг — багом, но он нам не мешает дальнейшему рассмотрению поднятой темы.
Итак, дело в том, что существует метод xPDOObject::addDerivativeCriteria(), который автоматически добавляет условие class_key в запрос. Давайте посмотрим код.
public function addDerivativeCriteria($className, $criteria) {
if ($criteria instanceof xPDOQuery && !isset($this->map[$className]['table'])) {
if (isset($this->map[$className]['fields']['class_key']) &&
!empty($this->map[$className]['fields']['class_key'])) {
$criteria->where(array('class_key' => $this->map[$className]['fields']['class_key']));
if ($this->getDebug() === true) {
$this->log(xPDO::LOG_LEVEL_DEBUG, "#1: Automatically
adding class_key criteria for derivative query of class {$className}");
}
} else {
foreach ($this->getAncestry($className, false) as $ancestor) {
if (isset($this->map[$ancestor]['table'])
&& isset($this->map[$ancestor]['fields']['class_key'])) {
$criteria->where(array('class_key' => $className));
if ($this->getDebug() === true) {
$this->log(xPDO::LOG_LEVEL_DEBUG, "#2: Automatically adding class_key criteria
for derivative query of class {$className} from base table class {$ancestor}");
}
break;
}
}
}
}
return $criteria;
}
То есть, если в описании запрошенного класса нет указания таблицы (что с большой вероятностью говорит о том, что запрошенный класс — производный), то в условие автоматически добавляется условие class_key=$className.
К слову о баге: дело в том, что класс modResource наследуется не напрямую от класса xPDOObject, а от его производного modAccessibleObject, в котором метод loadCollection() переопределяется, без вызова родительского, и в этом методе не прописано $xpdo->addDerivativeCriteria($className, $criteria); То есть у нас не происходит автоматического добавления условия class_key. Если бы эта бага отсутствовала, то при запросе $modx->getCollection('msProduct', $q) xPDO автоматически бы добавил условие class_key=msProduct, и сделал бы выборку только записей с этим class_key. Это позволило бы избежать необходимости каждый раз прописывать $where = array('class_key' => 'msProduct'), как это сейчас делается в minishop2. При чем это касается как сниппетов, так и процессоров на выборки и т.п. (кстати, там есть тоже бага, о которой скажу чуть позже).
Василий, бери на заметку, это тебе скорее всего пригодится: баг пофиксят, но не все сразу обновятся. Чтобы не писать во всех местах условие $where = array('class_key' => 'msProduct'), можно добавить два статических метода в класс msProduct
public static function load(xPDO & $xpdo, $className, $criteria= null, $cacheFlag= true){
if (!is_object($criteria)) {
$criteria= $xpdo->getCriteria($className, $criteria, $cacheFlag);
}
$xpdo->addDerivativeCriteria($className, $criteria);
return parent::load($xpdo, $className, $criteria, $cacheFlag);
}
public static function loadCollection(xPDO & $xpdo, $className, $criteria= null, $cacheFlag= true){
if (!is_object($criteria)) {
$criteria= $xpdo->getCriteria($className, $criteria, $cacheFlag);
}
$xpdo->addDerivativeCriteria($className, $criteria);
return parent::loadCollection($xpdo, $className, $criteria, $cacheFlag);
}
Тогда при явных запросах $modx->getCollection('msProduct') условие class_key=msProduct будет автоматически добавлено, и будут возвращены только объекты msProduct. А при выборке $modx->getCollection('modResource') будут возвращены все объекты без разбора, включая msProduct.
Но при формировании чистого SQL-я это не поможет, так как эти методы просто не будут вызваны. Но можно формировать запросы так (правда это уж совсем на заметку):
$q = $modx->newQuery('msProduct');
$modx->addDerivativeCriteria('msProduct', $q);
$docs = $modx->getCollection('msProduct', $q);
Баг №2.
Второй баг касается метода xPDO::getCount(). Этот метод так же формирует запрос без учета метода xPDO::addDerivativeCriteria(), в результате чего можно получить неверное количество найденных строк. Это особенно критично для GetList — процесоров. Когда первый баг пофиксят, не надо будет в List-процессорах дописывать условия where class_key=$className, как это сейчас используется в minishop2. Но при этом, скорее всего процессор вернет правильное количество объектов, но неправильное число найденных строк, так как он сначала выполнит подсчет строк без учета class_key= (и просто найдет все строки по критерию), а уже потом выполнит запрос с учетом class_key.
Этот баг тоже будет пофиксен в ближайшее время.
Момент 3. Наследование
Итак, считаем, что у нас баги все пофиксены. Давайте теперь более внимательно рассмотрим наследование, и что это нам может дать (в том числе и ошибки).
Мы уже отметили, что делая выборку $modx->getCollection('modResource'), мы получим все объекты с различными class_key, а делая выборку $modx->getCollection('modDocument'), мы получаем только объекты с class_key=modDocument (напоминаю, что сейчас это не так из-за багов, но мы договорились считать, что баги пофиксены).
Такое поведение обусловлено тем, что в модели класс modResource четко описана таблица, и метод xPDO::addDerivativeCriteria() не добавляет условия class_key=$className, а в классе modDocument таблица не указана, и потому условие class_name=modDocument было автоматически создано.
К слову, если добавить в описание класса modDocument 'table'=>'site_content', то xPDO вернет все записи, и создаст положенные записи об ошибках в лог. Ну да ладно, это так, уточнение.
Итак, мы получаем довольно хороший механизм: мы можем создать единую таблицу для нескольких кастомных классов, хранить их записи в единой таблице, все объекты будут иметь уникальные ID, и нам не придется париться с написанием условий для выборки, все будет создаваться автоматически, и указывая в запросе базовый класс, мы будем получать все объекты, а указывая конкретный класс, будем получать объекты только этого класса. Просто обалденно! Но здесь есть еще один маленький момент. Рассмотрим его на примере все того же msProduct. Он у нас унаследован от класса modResource. Выполним вот такой запрос:
$q = $modx->newQuery('msProduct');
$modx->addDerivativeCriteria('msProduct', $q);
$docs = $modx->getCollection('modDocument', $q);
Получаем все ту же ошибку
Instantiated a derived class msProduct that is not a subclass of the requested class modDocument
Дело в том, что и modDocument, и msProduct — дочерние классы от modResource, но msProduct не является дочерним классом от modDocument. Это очень важно понимать, чтобы правильно планировать свою структуру наследуемых классов.
На этом пожалуй все. Надеюсь, материал кому-то был полезен.