Конечно по установке MODX Revolution в Интернете много информации, в том числе и видеоуроков. Но мне часто задают вопросы типа «Посмотрите на общую структуру моего сайта», или «Не могли бы вы дать рекомендации по оптимизации сайта» и т.п. Кстати, немного забегая вперед, скажу, что пожалуй один из самых правильных вопросов был:
В ваших проектах наверняка есть куча шаблонов, чанков и полей. Наверняка есть категории, правила именования и так далее. Не могли бы вы поделиться сим ценным опытом
Этот вопрос мы обязательно рассмотри более детально в одном из следующих уроков.
Так вот, изучая такие подопытные сайты, могу выявить пару самых важных ошибок, которые допускают начинающие MODX-программисты:
1) Слишком много сторонних пакетов. Куча чанков, сниппетов, плагинов (которые работают тогда, когда это вообще не нужно). И это даже на мелких сайтах-визитках.
2) Слишком много пользовательских объектов в принципе. Огромная куча чанков, сниппетов и т.п. просто по любому случаю. Но это часто не только «помойка» в целом (в плане сложностей структурирования такого количества объектов), это еще и огромное количество ненужных вызовов этих элементов. Вот яркий пример:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <title>[[++site_name]] - [[*pagetitle]]</title> <base href="[[++site_url]]" /> [[$myIncludeCSS? &name=`jquery-ui-1.9.1`]] [[$myIncludeCSS? &name=`common`]] [[$myIncludeCSS? &name=`ucp`]] [[$myIncludeCSS? &name=`login`]] [[$myIncludeCSS? &name=`mainpage`]] [[$myIncludeCSS? &name=`container`]] [[$myIncludeCSS? &name=`comments`]] <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.8.2/jquery.min.js"></script> <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jqueryui/1.9.0/jquery-ui.min.js"></script> </head> <body> [[$myKovchegTemplateBegin]] [[*content]] [[$myKovchegTemplateEnd]] </body> </html>
3) Не оптимизированные шаблоны. Вот пример шаблона:
<!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml"> <head> [[$HEAD]] </head> <body> <!--header_wrap-start--> <div id="header_wrap"> <!--header-start--> <div id="header"> [[$Header]] </div> <!--header-end--> </div> <div class="greenline"></div> <!--header_wrap-end--> <div class="banner"> <div class="banner_left"> [[$Menu.Slider]] </div> <div class="banner_right"> [[$Menu.top]] </div> <div class="clrflt"> </div> <div class="margin5"> </div> <div class="greenline"></div> <div class="clrflt"> </div> <div class="margin3"> </div> <marquee loop="infinite" behavior="altemate" bgcolor="#14601e" direction="left" height="23" width="100%" scrollamount="3" style="color:#fff; font-size:1.1em; padding:7px 0 0 0; font-family:Georgia, Times New Roman, Times, serif; font-style:italic; ">[[$Stroka]]</marquee> <div class="clrflt"> </div> </div> <!--banner-end--> <div id="content"> <!--content-start--> [[$Menu.left]] <div class="content_right fltright"> <div class="right_box1"> <!--div class="right_box1_left fltleft"><img src="inc/img/right_box1_pic.png" alt="" width="187" height="187" /></div--> <!--div class="right_box1_right fltright"--> [[*content]] <a href="[[~4]]">Узнать больше</a> <!--div class="clrflt"> </div> </div--> <div class="clrflt"> </div> </div> <div class="right_box2"> <h2>Наш коллектив</h2> <div class="infiniteCarousel"> <div class="wrapper"> <ul> [[getResources? &parents=`45` &tpl=`tpl.gR.Slider_foto.row` &includeTVs=`1` &processTVs=`1` &tvPrefix=`` &hideContainers=`1` &limit=`0` &sortdir=`ASC` &sortby=`[[getResourcesTree? &parents=`45` &depth=`100`]]` &tvFilters=`show_in_carusel==on` ]] </ul> </div> </div> <!-- --> </div> <div class="right_box3"> [[$Block_news]] [[$Block_Article]] <div class="clrflt"> </div> </div> <div class="clrflt"> </div> </div> <!--content-end--> <!--footer-start--> <div id="footer"> [[$Footer]] </div> <!--footer-end--> </body> </html>
Такое, с мелкими изменениями, присутствует в каждом шаблоне. А шаблонов двадцать. Представьте сколько работы нужно провести, чтобы обновить дизайн такого сайта… Да и элементарное сопровождение такого сайта.
4. Слишком много чанков, и очень мало PHP-кода. Конечно, если у вас сайт-визитка на 5 страниц, можно и чанками обойтись. Но если вы хоть сколько-нибудь серьезный сайт делаете, то надо больше писать чистого PHP-кода. А для этого надо изучать API MODX. И здесь не стоит бояться низкого уровня знаний PHP. На 95% задач достаточно API MODX в чистом виде. Ведь очень много веб-программистов клепают всякие плюшки на jQuery, и просто используют его в повседневной работе. При этом многие не знают pure-javascript методов типа document.getElementByID, document.getElementsByTagName, не знают, что в IE есть метод outerHTML, и нет его в FireFox и т.д. При этом активно используют $('tag').find('.class',function(){}) и т.д. А ведь это тоже практически ООП. Просто многие этого не боятся, потому что не знают. Так вот, не надо бояться ООП, и больше изучать API MODX. Оно довольно интуитивно и не плохо документировано. Нет ничего сложного в использовании
$docs = $modx->getCollection('modResource', array( 'parent' => $modx->getOption('site_start') ));
или
$context = $modx->getContext('web'); $contextConfig = $context->config;
В последующих уроках мы будем много внимания уделять именно PHP-программированию с использованием API MODX, потому что только так можно научиться делать какие-то серьезные проекты, и так, чтобы они работали стабильно и эффективно.
Итак, после такого большого вступления, зададимся вопросом: «Как же всего этого избежать?». Ответ: научиться конечно предстоит многому, но главное, с чего надо начать, это научиться устанавливать ту минимальную сборку MODX Revolution, которая позволила бы решать 95% процентов повседневных задач, и при этом не захламлять систему. Я имею ввиду голую установку + минимум самых важных пакетов. Лично для меня минимальный список пакетов такой:
  • CodeMirror (подсветка кода в чанках, сниппетах и т.п. Кто-то предпочитает Ace)
  • TinyMCE (контент-редактор)
  • Wayfinder (для генерации меню)
  • MetaX (Формирует правильные Meta-теги страницы)
  • GoogleSiteMap (про сеошность ведь тоже не забываем)
  • Login (авторизация во фронтенде)
Второстепенные пакеты:
  • Breadcrumbs (хлебные крошки)
  • Gallery (понятно дело, что галерея)
  • DirectResize (неплохой пакет для автоматического «обрезания» картинок на сайте, когда на страницу попадают картинки, искусственно уменьшенные (стилями или атрибутами)), и автоматически оборачивающий такие картинки в галереи типа Colorbox, HighSlide и т.п.
  • getResources (Замена Эволюшинскому Ditto. Вывод новостных лент, постраничности и т.п.)
Да, getResources у меня во второстепенных пакетах (хотя многие удивятся, ведь это третий по популярности пакет, и все его ставят). Более того, я его не ставил уже года полтора, и не поставлю больше никогда. Потому что он очень тяжелый и крайне глючный, а написать свой компонент для вывода новостей — пара часов. И будет это быстрее работать, а главное — будет полный контроль над фильтрами и т.п. Но в список его добавил, потому что понимаю, что многим будет без него не обойтись, а какой сайт без новостей и статей?
UPD:Видео. В ролике демонстрируется закачка установочного пакета MODX сразу на сервер по SSH командой wget, полная установка MODX Revolution, а так же установка самых необходимых пакетов из официального репозитория + установка пакета локально.
Переносил сейчас сайт с Evo на Revo. Установил DirectResize из официального репозитория. В целом все шуршит. Но на одной странице проявился баг. Картинка не только не ресайзилась, у нее еще и установленные атрибуты width и height слетели, в результате чего на странице картинка стала крупней, чем надо.
Полез ковырять плагин. Нашел там два бага.
1. $height = preg_replace("[^0123456789]","",$array[0]); Ошибка в регулярном. Забыли слеши по краям прописать. Из-за этого плагин не мог очистить HTML-атрибут тега картинки, и получить чисто числовое значение высоты. Кстати, этот баг я не менее полутора лет назад встречал в DirectResize, но его так и не поправили.
2. Картинку как обычно заливал какой-то манагер, и в названии картинки присутствовали пробелы. При вставке картинки TinyMCE благополучно пробелы заменил на коды %20, а вот DirectResize urldecode не выполнил при обработке пути картинки. В результате все и побилось.
Так как официальный пакет обновлялся последний раз 4-го января 2012-го, то рассчитывать на скорейший патч особо не приходится.
Собрал новый пакет с заплатками. Скачать можно на sourceforge.net. Можно устанавливать как обновление текущему пакету, и не бояться, что не встанет потом официальное обновление.
Кстати, кто не читал эту статью, почитайте. Благодаря этому у меня вообще не было головняков с контролем того, какие параметры мне опять обновлять при установке обновленного пакета, или при откате версии.
Так как MODX — это не только среда для разработки веб-проектов, а еще и PHP-фреймворк, и в разработке используются не только MODX-объекты, а еще и на чистом PHP пишем, и так как хочу опубликовать уже вторую заметку о чисто ПХП-шной функции, создаю-таки отдельную ветку под это дело. Думаю, кому-нибудь да пригодится.
В этот раз хочу написать заметку про функцию getimagesize. Столкнулся с ней сейчас, когда опять ковырял плагин DirectResize (пара косяков в нем как была два года назад, так до сих пор и есть...).
Так вот, чем же интересна эта функция? Интересна она тем, что не только определяет размер картинки, но еще и определяет ее mime-тип, и формирует HTML-атрибуты WIDTH и HEIGHT.
Вот результат выполнения этой функции:
Array ( [0] => 500 [1] => 331 [2] => 2 [3] => width="500" height="331" [bits] => 8 [channels] => 3 [mime] => image/jpeg )
Вообще раньше только пару раз использовал Наборы параметров. А зря… Их силу оценил только сегодня, когда полез переносить очередной старый сайт на Рево. Как раз недавно писал про обновление сайта. Так вот, там я писал, что так как сайт старый, то просто в базе копировал содержимое отдельных таблиц, и последним пунктом выполнял обновление (читай установку новых/голых) пакетов. Так вот, я там не написал с какой проблемкой столкнулся при таком переносе. И проблема не в самом способе переноса, а в ошибочном подходе к переопределению параметров сторонних элементов.
Простой пример: прописываем вызов сниппета [[Wayfinder?startId=`0`]], но чтобы не переопределять каждый раз все эти innerClass, rowTpl и прочее, просто идем в редактирование сниппета Wayfinder и в удобном редакторе правим нужные нам Параметры по умолчанию. ?
И все. Переопределили все нужные нам параметры, и теперь везде можно тыкать [[Wayfinder]], и не прописывать каждый раз одни и те же параметры. Удобно? А то!
А теперь представьте, что по какой-то причине сниппет Wayfinder (не важно по какой). Само собой удалились и все Параметры по умолчанию, в том числе и те, которые переопределели вы. А там и шаблон был указан с интуитивным названием RowTpl12321SDF, и несколько excludeDocs (2,3,4,5,7,12-34,56-77) и т.д. И как, теперь все это опять где-то искать, переопределять и т.п.?
Вот это как раз и есть проблема, с которой я столкнулся. Конечно всего пара параметров было переопределено, но все равно пришлось их поискать. Проблема просто еще не только в том, что надо найти эти параметры, проблема в том, что они в общей куче со всеми Параметрами по умолчанию, и поиск нужного параметра, который переопределенный, а не с исходным значением, может превратиться в поиск иголки в стоге сена.
Но как оказалось, есть вполне деликатное решение данной проблемы. И это — Наборы параметров. В чем их суть? Создается набор параметров, связывается с объектами MODX, и можно создавать свои параметры, или (и это главное), переопределять имеющиеся параметры связанных объектов.
Опять пример. В свой шаблон мы воткнули сниппет [[!Login]]. Смотрим результат. ?
Хотим изменить шаблон формы. Само собой нам надо изменить параметр loginTpl. Но для этого мы не будем ни редактировать этот параметр по умолчанию, не передавать его в вызов сниппета. Вместо этого мы создадим свой Набор параметров. Назовем его Site, дабы не профилировать его с ходу. ?
?
?
Теперь этот Набор нужно привязать к сниппету Login. Вообще связывать можно с чанками, сниппетами, плагинами и TV, при чем не с одним элементом. Потому и говорю, что не обязательно с ходу профилировать набор. ?
?
?
?
Все, связали со сниппетом Login. А теперь внимание! Кликаем Login, и что там видим? ?
А там мы сразу видим все Параметры по умолчанию сниппета Login. И здесь же можем изменить нужные нам параметры. К примеру я изменю loginTpl. ?
Но самое приятное не то, что мы здесь же можем параметры править. Самое приятное вот что: 1) Мы не только этот параметр видим в списке всех параметров. ?
Главное — мы видим измененный параметр в отдельности, если кликнем на сам Набор параметров. ?
2) Изменив параметр сниппета Login, мы не изменили параметр в самом сниппете. Там он остался прежним. ?
А вот это уже действительно удобно. Теперь остается только в элемент передать ссылку на Набор, и все. [[!Login@Site]] Смотрим результат (только не забывайте, что WebLoginSideBar — это мой пользовательский чанк-шаблон для Login, потому кто захочет поэкспериментировать, просто написать имя этого чанка недостаточно): ?
А теперь в этот же набор можно добавить и другие объекты, к примеру сниппет Wayfinder. И не только переопределим его параметры, но и свой новый добавим. ?
И все измененные параметры мы видим в одном месте. ?
При этом мы сами решаем, когда использовать эти параметры, а когда нет (то есть передавать параметр @Site объекту или нет).
В общем кому как, но на мой взгляд все это очень удобно. Хотя видимо не очень популярно, так как не смотря на то, что уже версия Revolution 2.2.5, не то, чтобы ошибки какие-то, элементарные недоработки есть. К примеру, тупо нет возможности удалить какой-либо параметр, даже свой, пользовательский. В контекстном меню пункт Редактировать есть, а Удалить — нет. ?
Но это не большая проблема. Сейчас мне это не мешает, а потом наверняка доработают.
UPD: А вот с синтаксисом есть особенности. Название Набора параметров не должно следовать за вопросительным знаком. Так же нельзя указать сразу два набора параметров. Так что, если хотите вызвать с набором параметров, и еще и на лету переопределить какие-то параметры, то пишем так: [[!Wayfinder@Site?&limit=`2`&rowClass=`row`(и так далее)]]
UPD 2:Еще одна приятность: возможность восстановить изначальное значение по умолчанию, при этом это действие будет локально только для связанных с этим Набором объектов, а не затронет всю систему в целом. Вот у меня два Набора, связанных со сниппетом Wayfinder. Каждый набор использует для себя дефолтовые параметры из Wayfinder. В каждом наборе свои переопределения параметров. Но если в каком-то наборе я восстанавливаю дефолтовое значение, это восстановление касается только этого набора. ?
Правда на практике это действие через контекстное меню опять-таки не доработано, то есть не выполняет на сервер запрос с целью выяснения дефолтового значения, и приходится значение восстанавливать самому вручную, но в обозримом будущем наверняка это доработают, или если спрос будет, то сделаю заплатку.
Топик наверняка будет полезен тем, кто собирает свои пакеты для MODX Revolution.
Собираю один пакет, и вот понадобилось мне, чтобы при установке пакета сразу прописывались доступы к контекстам. Выполнить пользовательский php-скрипт в пакете не особая проблема, это даже в топике описывать не буду. А вот решил я, чтобы много кода не писать и по религии все было, тупо вызвать процессор $modx->runProcessor('security/access/addacl', $params); Дело не хитрое, быстро прописал все, собрал пакетик, пробую установить, и… Лог установки в окошко не полный вышел, процесс установки явно встал колом, а на сервер бесконечно отправляются запросы на проверку результата установки, а в ответ от сервера тишина… Вот так вот как обычно желание сэкономить пару строк кода приводит к потере нескольких часов времени и полному вскрытию движка…
Немного теории: Когда мы устанавливаем через админку пакет (у нас же там Ajax по всей комнате), отправляется запрос на установку, потом шаги всякие и т.п., и после окончательного «Установить», отправляет окончательный запрос на установку, после чего начинают периодически отправляться на сервер запросы, проверяющие ход установки и выводящие логи. Запросы прекращают идти только после того, как получат окончательный ответ с результатом True or False.
Теперь суть возникшей проблемы: Оказывается, вызов процессора не проходит даром. Сюрприз крылся к логировании ошибки, возникшей в процессоре. Когда в процессоре возникла ошибка, он попытался ее залогировать $modx->error->failure($error);, и у него даже получилось. Так а почему же тогда установка колом встала? Забегая вперед, можно сказать, что установка в целом проходила в обычном режиме, и даже все устанавливалось, а вот связь клиент-сервер рушилась, то точнее «общение» на уровне логов. Посмотрим метод $modx->error->failure().
public function failure($message = '', $object = null) { return $this->process($message, false, $object); }
А там
public function process($message = '', $status = false, $object = null) { if (isset($this->modx->registry) && $this->modx->registry->isLogging()) { $this->modx->registry->resetLogging(); }
И здесь самое интересное: $this->modx->registry->resetLogging(); То есть тупо сбрасывается текущее логирование… modRegistry::resetLogging()
public function resetLogging() { if ($this->_loggingRegister && $this->_prevLogTarget && $this->_prevLogLevel) { $this->modx->setLogTarget($this->_prevLogTarget); $this->modx->setLogLevel($this->_prevLogLevel); $this->_loggingRegister = null; } }
И что же получается? Клиент постоянно шлет запросы на сервер, а там лог сброшен, и никакого ответа уже для него нет. Собственно, это и печально. Но расковырять проблему было гораздо проще, чем найти ее решение. Не буду утомлять большим количеством буков, напишу решение вкратце:
$LogTarget = $modx->getLogTarget(); $LogTopic = $LogTarget->subscriptions[0]; /* processor_code */ $modx->runProcessor($processor); $modx->registry->setLogging($LogTarget, $LogTopic);
То есть перед вызовом процессоров, получаем текущий инстанс логера, перехватываем его идентификатор (Топик), затем выполняем нужные нам процессоры, после чего опять передаем MODX-у этот логер и продолжаем писать в него что нам нужно до окончания установки пакета. Только надо иметь ввиду, что нельзя такой фокус провернуть дважды в одном скрипте, то есть такое не проканает:
$LogTarget = $modx->getLogTarget(); $LogTopic = $LogTarget->subscriptions[0]; /* processor_code */ $modx->runProcessor($processor); $modx->registry->setLogging($LogTarget, $LogTopic); $LogTarget = $modx->getLogTarget(); $LogTopic = $LogTarget->subscriptions[0]; /* processor_code */ $modx->runProcessor($processor); $modx->registry->setLogging($LogTarget, $LogTopic);
Почему? Потому что метод $modx->registry->setLogging принимает ссылку на объект.
public function setLogging(modRegister &$register, $topic, $level = modX::LOG_LEVEL_ERROR)
А метод modRegistry::resetLogging() убивает этот объект ($this->_loggingRegister = null;). То есть в первый раз мы еще успеваем что-то перехватить, во второй раз уже нечего перехватывать, и наш объект уничтожен.
Пытался тут настроить на Рево 2.2.5 определенные политики безопасности, но ничего не могу понять: как не крути, все равно все доступно. Даже выполняю $modx->hasPermission('asdasdasda'); (то есть на проверку того, чего вообще нет), а все равно 1. Полез в код. И что там видно?
public function checkPolicy($criteria, $targets = null) { if ( $criteria && $this->xpdo instanceof modX && $this->xpdo->getSessionState() == modX::SESSION_STATE_INITIALIZED ){ if ($this->xpdo->user->get('sudo')){ return true; } } }
То есть если пользователь SUDO, то для него вообще не существует препятствий. А ведь раньше можно было одним неловким движением админу полностью закрыть доступ в админку и т.п. Полез в changelog, ввели эту радость в версии 2.2.1 Так что если решите экспериментировать с доступами, сразу создавайте тестового пользователя, не SUDO.
Сегодня оптимизировал работу одного сайта и хочу дать пару советов, которые могут пригодиться многим начинающим программистам, особенно тем, кто по большей степени ведет разработку на уровне тегов MODX.
Кстати, сразу отмечу, что это сайт-визитка, и все страницы кешируемые за исключением отдельных блоков.
Итак, самый главный минус, с которым я столкнулся. В большинстве шаблонов контент страницы выводился через пользовательский чанк, в котором уже был прописан тег [[*content]]. Вот полный листинг этого чанка:
[[*longtitle:notempty=`<h2>[[*longtitle]]</h2>`]] [[Breadcrumbs? &homeCrumbTitle=`Главная` ¤tAsLink=`0` &showHomeCrumb=`1` &crumbSeparator=`<li><img src="/inc/img/right_box1_a.png" style="vertical-align:middle" alt="" /></li>` &respectHidemenu=`1` ]] [[*parent:is=`244`:then=` <img class="img_left" src="[[*img_programm]]" alt="[[*pagetitle]]" /> `]] [[*content]] [[*id:is=`6`:then=` <hr /> [[!getPage@Pages? &elementClass=`modSnippet` &element=`getResources` &parents=`6` &limit=`5` &sortby=`menuindex` &sortdir=`DESC` &tpl=`tpl.otziv` &includeTVs=`1` &includeContent=`1` &tvPrefix=`` &pageVarKey=`page` ]] <ul class="pageList"> [[!+page.nav]] </ul> <div class="clear"></div> `]] [[*parent:is=`240`:then=` <!--dl id="accordion"--> [[!getResources? &tpl=`ProgramsTpl_2` &tplFirst=`ProgramsTpl_2_First` &tplLast=`ProgramsTpl_2_Last` &includeTVs=`1` &tvPrefix=`` &includeContent=`1` &resources =`[[*list_prog_ch]]` &hideContainers=`1` &sortdir=`ASC` &limit=`100` ]] <!--/dl--> `]] [[*parent:is=`244`:then=` <p align="right"><span class="link"><a href="javascript:history.back();">Назад к списку</a></span></p> `]] <hr /> <div class="right_box3"> [[$Block_news]] [[$Block_Article]] <div class="clrflt"> </div>
Здесь предлагаю обратить внимание именно на конструкции типа [[*parent:is=`240`:then=`…
В чем же здесь проблема? Проблема в том, что MODX-парсер работает не так, как, к примеру, php-интерпретатор. По логике вещей здесь должно быть следующее: если ID родителя текущего документа равен 240, то выполняется блок Then.
По логике Да, а на практике Нет. Покажу простой пример как обрабатываются условия на php, чтобы показать, чего не происходит. Вот простой код:
if($modx->resource->parent == 240){ print time(); }
Так вот, в данном примере, если parent не будет равен 240, то функция time() не будет выполнена в принципе. При парсинге MODX же содержимое блока выполнится в любом случае, просто будет результат выведен на странице или нет, определит истинность условия.
В данном случае независимо от условия MODX-парсер выполнил полностью блок Then, а только потом на основе условия решит выводить результат этого блока или нет. То есть не зависимо от того, какой родитель у документа, все блоки в этом чанке будут выполнены. А меж тем этих блоков здесь четыре, при этом два из них — НЕкешируемый сниппет getPage. То есть даже если страница уже закеширована, каждый раз при обращении к ней MODX будет обрабатывать эти сниппеты, потому что они некешируемые. Как результат: 5-10 секунд на генерацию кода закешированной страницы против одной секунды с исправленной логикой.
Здесь мы подобрались к тому, как же правильней прописывать подобные вещи. Мой совет: меньше используйте чанки, больше сниппеты. Не смотря на то, что чанки психологически воспринимаются более простыми элементами (так как в них вроде как HTML, а не исполняемый php), тем не менее их использование накладывает дополнительные расходы, так как дополнительно нагружают парсер. Плюс, как я писал здесь, в чанках вы управляете кешем элементов только на уровне текущего документа, а не на уровне всего сайта. Потому старайтесь придерживаться правила: всякая логика в сниппетах, оформление в чанках.
Как же было бы правильней реализовать логику?
1. Создаем чанк, который будет у нас шаблоном для вывода результата работы сниппета.
[[*longtitle:notempty=`<h2>[[*longtitle]]</h2>`]] [[Breadcrumbs? &homeCrumbTitle=`Главная` ¤tAsLink=`0` &showHomeCrumb=`1` &crumbSeparator=`<li><img src="/inc/img/right_box1_a.png" style="vertical-align:middle" alt="" /></li>` &respectHidemenu=`1` ]] [[+pre_content]] [[*content]] [[+post_content]] <hr /> <div class="right_box3"> [[$Block_news]] [[$Block_Article]] <div class="clrflt"> </div>
На заметку: здесь есть два плейсхолдера: pre_content и post_content. При создании чанков, если не уверены в том, что для всех плейсхолдеров будут переданы переменные, на всякий случай создайте пустые Параметры для этого чанка. В нашем случае это Параметры pre_content и post_content. Не знаю насколько сейчас пофиксили, но раньше парсер до 10-ти раз пытался «найти» переменную, пережевывая чанк.
2. Создаем сниппет.
<?php $pre_content = ''; $post_content = ''; $parent = $modx->resource->id; switch ($parent) { case '244': $pre_content = '<img class="img_left" src="[[*img_programm]]" alt="[[*pagetitle]]" />'; $post_content = '<p align="right"><span class="link"><a href="javascript:history.back();">Назад к списку</a></span></p>'; break; case '6': $post_content = '<hr /> [[!getPage@Pages? &elementClass=`modSnippet` &element=`getResources` &parents=`6` &limit=`5` &sortby=`menuindex` &sortdir=`DESC` &tpl=`tpl.otziv` &includeTVs=`1` &includeContent=`1` &tvPrefix=`` &pageVarKey=`page` ]] <ul class="pageList"> [[!+page.nav]] </ul> <div class="clear"></div>'; break; case '240': $post_content = '<!--dl id="accordion"--> [[!getResources? &tpl=`ProgramsTpl_2` &tplFirst=`ProgramsTpl_2_First` &tplLast=`ProgramsTpl_2_Last` &includeTVs=`1` &tvPrefix=`` &includeContent=`1` &resources =`[[*list_prog_ch]]` &hideContainers=`1` &sortdir=`ASC` &limit=`100` ]] <!--/dl-->'; break; default:; } return $modx->getChunk('Template.Content_simple', array( 'pre_content' => $pre_content, 'post_content' => $post_content, ));
В этом сниппете мы не прописали дополнительное кеширование, но для нас сейчас главное то, что блоки внутри условий будут выполнены только тогда, когда условие будет выполнено. За счет этого мы на практике очень сильно сократили время на генерацию страницы. В итоге код не стал запутанней, но производительность выросла сильно.
И напоследок совет: не храните много кода в шаблонах. На многих сайтах страницы имеют одинаковые шапку и подвал на всем сайте (как правило только главная может существенно отличаться). Так вот создайте общие чанк header и footer, и подгружайте их в каждом шаблоне, чтобы не пришлось потом везде менять, если какой-то блок надо изменить. А то представьте сколько движений надо сделать, чтобы поменять дизайн и логику на сайте, на котором штук 15 вот таких шаблонов:
<!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml"> <head> [[$HEAD]] </head> <body> <!--header_wrap-start--> <div id="header_wrap"> <!--header-start--> <div id="header"> [[$header]] </div> <!--header-end--> </div> <div class="greenline"></div> <!--header_wrap-end--> <div class="banner"> <div class="banner_left"> [[$Menu.Slider]] </div> <div class="banner_right"> [[$Menu.top]] </div> <div class="clrflt"> </div> <div class="margin5"> </div> <div class="greenline"></div> <div class="clrflt"> </div> <div class="margin3"> </div> <marquee loop="infinite" behavior="altemate" bgcolor="#ddd" direction="left" height="20" width="100%" scrollamount="3" style="color:#076324; font-size:1.1em; padding:7px 0 0 0; font-family:Georgia, Times New Roman, Times, serif; font-style:italic; ">[[$Stroka]]</marquee> <div class="clrflt"> </div> </div> <!--banner-end--> <div id="content"> <!--content-start--> [[$Menu.left]] <div class="content_right fltright"> <div class="my_box"> [[Template.Content_simple]] </div> </div> <div class="clrflt"> </div> </div> <!--content-end--> <!--footer-start--> <div id="footer"> [[$Footer]] </div> <!--footer-end--> </body> </html>


Получил в личку очень интересный вопрос. Цитирую:
Не могли бы вы помочь мне с Рево — никак не могу найти тегов для вывода статистики по рендерингу страницы. Из Ево сработало часть: [^qt^] - время на запросы к базе данных [^q^] - запросов к базе данных [^p^] - время на работу PHP скриптов [^t^] - общее время на генерацию страницы [^s^] - источник содержимого (база или кэш) только 2 последних. А мне бы хотелось узнать сколько нужно памяти для рево и время генерации страницы поскольку балуюсь на локалке.
Взялся покопать. Как оказалось, в Эво с этим вообще проблем нет, все отлично выводится. А вот в Рево как бе совсем не все так просто…
В принципе и в Эво и в Рево эти теги остались без именения. Вот код Эво:
$out= str_replace("[^q^]", $queries, $out); $out= str_replace("[^qt^]", $queryTime, $out); $out= str_replace("[^p^]", $phpTime, $out); $out= str_replace("[^t^]", $totalTime, $out); $out= str_replace("[^s^]", $source, $out);
А вот код Рево:
$this->modx->resource->_output= str_replace("[^q^]", $queries, $this->modx->resource->_output); $this->modx->resource->_output= str_replace("[^qt^]", $queryTime, $this->modx->resource->_output); $this->modx->resource->_output= str_replace("[^p^]", $phpTime, $this->modx->resource->_output); $this->modx->resource->_output= str_replace("[^t^]", $totalTime, $this->modx->resource->_output); $this->modx->resource->_output= str_replace("[^s^]", $source, $this->modx->resource->_output);
Так если все тоже самое, почему не работает?
В Эво с этим было проще, так как там на все запросы один единственный класс и по сути единственная центровая функция на выполнение запроса function query($sql).
$tstart = $modx->getMicroTime(); if (!$result = @ mysql_query($sql, $this->conn)) { $modx->messageQuit("Execution of a query to the database failed - " . $this->getLastError(), $sql); } else { $tend = $modx->getMicroTime(); $totaltime = $tend - $tstart; $modx->queryTime = $modx->queryTime + $totaltime; if ($modx->dumpSQL) { $modx->queryCode .= "<fieldset style='text-align:left'><legend>Query " . ($this->executedQueries + 1) . " - " . sprintf("%2.4f s", $totaltime) . "</legend>" . $sql . "</fieldset><br />"; } $modx->executedQueries = $modx->executedQueries + 1; return $result; }
При чем обратите внимание на то, что если запрос не удалось выполнить, статистика не учитывается (не плюсуются ни счетчик запросов, ни время выполнения), что тоже не есть гуд, но с этим можно смериться.
В Рево все оказалось гораздо более запущенно… Давайте проследим выполнение одной из функций выборки $modx->getCollection(). Сам класс modX не имеет этого метода, а наследует его от объекта xPDO.
public function getCollection($className, $criteria= null, $cacheFlag= true) { return $this->call($className, 'loadCollection', array(& $this, $className, $criteria, $cacheFlag)); }
В данном случае xPDO не выполняет с ходу запрос, а выполняет данный метод на целевом объекте $className, который в свою очередь напрямую или через несколько колен является расширением от класса xPDOObject. То есть вызывая метод $modx->getCollection('modResource'); мы по сути выполняем modResource->loadCollection(); К слову, именно поэтому я и говорил, что выдернуть xPDO из MODX Revolution — почти нереал.
Ладно, отвлеклись, едем дальше. Смотрим метод xPDOObject::loadCollection(); Находим там вот это:
$rows= xPDOObject :: _loadRows($xpdo, $className, $criteria);
Вот собственно как раз здесь уже очень близки к месту, где выполняется сам запрос…
Смотрим xPDOObject::_loadRows(), и вот как раз здесь все самое интересное...
public static function & _loadRows(& $xpdo, $className, $criteria) { $rows= null; if ($criteria->prepare()) { if ($xpdo->getDebug() === true) $xpdo->log(xPDO::LOG_LEVEL_DEBUG, "Attempting to execute query using PDO statement object: " . print_r($criteria->sql, true) . print_r($criteria->bindings, true)); $tstart= $xpdo->getMicroTime(); if (!$criteria->stmt->execute()) { $tend= $xpdo->getMicroTime(); $totaltime= $tend - $tstart; $xpdo->queryTime= $xpdo->queryTime + $totaltime; $xpdo->executedQueries= $xpdo->executedQueries + 1; $errorInfo= $criteria->stmt->errorInfo(); $xpdo->log(xPDO::LOG_LEVEL_ERROR, 'Error ' . $criteria->stmt->errorCode() . " executing statement: \n" . print_r($errorInfo, true)); if (($errorInfo[1] == '1146' || $errorInfo[1] == '1') && $xpdo->getOption(xPDO::OPT_AUTO_CREATE_TABLES)) { if ($xpdo->getManager() && $xpdo->manager->createObjectContainer($className)) { $tstart= $xpdo->getMicroTime(); if (!$criteria->stmt->execute()) { $xpdo->log(xPDO::LOG_LEVEL_ERROR, "Error " . $criteria->stmt->errorCode() . " executing statement: \n" . print_r($criteria->stmt->errorInfo(), true)); } $tend= $xpdo->getMicroTime(); $totaltime= $tend - $tstart; $xpdo->queryTime= $xpdo->queryTime + $totaltime; $xpdo->executedQueries= $xpdo->executedQueries + 1; } else { $xpdo->log(xPDO::LOG_LEVEL_ERROR, "Error " . $xpdo->errorCode() . " attempting to create object container for class {$className}:\n" . print_r($xpdo->errorInfo(), true)); } } } $rows= & $criteria->stmt; } else { $errorInfo = $xpdo->errorInfo(); $xpdo->log(xPDO::LOG_LEVEL_ERROR, "Error preparing statement for query: {$criteria->sql} - " . print_r($errorInfo, true)); if (($errorInfo[1] == '1146' || $errorInfo[1] == '1') && $xpdo->getOption(xPDO::OPT_AUTO_CREATE_TABLES)) { if ($xpdo->getManager() && $xpdo->manager->createObjectContainer($className)) { if (!$criteria->prepare()) { $xpdo->log(xPDO::LOG_LEVEL_ERROR, "Error preparing statement for query: {$criteria->sql} - " . print_r($errorInfo, true)); } else { $tstart= $xpdo->getMicroTime(); if (!$criteria->stmt->execute()) { $xpdo->log(xPDO::LOG_LEVEL_ERROR, "Error " . $criteria->stmt->errorCode() . " executing statement: \n" . print_r($criteria->stmt->errorInfo(), true)); } $tend= $xpdo->getMicroTime(); $totaltime= $tend - $tstart; $xpdo->queryTime= $xpdo->queryTime + $totaltime; $xpdo->executedQueries= $xpdo->executedQueries + 1; } } else { $xpdo->log(xPDO::LOG_LEVEL_ERROR, "Error " . $xpdo->errorCode() . " attempting to create object container for class {$className}:\n" . print_r($xpdo->errorInfo(), true)); } } } return $rows; }
Специально выдернул всю функцию, дабы лучше показать насколько все запущенно…
Первое, на что обратим внимание, это на то, что счетчик запросов и времени исполнения засунут в блок if (!$criteria->stmt->execute()) {}, то есть уже косяк, ибо получается, что счетчик работает только тогда, когда запрос не выполняется, при чем именно из-за SQL-ошибки или типа того. Кстати, сразу забегая вперед, обращаю ваше внимание на то, что именно метод $criteria->stmt->execute() — это и есть метод, который выполняет сам SQL-запрос. Но почему это проблема, раскрою дальше.
Подтвердим теорию на практике. Создаем пользовательский запрос:
$q = $modx->newQuery('modResource'); $q->where(array( 'id' => 5, )); $docs = $modx->getCollection('modResource', $q);
Сразу скажу, что документа с id=5 не существует, потому результат будет нулевой. При этом SQL-ошибки нет, потому и запрос считается выполненным. Выполняем и смотрим результат:
Query Time: 0.0000 s Request: 0 PHP Exec: 0.0760 s Total time: 0.0760 s
Теперь испортим наш запрос
$q = $modx->newQuery('modResource'); $q->where(array( 'idddddddddddddd' => 5, )); $docs = $modx->getCollection('modResource', $q);
Сами понимаете, что колонки idddddddddddddd в этой таблице нет, а значит будет SQL-ошибка. Выполняем и смотрим:
Query Time: 0.0000 s Request: 1 PHP Exec: 0.0800 s Total time: 0.0800 s
То есть выполнен один запрос. Время запроса нулевое скорее всего из-за того, что база пустая, не будем обращать внимание. Если что, пусть кто-нибудь проверит это у себя и отпишется в комменте.
Итак, это первый косяк. Попробуем его чуть-чуть исправить и переписать блок функции так:
if ($criteria->prepare()) { if ($xpdo->getDebug() === true) $xpdo->log(xPDO::LOG_LEVEL_DEBUG, "Attempting to execute query using PDO statement object: " . print_r($criteria->sql, true) . print_r($criteria->bindings, true)); $tstart= $xpdo->getMicroTime(); if (!$criteria->stmt->execute()) { $errorInfo= $criteria->stmt->errorInfo(); $xpdo->log(xPDO::LOG_LEVEL_ERROR, 'Error ' . $criteria->stmt->errorCode() . " executing statement: \n" . print_r($errorInfo, true)); if (($errorInfo[1] == '1146' || $errorInfo[1] == '1') && $xpdo->getOption(xPDO::OPT_AUTO_CREATE_TABLES)) { if ($xpdo->getManager() && $xpdo->manager->createObjectContainer($className)) { if (!$criteria->stmt->execute()) { $xpdo->log(xPDO::LOG_LEVEL_ERROR, "Error " . $criteria->stmt->errorCode() . " executing statement: \n" . print_r($criteria->stmt->errorInfo(), true)); } } else { $xpdo->log(xPDO::LOG_LEVEL_ERROR, "Error " . $xpdo->errorCode() . " attempting to create object container for class {$className}:\n" . print_r($xpdo->errorInfo(), true)); } } } $tend= $xpdo->getMicroTime(); $totaltime= $tend - $tstart; $xpdo->queryTime= $xpdo->queryTime + $totaltime; $xpdo->executedQueries= $xpdo->executedQueries + 1; $rows= & $criteria->stmt; }
Мы конечно таким образом не отслеживаем все запросы с учетом ошибок, автосоздания таблиц и т.п., но хотя бы выполненные запросы отслеживаем. Проверяем:
Query Time: 0.0000 s Request: 7 PHP Exec: 0.0680 s Total time: 0.0680 s
То есть мы видим, что выполнено 7 запросов. Уже что-то.
Но, к сожалению, на этом наши беды не заканчиваются… Еще проблема:Кто знает xPDO получше, мог в своей практике использовать метод $modx->getCollectionGraph(); Описанным выше путем доходим до того, что это на самом деле метод xPDOObject::loadCollectionGraph(); Смотрим код функции:
public static function loadCollectionGraph(xPDO & $xpdo, $className, $graph, $criteria, $cacheFlag) { $objCollection = array(); if ($query= $xpdo->newQuery($className, $criteria, $cacheFlag)) { $query = $xpdo->addDerivativeCriteria($className, $query); $query->bindGraph($graph); $rows = array(); $fromCache = false; $collectionCaching = (integer) $xpdo->getOption(xPDO::OPT_CACHE_DB_COLLECTIONS, array(), 1); if ($collectionCaching > 0 && $xpdo->_cacheEnabled && $cacheFlag) { $rows= $xpdo->fromCache($query); $fromCache = !empty($rows); } if (!$fromCache) { if ($query->prepare()) { if ($query->stmt->execute()) { $objCollection= $query->hydrateGraph($query->stmt, $cacheFlag); } else { $xpdo->log(xPDO::LOG_LEVEL_ERROR, "Error {$query->stmt->errorCode()} executing query: {$query->sql} - " . print_r($query->stmt->errorInfo(), true)); } } else { $xpdo->log(xPDO::LOG_LEVEL_ERROR, "Error {$xpdo->errorCode()} preparing statement: {$query->sql} - " . print_r($xpdo->errorInfo(), true)); } } elseif (!empty($rows)) { $objCollection= $query->hydrateGraph($rows, $cacheFlag); } } return $objCollection; }
И что мы видим? Главное, мы НЕ видим — не видим счетчика запросов и т.п. А это еще один минус по статистике.
То есть мы видим, что для разных запросов используются различные методы, которые между собой не пересекаются. Это же касается методов xPDOObject::save(), xPDOObject::remove() и т.п.
Продемонстриую еще пример. Есть метод xPDO::exec();
public function exec($query) { if (!$this->connect(null, array(xPDO::OPT_CONN_MUTABLE => true))) { return false; } $tstart= $this->getMicroTime(); $return= $this->pdo->exec($query); $tend= $this->getMicroTime(); $totaltime= $tend - $tstart; $this->queryTime= $this->queryTime + $totaltime; $this->executedQueries= $this->executedQueries + 1; return $return; }
Данный метод выполняет произвольный SQL-запрос. Выполним вот это:
$q = $modx->newQuery('modResource'); $q->prepare(); $sql = $q->toSQL(); $modx->exec($sql); $modx->exec($sql); $modx->exec($sql);
В данном примере мы генерируем SQL $sql = $q->toSQL() и выполняем его $modx->exec($sql); И да, счетчик запросов щелкает.
Query Time: 0.0040 s Request: 10 PHP Exec: 0.0680 s Total time: 0.0720 s
И это хорошо. Но плохо то, что я не видел, чтобы MODX для себя где-то выполнял exec(). Потому можете воткнуть в exec() exit(), и у вас сайт не перестанет работать. То есть сбор статистики здесь мало смысла имеет.
«А как же $stmt->execute()? Ведь он же встречается практически во всех перечисленных функциях.» спросят наиболее наблюдательные… А вот здесь как раз и кроется еще одна беда, на которую я намекал выше. Дело в том, что xPDO это расширение от PDO, которое вшито в Си-расширение PHP, и на методы которого напрямую мы не можем воздействовать, а $stmt — это PDOStatement.
Таким образом ответ на вопрос пользователя: указанные теги для вывода статистики не потерялись, просто в них информация не актуальная выводится. И чтобы заставить все это дело заработать, придется вклиниться во многие функции xPDO. При чем уверен, здесь не сможет решить этот вопрос ни один модуль для Ревы.
Опять пишу большой топик в ответ на чужой вопрос. Топик раскрывает некоторые преимущества работы с xPDO и написал одну хитрость для не посвященных. Но в целом топик оформлен ужасно и не является исчерпывающей мануалой, потому заходите на свой страх и риск, и не пишите сообщений типа «фуууу...». Так что же дает xPDO лично мне? 1) Сравним эти 2 запроса:
$sql = array( select => "a.*, ah.description, ah.conditions, ah.features, ah.action_end, ah.full_price, ah.discount, ah.sale_price, ah.cost_discount, adrs.name as firmname, (select act.cat_cat_id from ".self::$tables['actions_categories']." as act where act.action_action_id = a.action_id limit 1) as cat_id, (select cat.name from ".self::$tables['actions_categories']." as act join ".self::$tables['categories']." as cat on cat.cat_id = act.cat_cat_id where act.action_action_id = a.action_id limit 1) as category, c.pagetitle, c.longtitle, c.id, c.uri, cp.id as partnerDocID, cp.uri as partnerDocUri, cp.published as parentIsPublished, cp.deleted as parentIsDeleted, cp.hidemenu as parentIsHideMenu, ai.value as action_image, pi.value as partner_logo, (select count(*) from ".self::$tables['user_cupon']." as uc where uc.action_action_id = a.action_id) as bought, metro.station as metro", from => self::$tables['actions']." as a join ".self::$tables['actions_history']." ah ON ah.action_action_id = a.action_id and ah.actstat_actstat_id = 5 and now() between ah.start_date and ah.end_date AND NOW() < ah.action_end join ".self::$tables['actions_status']." as `as` on `as`.actstat_id = ah.actstat_actstat_id join ".self::$tables['partners']." as p on p.partner_id = a.partner_partner_id join ".self::$tables['partners_history']." as ph on ph.partner_partner_id = p.partner_id and now() between ph.start_date and ph.end_date join ".self::$tables['partners_statuses']." as ps on ps.partstatus_id = ph.partstatus_partstatus_id". " join ".self::$tables['catalog_content']." as cc on cc.action_action_id = a.action_id join ".self::$tables['catalog_content']." as ccp on ccp.partner_partner_id = p.partner_id join ".self::$tables['site_content']." as c on cc.cont_cont_id = c.id and c.published = 1 and c.hidemenu = 0 and c.deleted = 0 join ".self::$tables['tv_values']." as ai on ai.contentid = c.id and ai.tmplvarid = ".self::$TVs['actionImg']." left join (select aa.action_action_id, adh.name from ".self::$tables['addresses']." as ca join ".self::$tables['addresses_history']." as adh on adh.adrs_adrs_id = ca.adrs_id and adh.adrstat_adrstat_id = 1 and now() between adh.start_date and adh.end_date join ".self::$tables['actions_addresses']." as aa on ca.adrs_id = aa.adrs_adrs_id and aa.status = 1 and now() between aa.start_date and aa.end_date where adh.city in (". implode(",", $cities). ") {$adrs_where} group by aa.action_action_id ) as adrs on adrs.action_action_id = a.action_id left join ".self::$tables['site_content']." as cp on ccp.cont_cont_id = cp.id left join ".self::$tables['tv_values']." as pi on pi.contentid = cp.id and pi.tmplvarid = ".self::$TVs['partnerLogo']." left join ( SELECT caa.action_action_id, if(cm.complex_name is not null && cm.complex_name != '', cm.complex_name, cm.station) as station FROM modx_catalog_actions_addresses as caa join modx_catalog_metro_relations as cmr on cmr.adrs_adrs_id = caa.adrs_adrs_id join modx_catalog_metro as cm on cm.metro_id = cmr.metro_metro_id where now() between caa.start_date and caa.end_date group by 1 ) as metro on metro.action_action_id = a.action_id ". ($not_sended == true ? " left join modx_catalog_actions_sended as `asn` on `asn`.action_action_id = a.action_id ": "")." where `as`.status = 'Активна' and ps.status = 'Активный' and (adrs.action_action_id is not null OR ah.all_cities = '1') {$where} order by c.publishedon desc", limit => self::$limit, offset => self::$offset );
и
// Получаем города, которые используются для этого хоста $cities = self::get_host_cities(); $where = array( 'now() between ah.start_date and ah.end_date', 'ah.actstat_actstat_id' => 5, 'NOW() < ah.action_end', 'now() between ph.start_date and ph.end_date', 'ph.partstatus_partstatus_id' => 2, 'c.published' => 1, 'c.hidemenu' => 0, 'c.deleted' => 0, 'ai.tmplvarid' => self::$TVs['actionImg'], 'now() between aa.start_date and aa.end_date', 'aa.status' => 1, 'now() between adh.start_date and adh.end_date', ); $q = self::$modx->newQuery('Actions'); $q->select(array( 'Actions.*', 'ah.description', 'ah.conditions', 'ah.features', 'ah.action_end', 'ah.full_price', 'ah.discount', 'ah.sale_price', 'ah.cost_discount', 'adh.name as firmname', 'c.pagetitle', 'c.longtitle', 'c.id', 'c.uri', 'cp.id as partnerDocID', 'cp.uri as partnerDocUri', 'cp.published as parentIsPublished', 'cp.deleted as parentIsDeleted', 'cp.hidemenu as parentIsHideMenu', 'ai.value as action_image', 'pi.value as partner_logo', "if(cm.complex_name is not null && cm.complex_name != '', cm.complex_name, cm.station) as metro" )); $q->innerJoin('ActionsHistory', 'ah', 'ah.action_action_id = Actions.action_id'); $q->innerJoin('Partners', 'p', 'p.partner_id = Actions.partner_partner_id'); $q->innerJoin('PartnersHistory', 'ph', 'ph.partner_partner_id = p.partner_id'); $q->innerJoin('Content', 'cc', 'cc.action_action_id = Actions.action_id'); $q->innerJoin('Content', 'ccp', 'ccp.partner_partner_id = p.partner_id'); $q->innerJoin('modResource', 'c', 'cc.cont_cont_id = c.id'); $q->innerJoin('modResource', 'cp', 'ccp.cont_cont_id = cp.id'); $q->innerJoin('modTemplateVarResource', 'ai', 'ai.contentid = c.id'); $q->innerJoin('ActionsAddresses', 'aa', 'aa.action_action_id = Actions.action_id'); $q->innerJoin('AddressesHistory', 'adh', 'adh.adrs_adrs_id = aa.adrs_adrs_id'); $q->leftJoin('MetroRelations', 'cmr', 'cmr.adrs_adrs_id = aa.adrs_adrs_id'); $q->leftJoin('Metro', 'cm', 'cm.metro_id = cmr.metro_metro_id'); $q->leftJoin('modTemplateVarResource', 'pi', 'pi.contentid = cp.id AND pi.tmplvarid = '.self::$TVs['partnerLogo']); if($not_sended == true){ $q->leftJoin('ActionsSended', 'asn', 'asn.action_action_id = Actions.action_id'); $where['asn.actsnd_id'] = NULL; } $q->where($where); $q->andCondition( "(adh.city IN (".implode(",", $cities).") OR ah.all_cities = '1')" ); if($not_sended == false){ self::$rows = self::$modx->getCount('Actions', $q); $q->limit(self::$limit, self::$offset); } // Подсчитываем кол-во строк $q->groupby('Actions.action_id'); $q->sortby('c.publishedon', 'DESC'); if(!$q->prepare()){ return false; } if(!$q->stmt->execute()){ return false; } $result__ = $q->stmt->fetchAll(PDO::FETCH_ASSOC);
Да, каждый пример — это один запрос и да, иногда нужны и такие большие запросы. Так вот, как считаешь, какой легче будет сопровождать и дорабатывать? Лично я отвечу: второй запрос (который на xPDO) сопровождать в разы легче.
2. Если у тебя таблица изменилась, тебе не надо лазить по всему проекту и искать, во всех ли у тебя запросах прописаны все необходимые колонки…
3. Простейшая для xPDO операция:
$object = $modx->getObject('object', $where); $object->fromArray($data); $object->save();
На чистой связке PHP+Mysql это как бе гемор, так как надо проверить какие данные есть в массиве $data, какие колонки могут этому соответствовать, а какие нет, какие типы данных и какие преобразования могут понадобиться… Дофига гемора.
Это маленькая часть из преимуществ.
Далее.
SELECT * FROM atable WHERE id=$parent AND id>(MIN+((MAX-MIN)/COUNT)*offset LIMIT numrows
Это вообще не проблема. Подробней читаем здесь. А конкретно по поводу WHERE id=$parent AND id>(MIN+((MAX-MIN)/COUNT)*offset: надо правильно массив условий собирать.
$where = array( 'id' => $parent, 'id > (MIN+((MAX-MIN)/COUNT)*offset' )
Не 'id:>' =>, а именно так, как я написал, потому что иначе xPDO запрос соберет так, как будто '(MIN+((MAX-MIN)/COUNT)*offset' — это название колонки.
Но все же одно ограничение действительно есть, которое я встречал. На xPDO нельзя собрать запроса типа UNION ALL. Но это так редко используется…
И под конец компенсация за мой наезд: твоя проблема в том, что ты не видишь SQL?
$q = $modx->newQuery($class); $q->select($select); $q->where($where); $q->prepere(); <strong>print $q->toSQL();</strong>
И вот тебе чистый SQL:-)
Увидел вопрос, который остается без ответа уже 12 часов, и решил написать статью по этому поводу (вообще писал коммент, но он получился слишком большой :-)). Уверен, пригодится многим. Тема очень деликатная, так как политики безопасности в Рево настолько совершенны, на сколько и сложны.
Автору: Могу дать наколку. Смотрите сниппет Wayfinder. Там вам поможет метод Wayfinder.class.php ::getData(); В нем много чего написано. Наиболее интересные моменты, это
$c = $this->modx->newQuery('modResource'); $c->leftJoin('modResourceGroupResource','ResourceGroupResources'); $c->query['distinct'] = 'DISTINCT'; if ( $this->modx->user->hasSessionContext('mgr') && $this->modx->hasPermission('view_unpublished') && $this->_config['previewUnpublished'] ) { } else { $c->where(array('modResource.published:=' => 1)); }
и
if ( (!empty($this->_config['permissions'])) && (!$doc->checkPolicy($this->_config['permissions'])) ){ continue; }
То есть проверять надо во-первых, опубликован документ или нет, и есть ли право просматривать неопубликованные (то есть это относится к сфере проверки глобальных прав $modx->hasPermission()), а во-вторых, проверять локальные права на объект, то есть $doc->checkPolicy();
Это в общем. А в частности стоит уяснить, что локальные ограничения к ресурсу могут быть ограничены только на уровне Групп ресурсов? по-этому Wayfinder в целях экономии ресурсов и проверяет сразу, относится ресурс к какой-либо группе или нет, так как если он не в группе, то никаких прав и не может быть прописано, и не стоит выполнять более нагруженную операцию проверки прав. То есть действуют правила «Если документ не отнесен к группе ресурсов, то можно все», и «Если на группу ресурсов в конкретном контексте не назначено хоть одной политики доступа, то тоже можно все».
Вот вам пример (данный скрипт я выполнял через админку, потому делаю смену контекста и текущего пользователя):
print '<pre>'; $doc_id = 7; $user_id = 9; $doc = $modx->getObject('modResource', $doc_id); $modx->switchContext('web'); $modx->user = $modx->getObject('modUser', $user_id); print_r($doc->findPolicy('web')); // Можно указать конкретный контекст для проверки print "<br />". ($doc->checkPolicy('list') ? '1' : '0');
В данном примере вот этот метод $doc->findPolicy('web') и вернет вам массив назначенных доступов к ресурсу. Если вы увидите что-то типа
Array ( [modAccessResourceGroup] => Array ( [1] => Array ( [0] => Array ( [principal] => 2 [authority] => 1000 [policy] => Array ( [load] => 1 ) ) ) ) )
значит к ресурсу назначены права доступа. В нашем случае это load. То есть пользователь должен иметь права load к этому ресурсу. Их вы и проверите методом $doc->checkPolicy('load')

Ежели вы увидите просто пустой массив, значит прав на ресурс не назначено, а значит любой пользователь имеет доступ. Но имейте ввиду, что в разных контекстах могут быть назначены разные права к группам ресурсов. Таким образом можно проверить любые права, типа load, view и т.п. (см. Система -> Безопасность -> Контроль доступа -> Политики доступа)