Наверх

Разбор кода начинающего программиста сниппетов MODX

Сегодня мне пришло письмо с просьбой посмотреть код сниппета «и своим комментарием подтолкнуть в нужную сторону».

Я решил ответить на это письмо статьёй, так как многим начинающим программистам будет интересно эти комментарии почитать.

Автор учится писать сниппеты для MODX и для примера поставил себе задачу написать относительно простой сниппет для вывода меню сайта.

Чтобы не раздувать статью буду давать ссылки на версии файла на Github. И вот с чего мы начнем: menucreator.php именно этот код автор попросил прокомментировать.

Избавляемся от дублирования кода

Итак… Первое, что бросается в глаза — сниппет состоит из двух практически одинаковых частей, разделенных условием
if (isset($showSubMenu) && $showSubMenu == 1) {
    // ...
} else {
    // ...
}
Из-за того, что код в обеих частях практически идентичен и во многом повторяется, однажды случится такая ситуация — код в первой части мы изменим, а про вторую забудем, тогда поведение сниппета станет непредсказуемым.

Попробуем исправить. Перенесем условие внутрь — как раз к тому месту, в котором начинаются различия в коде:
//...
if (isset($showSubMenu) && $showSubMenu == 1) //Если показываем подменю
{
	$subwhere = array('hidemenu'=>$hidemenu, 'published' => true, 'parent'=>$resourceArray['id']);
	$subresources = $modx->getCollection('modResource',$subwhere);
	if (isset($subresources) && $subresources)
	{
		$output .= '<ul>';
	}
	foreach ($subresources as $subresource)
	{
		$subresourceArray = $subresource->toArray();
		$output .= $modx->getChunk($template,$subresourceArray);
	}
	if (isset($subresources) && $subresources)
	{
		$output .= '</ul>';
	}
}
$resourceArray['test1']='test22Ok';
$output .= $modx->getChunk($template,$resourceArray);
//...
Отлично. Теперь вторая часть сниппета нам не нужна и мы ее удаляем. Код сразу сократился на 25 строк. Вот так код выглядит теперь.

Оптимизируем выборку

На строчке № 36 мы проверяем значение поля parent. Это было бы оправдано, если мы хотели как-то выделить такие ресурсы в списке или сделать с ними что-то. Но здесь мы их просто пропускаем.

На небольших сайтах это не страшно, но как только количество ресурсов начнет расти, скорость работы сниппета начнет падать — ведь ему нужно перебрать все ресурсы сайта.

А раз нам не нужны все ресурсы, просто вынесем условие с parent в выборку where
$where = array('hidemenu'=>$hidemenu, 'published' => true, 'parent' => 0);
Ну и удаляем эту проверку, так как теперь она нам не нужна. Вот новая версия.

Продолжаем оптимизацию

На строчке № 39 мы делаем еще один запрос в базу данных, чтобы «вытащить» список ресурсов второго уровня. Хоть в коде это лишь одна строчка, запросов будет много, так как она выполняется в цикле для каждого ресурса из предыдущей выборки.

И опять таки, на небольших сайтах можно даже не заметить никаких проблем, но это не наш путь)) Мы выберем дочерние ресурсы перед циклом — за один запрос.
if (isset($showSubMenu) && $showSubMenu == 1) //Если показываем подменю
{
        $parentIDs = array(); // Сюда запишем все ID-шники первого уровня
        foreach ($resources as $resource)
        {
            $parentIDs[] = $resource->get('id');
        }
        // Составляем запрос, чтобы выбрать ресурсы второго уровня
        $subwhere = array('hidemenu'=>$hidemenu, 'published' => true, 'parent:IN'=>$parentIDs);
        $subresources = $modx->getCollection('modResource',$subwhere);
        $parents = array(); // Здесь будем хранить уже дерево ресурсов, разбитых на группы
        foreach ($subresources as $subresource)
        {
            $parentID = $subresource->get('parent');
            $parents[$parentID][] = $subresource;
        }
}
Сниппет целиком.

Отделяем логику от представления

Ну и тут уже кому как нравится, но чем меньше HTML внутри PHP-кода, тем лучше.

Со сниппетом в комплекте должны идти чанки для оформления по умолчанию, чтобы пользователь мог создать свои, используя стандартные как пример.

row.menucreater
<li><a href="[[+uri]]">[[+menutitle:default=`[[+pagetitle]]`]]</a></li>
[[+output:notempty=`<ul>[[+output]]</ul>`]]
outer.menucreator
<ul[[+classes]]>[[+output]]</ul>

Теперь пользователь может менять оформление как угодно, не заглядывая в код сниппета.
Это одно из преимуществ MODX и мы должны им пользоваться.

Последний подход

Теперь «причешем» код, а так же укажем значение параметров по умолчанию. Здесь каждый решает сам, как ему форматировть код, как выводить ресурсы… Например, мне нравится составлять массив $output, а потом выводить его в виде строки, используя implode.
<?php
ini_set('display_errors', -1);
error_reporting(E_ALL);

$tpl = $modx->getOption('tpl', $scriptProperties, 'row.menucreater');
$outerTpl = $modx->getOption('outerTpl', $scriptProperties, 'outer.menucreater');
$ulClass = $modx->getOption('ulClass', $scriptProperties, 'menu');
$classes = $ulClass ? ' class="'.$ulClass.'"' : "";
$showHideMenu = $modx->getOption('showHideMenu', $scriptProperties, 0);
$showSubMenu = $modx->getOption('showSubMenu', $scriptProperties, 0);
$outputSeparator = $modx->getOption('outputSeparator', $scriptProperties, PHP_EOL);

// Составляем условие для выборки первого уровня
$where = array('hidemenu' => 0, 'published' => 1, 'parent' => 0);

// Показываем скрытые пункты меню, если надо
if (isset($showHideMenu) && $showHideMenu == 1) {
    $where['hidemenu'] = 1;
}

// Получаем список ресурсов первого уровня
$resources = $modx->getCollection('modResource',$where);

if ($showSubMenu == 1) {
    // Составляем список ID ресурсов первого уровня
    $parentIDs = array();
    foreach ($resources as $resource) {
        $parentIDs[] = $resource->get('id');
    }
    
    // Составляем запрос, чтобы выбрать ресурсы второго уровня
    unset($where['parent']);
    $where['parent:IN'] = $parentIDs;
    
    // Получаем все ресурсы второго уровня
    $subresources = $modx->getCollection('modResource', $where);
    
    // Строим дерево ресурсов второго уровня
    $parents = array();
    foreach ($subresources as $subresource) {
    	$parentID = $subresource->get('parent');
    	$parents[$parentID][] = $subresource;
    }
}

$output = array();

foreach ($resources as $resource) {
	// Забираем все параметры ресурса для передачи их в чанк
	$resArr = $resource->toArray();
	
	// Проверяем, были ли найдены дочерние ресурсы
	if (isset($parents) && isset($parents[$resArr['id']]) && is_array($parents[$resArr['id']])) {
		$subOutput = array();
		$subresources = $parents[$resArr['id']];
		
		// Оформляем каждый ресурс второго уровня
		foreach ($subresources as $subresource) {
			$subresArr = $subresource->toArray();
			$subOutput[] = $modx->getChunk($tpl,$subresArr);
		}
	}
	
	// Разделяем ресурсы пустой строкой (или другим разделителем)
	$resArr['output'] = implode($outputSeparator, $subOutput);
	
	// И оформляем ресурс первого уровня
	$output[] = $modx->getChunk($tpl,$resArr);
}

// Выводим ресурсы с разделителем
return $modx->getChunk($outerTpl, array('output' => $output, 'classes' => $classes));
Без комментариев код прекрасно чувствует себя на 50 строчках.

Что дальше?

Вот мы и разобрали основные моменты в этом сниппете. Его можно и дальше развивать, например,
  • отмечать текущий пункт меню и его родителей,
  • добавить возможность указать класс не только для UL, но и для LI,
  • оптимизировать скорость, используя PDO для выборки,
  • добавить возможность выводить меню любого уровня вложенности
  • и прочее-прочее-прочее)

Но начало положено хорошее.


5 комментариев

  1. Любовь 11 апреля 2016, 08:09(Комментарий был изменён) # 0
    Добрый день!
    Подскажите, пожалуйста, по поводу правильного написания сниппета.
    У меня в нем происходит расчет по параметрам из формы (из GET массива). Вот он:
    <?php
    $out = '';
    if (isset($_GET)) {  
        $price=$_GET['price']; // цена контракта
        $done=$_GET['done']; // выполнено
        $days=$_GET['days']; // дни просрочки
        $disp=$_GET['disp']; // дни на исполнение
        $cb=$_GET['cbproc']/100; // размер ставки ЦБ (вводится в %)
        $dcb=$cb/300; // размер ставки ЦБ за день
        $k=($days/$disp)*100; // расчет коэффициента * 100%
        if($k<=50) {$scb=0.01;}
            elseif ($k<100)  {$scb=0.02;}
                elseif ($k>=100) {$scb=0.03;}
        $rsize1=$scb*$cb*$days;
        $peni1=($price-$done)*$rsize1; // пени По ПП 1063
        $rsize2=$dcb*$days; // пени 1/300 ставки
        $peni2=($price-$done)*$rsize2;
    
      
        if($peni1!=0){
        $out= '<p>Вы запрашивали расчёт пени для следующих параметров:</p>
        <ul>
            <li>Цена контракта, руб.: '.$_GET['price'].'</li>
            <li>Выполнено: '.$_GET['days'].'</li>
            <li>Дни просрочки: '.$_GET['done'].'</li>
            <li>Дни на исполнение: '.$_GET['disp'].'</li>
            <li>Размер ставки ЦБ: '.$_GET['cbproc'].'</li>
        </ul>
        <h3>Пени по Постановлению правительства № 1063: <span class="pnum">'.round($peni1 , 2, PHP_ROUND_HALF_UP).'<span></h3>';
       
        }
        return $out;
    }
    Результат расчета я вывожу на экран, но хочу еще дать возможность пользователю получать результаты расчета на почту — это я тоже сделала с помощью готового сниппета. Но отправить могу только сам результат расчёта, а хочется отправить еще параметры, по которым считали, т.е. те, что на данный момент у меня в GET-массиве.

    P.S. Вернее то, что я написала в $out — все отправляется на почту, но в одну строку без форматирования. А нужно чтобы было красиво. И наверное писать этот сниппет надо как-то по-другому.
    1. Илья Уткин 11 апреля 2016, 08:26 # 0
      Скорее, дело в том, как вы отправляете почту.
      Попробуйте такой способ: https://ilyaut.ru/reposts/sending-mail-through-modmail/
      1. Любовь 11 апреля 2016, 08:30 # 0
        Спасибо, посмотрю.
        Отправляю так:
        [[!AjaxForm?
            &snippet=`FormIt`
            &form=`form.pdf`
            &emailTpl=`emailPdf_tpl`
            &pdfTpl=`pdf.tpl`
            &hooks=`hpdf,email`
            &emailSubject=`сообщение c pdf`
            &emailTo=`example@example.com`
            &validate=`name:required,email:required`
            &validationErrorMessage=`В форме содержатся ошибки!`
            &successMessage=`Сообщение успешно отправлено`
        ]]
    2. Толик 07 июля 2017, 11:55 # 0
      Доброго времени суток Илья!
      Очень интересный урок, но возник вопрос, каким образом можно реализовать возможность выводить меню любого уровня вложенности? Это через цикл делается? Благодарю за совет или подсказку!
      1. Илья Уткин 13 июля 2017, 08:32 # 0
        Да, самое простое — через цикл. Варианта два — либо мы делаем вложенные запросы внутри цикла, либо выбираем сначала все ресурсы, а потом строим дерево с помощью полей parent. Второй способ сложнее в реализации, но использует меньше ресурсов при запуске. Он реализован в pdoMenu.

      Авторизация

      через сервис Loginza:


      Шаблоны MODX

      1 2 Дальше »

      Объектная
      модель
      MODX