Многоуровневые условия в xPDO

Материал для экспертов. Честно сказать, мало представляю, что это кому-то всерьез понадобится, но тем не менее опишу, хотя и сложно будет (несколько растянутая предыстория будет). Если вы уже работали с нашими getdata-процессорами, понять тему публикации будет легче. Если нет, советую к прочтению: https://modxclub.ru/topics/vse-chto-vyi-xoteli-znat-o-proczessorax-no-boyalis-sprosit-1563.html

Итак, есть у нас getdata-процессор на получение товаров. При чем важно учитывать, что процессоры изначально расширяют другие процессоры на выборки, и рассчитаны на то, что их могут и далее расширять. В итоге один и тот же метод (как тот же prepareQueryBeforeCount, который нас здесь больше всего и интересует) может быть переопределен несколько раз, а конечный SQL-запрос сформироваться из нескольких дополняющих друг друга условий. Для лучшего понимания приведу небольшой пример на чистом xPDO. Сформируем простейший запрос на выборку документов.

$q = $modx->newQuery("modResource");
$alias = $q->getAlias();
$q->select(array(
    "{$alias}.*",
));
$q->prepare();
print $q->toSQL();

На выходе получим SQL-запрос:

SELECT modResource.* FROM `modx_site_content` AS `modResource`

Теперь возьмем и добавим в него пару условий.

$q = $modx->newQuery("modResource");
$alias = $q->getAlias();
$q->select(array(
    "{$alias}.*",
));
$q->where(array(
    "parent"    => 85,
    "deleted"    => 0,
));

Результирующий запрос:

SELECT modResource.* 
   FROM `modx_site_content` AS `modResource` 
   WHERE  ( 
       `modResource`.`parent` = 85 AND `modResource`.`deleted` = 0 
   )

Теперь мы получим документы из раздела 85 и не удаленные. Добавим еще условие, чтобы получить плюс к этому только опубликованные документы.

$q = $modx->newQuery("modResource");
$alias = $q->getAlias();
$q->select(array(
    "{$alias}.*",
));
$q->where(array(
    "parent"    => 85,
    "deleted"    => 0,
));
$q->where(array(
    "published" => 1,
));

Да, xPDO позволяет несколько раз вызывать методы типа where, select, sortby и т.п. Получили более сложный запрос:

SELECT modResource.* 
    FROM `modx_site_content` AS `modResource` 
    WHERE  (  
        ( `modResource`.`parent` = 85 AND `modResource`.`deleted` = 0 )  
        AND `modResource`.`published` = 1 
    )

Здесь, как мы видим, все условия перечислены через AND, так что скобки для нас не играют никакой роли. Но в случае, если у нас в запросе начинают фигурировать условия OR, картина сильно меняется, и очередность скобок и условий начинает играть очень важную роль. Вот давайте в запрос добавим условие "или раздел = 100". То есть у нас выборка уже идет из раздела 85, а хотим еще с теми же условиями выбрать, но включая раздел 100. Запрос расширим, следуя нашей текущей методике, то есть последовательно.

$q = $modx->newQuery("modResource");
$alias = $q->getAlias();
$q->select(array(
    "{$alias}.*",
));
$q->where(array(
    "parent"    => 85,
    "deleted"    => 0,
));
$q->where(array(
    "published" => 1,
));
$q->where(array(
    "or:parent:="    => 100,
));

Что мы получим на выходе?

SELECT modResource.* 
    FROM `modx_site_content` AS `modResource` 
        WHERE  (  
            ( 
                `modResource`.`parent` = 85 AND `modResource`.`deleted` = 0 
            )  
            AND `modResource`.`published` = 1 or `modResource`.`parent` = 100 
        )

Вот тут уже запрос получился не такой, какой бы мы хотели. Здесь получилось буквально "Получить все документы (из раздела 85, не удаленные) и (опубликованные или из раздела 100)". А хотели бы "Получить все документы ((из раздела 85 или из раздела 100), не удаленные) и (опубликованные)". То есть наш желаемый запрос должен выглядеть примерно так:

SELECT modResource.* 
    FROM `modx_site_content` AS `modResource` 
        WHERE  (  
            ( 
                (`modResource`.`parent` = 85 or `modResource`.`parent` = 100) AND `modResource`.`deleted` = 0 
            )  
            AND `modResource`.`published` = 1 
        )

Да, я знаю, что для таких целей можно было бы использовать конструкцию "parent:in" => array(85, 100), но мы рассматриваем не ту задачу, когда мы за один раз формируем нужный нам запрос, а случай, когда мы работаем уже с ранее подготовленным запросом и дальше добавляем в него свои условия.

Итак, прежде чем выдать окончательный вариант, давайте чуть перепишем наш запрос другим позволительным способом:

$q = $modx->newQuery("modResource");
$alias = $q->getAlias();
$q->select(array(
    "{$alias}.*",
));
$where = array();
$where[] = array(
    "parent"    => 85,
    "deleted"    => 0,
);
$where[] = array(
    "published" => 1,
);
$where[] = array(
    "or:parent:="    => 100,
);
$q->where($where);

Это все то же самое, и SQL-запрос конечный тот же самый, но все условия мы не стали по частям отправлять в xPDO, а сначала подготовили массив условий. И здесь вот что интересно: xPDO вполне прилично справляется с многоуровневыми массивами запросов. К примеру, все то же самое можно запихнуть вот в такой массив:

$q = $modx->newQuery("modResource");
$alias = $q->getAlias();
$q->select(array(
    "{$alias}.*",
));
$where = array(
    array(),
);
$where[0][] = array(
    "parent"    => 85,
    "deleted"    => 0,
);
$where[0][] = array(
    "published" => 1,
);
$where[0][] = array(
    "or:parent:="    => 100,
);
$q->where($where);

На выходе сейчас мы получим ровно такой же SQL-запрос, что и ранее, то есть ничего не поменялось. Но теперь мы сформируем конечный правильный запрос и посмотрим что это нам дает.

Я специально добавил здесь больше условий, чтобы показать, что используя такой метод, можно добавлять свои условия в различные части запроса, не особо беспокоясь об очередности. Вот такой запрос у нас получился:

$q = $modx->newQuery("modResource");
$alias = $q->getAlias();
$q->select(array(
    "{$alias}.*",
));
$where = array(
    array(),
    array(),
);
$where[1][] = array(
    "parent"    => 85,
);
$where[0][] = array(
    "deleted"    => 0,
    "published" => 1,
);
$where[1][] = array(
    "or:parent:="    => 100,
);
$where[0][] = array(
    "hidemenu"    => 0,
    "template:in" => array(1,2),
);
$q->where($where);

То есть, буквально, у нас получился запрос "Получить все документы (не удаленные и опубликованные) и (не скрытые в меню и у которых шаблон 1 или 2) и (раздел 85 или раздел 100)".

Что для нас важно здесь, это то, что условия, добавленные в первый элемент массива условий, будут распространяться на все условия второго элемента массива. То есть если мы хотим добавить еще несколько разделов, и при этом добавить условие поиска по заголовку, и еще добавить разделы, это можно сделать в любой последовательности. К примеру

Запрос:

$q = $modx->newQuery("modResource");
$alias = $q->getAlias();
$q->select(array(
    "{$alias}.*",
));
$where = array(
    array(),
    array(),
);
$where[1][] = array(
    "parent"    => 85,
);
$where[0][] = array(
    "deleted"    => 0,
    "published" => 1,
);
$where[1][] = array(
    "or:parent:="    => 100,
);
$where[0][] = array(
    "hidemenu"    => 0,
    "template:in" => array(1,2),
);
$where[1][] = array(
    "or:parent:in"    => array(120,130),
);
$where[0][] = array(
    "pagetitle:like"    => "%test%",
);
$q->where($where);

На самом деле редки случаи построения таких хитрых запросов на лету, но тем не менее. Так же уточню, что эти же условия-массивы можно отправлять и в pdoTools, и они должны работать с большой долей вероятности. Небольшой пример такого вызова: https://modx.pro/development/7236-pdofetch-search-in-tv-fields-with-the-delimiter/

UPD: Василий Наумкин подсказывает, что есть еще нативный способ подобное реализовывать через передачу группы запроса в orCondition()/andCondition().

Пример:

$q = $modx->newQuery("modResource");
$alias = $q->getAlias();
$q->select(array(
    "{$alias}.*",
));
$q->where(array(
    "parent"    => 85,
), null, 0);
$q->orCondition(array(
    "deleted"    => 0,
    "published" => 1,
), "OR", 1);
$q->orCondition(array(
    "parent:="    => 100,
), null, 2);

$q->prepare();
print $q->toSQL();

Но как мы совместно выяснили, это не дает полный контроль над формированием запроса. К примеру, этот код сформирует такой запрос:

SELECT modResource.* 
    FROM `modx_site_content` AS `modResource` 
        WHERE  ( 
            `modResource`.`parent` = 85 
            AND  ( `modResource`.`deleted` = 0 OR `modResource`.`published` = 1 )  
            AND `modResource`.`parent` = 100 
        )

То есть как я ни старался, он не сбил блоки условием OR, а только AND. А вот с использованием массивов получилось вот такой запрос сформировать:

$q = $modx->newQuery("modResource");
$alias = $q->getAlias();
$q->select(array(
    "{$alias}.*",
));
$where = array(
    array(
        "parent"    => 85,
        "or:parent:="    => 100,
        array(
            "or:hidemenu:="    => 0,
            "template:in" => array(1,2),
        )
    ),
    array(
    ),
);

$where[0][0][] = array(
    "published" => 1,
);

получив вот такой запрос:

SELECT modResource.* 
    FROM `modx_site_content` AS `modResource` 
    WHERE  (  
        ( 
            `modResource`.`parent` = 85 or `modResource`.`parent` = 100 
            or  ( 
                `modResource`.`hidemenu` = 0 AND `modResource`.`template` in (1,2) AND `modResource`.`published` = 1 
            )  
        )  
    )