В последнее время довольно часто приходится писать скрипты, которые запускаются автономно. Т.е. не в бэкенде MODX. Например роутеры для Ext.Direct, разные утилиты для обработки страниц(выгрузки для Яндекса, интернет-магазинов и т.д.).

Часто на форумах и в статьях можно увидеть такую конструкцию:

define('MODX_API_MODE', true);  
require_once ('/modx-core-path/index.php');
$modx->initialize('web');
    

При чем этот код работает. Казалось бы забудь и не парься. Если б не родная документация:

Deprecated Example

This example is deprecated. So better change your code, if you still use the MODX_API_MODE.
In You can also use MODX in its API mode, and then include the primary index.php file for your site:
define('MODX_API_MODE', true);
// Full path to the index
require_once('/path/to/modx/public_html/index.php');
$modx->initialize('mgr');

Поэтому правильнее будет вот так:

    
require_once '/modx-core-path/config.core.php';
require_once MODX_CORE_PATH.'model/modx/modx.class.php';
$modx = new modX();
$modx->initialize('web');
$r=$modx->resource; // текущая страница  
$id=$r->get('id');
$r->set('pagetilte','Я ехала домой...');
$r->save();
В новых скриптах использую теперь только так. И все вертится.

Признаюсь, я довольно долго игнорировал XPDO. И когда это было можно, старался использовать "чистый" mySQL. Но постепенно оценил все удобства, особенно при сложных запросах и при выборке подчиненных таблиц. Таких как TV, у которых не простая связь с ресурсами(через таблицу-посредника). Встроенные объекты(ресурсы, чанки и проч.) уже загружены в систему. А пользовательские таблицы, вернее пакеты XPDO, подгружаются по мере необходимости. Без этого XPDO их не увидит. Некоторые авторы советуют куда-то их прописать и тогда система их сама загрузит при активации. Не думаю, что это хорошая идея. MODX и без того грузит огромное количество объектов. Вот рабочий пример подключения собственного пакета с выводом отладочной информации(использовался при разработке роутера):

<?php
require_once '/modx-core-path/config.core.php';
require_once MODX_CORE_PATH.'model/modx/modx.class.php';
$modx = new modX();
$modx->initialize('mgr');
$modx->addPackage('wwwchat',MODX_CORE_PATH.'components/wwwchat/model/');
$c = $modx->newQuery('ChatItem');
$c->prepare();
echo '<br/>'.$c->toSQL();
$total = $modx->getCount('ChatItem',$c);
echo '<br/>$total='.$total;
$message = $modx->getObject('ChatItem', 30);
$arr=$message->toArray();
echo '<br/>$arr='.nl2br(print_r($arr,true));
    

Это getObject, getCollection, getCollectionGraph, getIterator. Вообще у всех подобных функций похожий синтаксис.

  • Первый параметр - имя класса. Ее можно узнать из модели
  • Второй параметр - критерий отбора. Тут есть так называемый называемый "синтаксический сахар", что некоторых сбивает с толку.
    • Если указана просто целое число(напр.: 56) - интерпретируется как array('id'=>'56')
                       
      $res=$modx->getObject("modResource",56);
      
    • Можно сразу перечислить параметры выбора - array('pagetitle:LIKE'=>'%MODX%','isfolder'=>'0')
                       
      $res=$modx->getObject("modResource",array('pagetitle:LIKE'=>'%MODX%','isfolder'=>'0'));
      
    • А можно еще круче. Использовать "странную" функцию $criteria=$modx->newQuery(.......).
      $criteria = $modx->newQuery('modResource');
      $criteria->where(array('isfolder'=>0));
      $criteria->sortby('id','DESC');
      $criteria->limit(5);
      $arr_resource=$modx->getIterator('modResource',$criteria);
      
      ... мне кажется, можно было как-то красивее реализовать. По сути newQuery возвращает не запрос, как можно подумать, а критерий отбора. Меня такое название надолго сбило с толку.
      • $criteria->select('pagetitle,id') - для выборки определенных полей. Забавно, что $res->toArray() все равно выберет все поля из записи. Они подгрузятся. Чтобы в массиве остались только выбранные поля нужно - $res->toArray("",false,true)
      • 'pagetitle:LIKE'=>'%MODX%' и 'pagetitle:LIKE'=>'%modx%' идентичны, т.е. в XPDO выборка регистронезависима. Нет необходимости применять " UPPER(pagetitle) LIKE %MODX% "
      • Вот тут на хорошем английском языке подробно описаны прочие параметры выборки. Только "BETWEEN" я там так и не нашел.
Тут я в кучу собрал примеры. Как памятка.
$c->innerJoin('Owner','User'); 
$c->where(array(
    'Owner.name:LIKE' => '%a%',
    'Box.width:>=' => 10,
    'Box.height:!=' => 2,
    'Box.color:IN' => array('red','green','blue'),
));
$c->sortby('Box.name','ASC');
$c->sortby('Box.height','DESC');
$c->limit(4);
======== Надо помнить что по умолчанию критерии соединяются по "AND" =====
$c->where(array(
    array(
        'first_name:=' => 'Bob',
        array(
            'OR:last_name:LIKE' => 'Boblablaw',
            'AND:gender:=' => 'M',
        ),
    ),
    'password:!=' => null,
)); 

отобразится в:

(
  (      `Person`.`first_name` = 'Bob'
    OR ( `Person`.`last_name` LIKE 'Boblablaw' AND `Person`.`gender` = 'M' )
  )
  AND password IS NOT NULL
)
=============================================================================
$c->where(array(
  'name:=' => 'John', /* Equal To */
  'name:!=' => 'Sue', /* Unequal To */
  'age:>' => '21', /* Greater Than */
  'age:>=' => '21', /* Greater Than or Equal To */
  'age:<' => '18', /* Less Than */
  'age:<=' => '18', /* Less Than or Equal To */
  'search:LIKE' => 'Term', /* LIKE statement */
  'field' => null, /* check for NULL */
  'ids:IN' => array(1,2,3), /* IN statement */
));

$c->where(array('width:NOT LIKE' => '%15%'));
$c->where(array('width:NOT IN' => array(15,16,17,20)));

$c->where(array('width:IS' => null));

$c->groupby('type');
$c->andCondition(array('height' => 4)); - явное задание AND
$c->orCondition(array('width' => 12,)); - явное задание OR
                            другой пример для "OR":
$c->where(array(
        'published' => 1,
                array(
                        'pub_date' => 0,
                        'OR:pub_date:<=' => time(),
                ),
                array(
                        'unpub_date' => 0,
                        'OR:unpub_date:>' => time(),
                ),              
        )
);
                           //переопределение поведения по умолчанию 
$c->where(array('width' => 15));
$c->where(array('width' => 10),xPDOQuery::SQL_OR); //теперь будет применяться "OR" в пределах уровня

$c->where(array(
    array('width' => 15),
    array('width' => 10)
),xPDOQuery::SQL_OR); 
                    - то же самое, что и -
$c = $xpdo->newQuery('Box');
$c->where(array(
    array('width' => 15),
    array('OR:width:=' => 10)
));

$c->where(array(
   array(
      'width:=' => 15, //!!!  note that adding 'AND:' or 'OR:' in front of the attribute, an operator must be used ':='
      'OR:width:=' => 10
   ),
   array(
      'AND:height:>=' => 10,
      'AND:height:<=' => 15
   )
));
==========================  Присоединения ==================================
$c->innerJoin('Owner','Owner'); // (класс, алиас)
$c->where(array('Owner.name' => 'Mark',));

$c = $modx->newQuery('modUser');
$c->innerJoin('modUserProfile','Profile'); 
$c->where(array('modUser.username' => $email,));
$c->orCondition(array('Profile.email' => $email,));    
$user = $modx->getObject('modUser', $c);

$c->select($xpdo->getSelectColumns('Box'));

$c->select(array('Owner.name'));

$c->leftJoin('Owner','Owner');
$c->rightJoin('Owner','Owner');
============================================================================

$c->limit(10,10); - выборка с 11 по 20 записи (сколько_выбрать, смещение=0)

$c->select($xpdo->getSelectColumns('Box','Box','',array('id','name')));
$c->select('id,username');

//назначение алиаса, не очень понятно пока 
$c = $xpdo->newQuery('OtherBox');  
$c->setClassAlias('Box');
$c->where(array('Box.name' => 'RoundBox',));
$otherBoxes = $xpdo->getCollection('OtherBox',$c);

$c->sortby('name','ASC');
$c->sortby('RAND()');  - перемешивание
$c->sortby('FIELD(modResource.id, 4,7,2,5,1 )'); - назначение функции сортировки

================================ ГРАФЫ ======================================
$c = $modx->newQuery('myTable');
$c->where(array('Profile.fullname:LIKE' => '%Company%'));
$records = $this->ParentCMS->getCollectionGraph('myTable', '{"modUser": {"Profile":{} } }',$c);

$c = array();
$c['modTemplateVarResource.tmplvarid'] = 9;
$c['modTemplateVarResource.value:IN'] = array('Red','Green','Blue');
$c['Resource.template'] = 2;
$c = $modx->newQuery('modTemplateVarResource', $criteria);
$tvrs = $modx->getCollectionGraph('modTemplateVarResource','{"Resource":{}}', $c);
        ... ну это уж чистое издевательство над мозгом)))
    

Эта фунция возвращает только один объект, соответсвующий одной строке из таблицы. Даже если критерий выбора соответствует нескольким объектам(строкам таблицы), возвращается первый из выборки.

                 
$res=$modx->getObject("modResource",45);
$res=$modx->getObject("modResource",array('pagetitle:LIKE'=>'%rem%');
$res=$modx->getObject("modResource",$criteria);

Возвращает массив объектов. Очень ресурсоемкая функция. Особенно при большом количестве ресурсов(страниц). На некоторых сайтах может доходить до десятков тысяч страниц. Например товары в магазине. Поэтому следует применять с осторожностью и только при ограниченных выборках. Я предпочитаю использовать getIterator.

$c = $modx->newQuery('modResource');
$c->where(array('isfolder'=>0));
$c->sortby('id','DESC');
$c->limit(20);

$arr_resource=$modx->getCollection('modResource',$c);
foreach ($arr_resource as $res) {
    echo '<br/>',$res->get('id'),' parent=',$res->get('parent');
}

Аналогична getCollection. Но не загружает в память сразу всю выборку. При обходе (foreach) при каждой итерации создает только один объект. Поэтому предпочтительнее при больших выборках. При малых выборках, конечно, уступает getCollection по быстроте.

$c = $modx->newQuery('modResource');
$c->where(array('isfolder'=>0));
$c->sortby('id','DESC');
$c->limit(20);

$arr_resource=$modx->getIterator('modResource',$c);
foreach ($arr_resource as $res) {
    echo '<br/>',$res->get('id'),' parent=',$res->get('parent');
}

Это чудо служит для выборки сразу и объектов и связанных с ними объектов(related objects).

    
$boxes = $xpdo->getCollectionGraph('Box', '{"BoxColors":{"Color":{}}}', array('Box.width' => 40));
foreach ($boxes as $box) {
    foreach ($box->getMany('BoxColors') as $boxColor) {
        echo "A box with width of 40 and a color of " . $boxColor->getOne('Color')->get('name') . " was found.\n";
    }
}    
!!!Здесь используются имена классов в моделях, но не имена таблиц:
	
<object class="MyClassName" table="my_class_name" extends="xPDOObject">
Самое удивительное, что если сделать так:
    
$boxes = $xpdo->getCollection('Box', array('width' => 40));
foreach ($boxes as $box) {
    foreach ($box->getMany('BoxColors') as $boxColor) {
        echo "A box with width of 40 and a color of " . $boxColor->getOne('Color')->get('name') . " was found.\n";
    }
}    
- то получим такой же результат))).

Вся тонкость в том, что в первом случае не происходит многократных запросов к базе. Т.к. все связанные объекты уже выбраны. А $obj->getOne() и $obj->getMany() обращаются к уже существующей выборке. Круто....

Работа с выборкой, доступ к полям. Сохраниение.

После получения строки таблицы(объекта) доступны следующие методы (пусть $res - выбранная строка):

  • $res->get('id') - получить поле 'id'
  • $res->set('name','Настя') - записать в поле 'name' строку 'Настя'
  • $res->toArray - запись в массив
  • $res->toJSON - запись в JSON
  • $res->fromArray - загрузка полей из массива
  • $res->fromJSON - загрузка полей из JSON
  • $res->save() - сохранить строку
  • $res->remove() - удаление строки
$res=$modx->resource; - текущий ресурс modx(страница); $modx->resourceIdentifier - ID текущего документа(страницы)

Создание новой строки (объекта).

Создание новой строки(объекта):

  $myBox = $xpdo->newObject('Box');
  $myBox->fromArray(array(
   'id' => 23,
   'width' => 5,
   'height' => 5,
  ),'',true);
  echo $myBox->get('id'); // prints '23'
  
Здесь 3 параметр позволяет записать первичный ключ(поле 'ID')
 $myBox->fromArray(array(
  'width' 5',
  'notRealField' => 'boo',
),'',false,false,true);
 
Тут 5-ый парметр позволяет записать несуществующее поле, которое будет доступно как свойство объекта. В данном случае как $myBox->get('notRealField').

Ресурсы(modResource).

Создание ресурса:

    
$myBox = $modx->newObject('modResource');
$alias='item1';
$myBox->fromArray(array(
    'id' => 500045,
    'parent' =>6,
    'pagetitle' =>"Медогонка RX-9008",
    'alias' => $alias,
    'description' => $descr,
    'content' => '',
    'isfolder' =>1
  ),'',true);  
ИД можно назначить свое, что очень удобно при импорте товаров и каталогов из складских программ.

То же через процессор:

$response = $modx->runProcessor('resource/create', array(
    'parent' => 1,
    'pagetitle' => 'Мед Тотемский',
    'alias' => 'medTotema',
    'description' => "Мед экологически чистый с Тотемских полей"
));
 
if($response->isError()){
    echo "Произошла ошибка". $response->getMessage();
}
else{
    $res = $response->getObject();
    echo  "Cоздана страница <b>{$res['pagetitle']}</b>, id={$res['id']}";
}

Переменные шаблона(modTemplateVar).

Нет смысла через API подключаться к таблице TV без привязки к ресурсу. Общая схема работы с TV.

  • Создаем объект modResource
  • Используем методы этого объекта для работы с TV
Например:
$page = $modx->getObject('modResource', 500045);
return $page->getTVValue('price');    
    
$page = $modx->getObject('modResource', 500045);
$page->setTVValue('price',4500);
$page->save();
    

Если уж приспичит, можно работать непостредственно с таблицей TV. Пример из официальной документации:

$tvr = $modx->getObject('modTemplateVarResource', array(
  'tmplvarid' => $tvId,
  'contentid' => $resourceId
));
if ($tvr) {
  return $tvr->get('value');
}
else {
  $tv = $modx->getObject('modTemplateVar', $tvId);
  if ($tv) return $tv->get('default_text');
}
return '';

Обработка шаблонов.

Алгоритм такой:

  1. Заполучить текст для обработки(содержание чанка)
    • $Tpl=file_get_contents($_SERVER['DOCUMENT_ROOT']."/assets/......./myTpl.tpl");
    • или
    • $Tpl=$modx->getChunk (string $chunkName, [array $properties = array ()]);
      * если вторым аргументом передан ассоциативный массив,
      то в шаблоне произойдет замена плейсхолдеров,
      при их соответсвии ключам переданного массива
    • или
    • $Tpl='<img class="" src="[[+image:phpthumbof=`h=300`]]" title="[[+title]]" descr="[[+descr]]"/>';
  2. И пропарсить его
    • Создаем объект чанка
      $uniqid = uniqid();
      $chunk = $modx->newObject('modChunk', array('name' => "tmpCnank-{$uniqid}"));
      $chunk->setCacheable(false);
                          
    • Назначаем плейсхолдеры:
      $params['param1']=$param1;
      $params['param2']=$param2;
      $params['param3']=$param3;
      
      В Revo очень удобные прлейсхолдеры. Можно назначить так:
       $params['compPar']['par1']=$param1;
       $params['compPar']['par2']=$param2;
      
      или сразу весь массив:
       $arrParams=array();
       $arrParams[par1]=$param1;
       $arrParams[par2]=$param2;
       $params['compPar']=$arrParams;
      
      и в шаблоне можно назначить так:
          ...
          <li>[[+param1]]</li>
          <li>[[+param2]]</li>
          
      
    • Запускаем парсер:
      $output.=$chunk2->process($params,$Tpl);

Замечу, что есть тьма функций в названии которых есть слово "parser". Не было времени их все изучить, поэтому вероятно возможны и другие способы сделать то же самое. Можно отдельно запускать сниппеты. Там свои тонкости. По мне проще сниппеты обернуть в литеральный чанк и пропарсить указанным выше способом. Есть функции, которые просто заменяют тэги в чанках на плейсхолдеры. Причем только за один проход. И тут можно применить литеральный аналог:

    [[$myChank? &param1=`param1` &param2=`param2`]]
В отличие от Evo можно в чанк сразу загрузить плейсхолдеры.

Разное.

MODX дохнет тихо. Чтобы вывести ошибки в скриптах часто помогает это:

error_reporting(E_ALL | E_STRICT);
ini_set('display_errors', 1);                      
    

Плейсхолдеры для тэгов типа [ [+cinema]] можно установить так:

$modx->placeholders['cinema']='Дело было в Пенькове';         
    
Причем необязательно это делать до тэгов. Так как парсер рекурсивный и многопроходный. По умолчанию - 10 проходов.

При ajax-запросах, очень удобно получать ответ от сервера в виде JSON. В частности в нативных процессорах вывод json такой:

$this->outputArray($res);        
    
Я же предпочитаю так:
return json_encode($res, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_NUMERIC_CHECK);
    
Суть одна, но в последнем случае ничего не преобразовывается "для блага пользователя", что мне больше подходит.

    

PS

Я недавно по фрилансу дорабатывал немного сайт на MODX Evolution. Это как прийти во фраке на дискотеку в сельский клуб))). Вроде все знакомо, но разница дикая. В Revolution, похоже, решили впихнуть все самое навороченное. И ExtJS, и SMARTY, и XPDO. Если Evo я освоил за несколько дней, то на Revo ушло несколько месяцев. И то еще куча непознанного. Особо радует, что в Modx Revo очень легко и гармонично вписывается пользовательский PHP-код. Ну а сложность API... Ну да. Не каждый сразу въедет. Видно проще нельзя было. В Evo, на мой взгляд, API намного проще, раз в 100, но от этого там не легче))).