Для начала немного о том, как устроен шаблон vBulletin. По сути, это обычная PHP-строка в невидимых двойных кавычках (при этом двойные кавычки самой строки автоматически экранируются). Пример:
PHP Code:
<div class="username">$post[username]</div>
Это то, что видит пользователь. В «чёрном ящике» они хранятся в виде:
PHP Code:
<div class=\"username\">$post[username]</div>
Когда нужно подставить в шаблон переменные, поступают следующим образом:
PHP Code:
eval('$output = "' . fetch_template('username') . '";');
Если подставить вместо fetch_template то, что она возвращает, получим:
PHP Code:
eval('$output = "<div class=\"username\">$post[username]</div>";');
Таким вот нехитрым образом в шаблоне всегда будет локальный контекст, и не надо ничего никуда присваивать. Однако функционал подобных шаблонов весьма уныл — кроме переменных ничего нет. Естественно будет добавить конструкции if и if-else:
PHP Code:
<if condition="$post[username] != ''">
<div class="username">$post[username]</div>
<else />
Пользователь anonym и не указал своё имя
</if>
Которые превращаются в:
PHP Code:
" . (($post[username] != '') ? ("
<div class=\"username\">$post[username]</div>
") : ("
Пользователь anonym и не указал своё имя
")) . "
И выполняются вот так:
PHP Code:
$output = "" . (($post[username] != '') ? ("
<div class=\"username\">$post[username]</div>
") : ("
Пользователь anonym и не указал своё имя
")) . "";
На этом всё — циклы разработчики vBulletin не осилили. В результате имеем кучу отличных шаблонов типа
pollvote:
PHP Code:
<div><label for="rb_optionnumber_$option[number]"><input type="radio" name="optionnumber" value="$option[number]" id="rb_optionnumber_$option[number]" />$option[question]</label></div>
Которые склеивают вручную в коде. Знаете, так здорово поднимает производительность труда необходимость на каждую итерацию лепить по шаблону из одной строчки! И вот позавчера я почему-то вспомнил про эту проблему и за пять минут придумал как можно сделать циклы. Времени на это дело нашёл только сегодня, и вот пожалуйста:
PHP Code:
$pacany = array(
'Владимир' => array('Vladimir', 'Vova', 'Vovan'),
'Дмитрий' => array('Dmitriy', 'Dima', 'Dimka')
);
PHP Code:
<foreach from="$pacany" key="$pacan_name" value="$pacan_nicknames" import="$spacer_open $spacer_close" is_first="$is_first_pacan" is_last="$is_last_pacan">
<if condition="$is_first_pacan">$spacer_open</if>
<b>$pacan_name:</b>
<foreach from="$pacan_nicknames" value="$pacan_nickname" is_last="$is_last_nickname">
$pacan_nickname<if condition="!$is_last_nickname">,</if>
</foreach>
<if condition="$is_last_pacan">$spacer_close<else /><br /></if>
</foreach>
Собственно, единственная опция, требующая пояснения (и разгадывающая тайну реализации) — import, она указывает какие переменные из текущего контекста нужно видеть в теле цикла.
*
Как замутить такое у себя на форуме.
В
includes/adminfunctions_template.php, там где:
PHP Code:
// #############################################################################
/**
* Processes a template into PHP code for eval()
*
* @param string Unprocessed template
* @param boolean Halt on error?
*
* @return string
*/
function process_template_conditionals($template, $haltonerror = true)
делает
PHP Code:
return str_replace("\\'", "'", $template_cond);
Напишем по другому:
PHP Code:
$template_cond = str_replace("\\'", "'", $template_cond);
while (strstr($template_cond, '<foreach'))
{
$pos = strrpos($template_cond, '<foreach');
$template_cond = substr($template_cond, 0, $pos) . preg_replace_callback('#<foreach ([^>]+)>(.*)</foreach>#Us', 'process_template_foreach', substr($template_cond, $pos));
}
return $template_cond;
Я что-то не осилил доказать, что если в системе вложенных тегов всегда брать последний открывающий, находить его закрывающий, сворачивать, и так поступать пока блоки не закончатся, то при обработке каждого блока мы можем быть уверены, что вложенные в него уже обработаны, но по-моему это так, и 200 строк лапши по обработке if'ов выше там как бы совсем ни к чему.
process_template_foreach будет выглядеть так:
PHP Code:
function process_template_foreach($matches)
{
$foreach_args = $matches[1];
$foreach_body = $matches[2];
$helper_args = array();
preg_match_all('#(from|import|key|value|is_first|is_last)\s*=\s*\\\"(.+)\\\"#Usi', $foreach_args, $foreach_args_matches);
foreach (array_keys($foreach_args_matches[0]) as $i)
{
$arg_name = trim($foreach_args_matches[1][$i]);
$arg_value = trim($foreach_args_matches[2][$i]);
switch ($arg_name)
{
case 'from':
if (!preg_match('#^\$([a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)(\[[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*\])?$#U', $arg_value, $variable_name))
{
print_stop_message('is_not_a_variable_name', htmlspecialchars($arg_value), htmlspecialchars('<foreach ' . $foreach_args . '>'));
}
$helper_args['from'] = $variable_name[0];
break;
case 'import':
if (!preg_match_all('#\$([a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)#', $arg_value, $imported_variable_names))
{
print_stop_message('import_parameter_value_is_wrong', htmlspecialchars('<foreach ' . $foreach_args . '>'));
}
$helper_args['import'] = 'compact(' . var_export($imported_variable_names[1], true) . ')';
break;
case 'key':
case 'value':
case 'is_first':
case 'is_last':
if (!preg_match('#^\$([a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)$#U', $arg_value, $variable_name))
{
print_stop_message('is_not_a_variable_name', htmlspecialchars($arg_value), htmlspecialchars('<foreach ' . $foreach_args . '>'));
}
$helper_args[$arg_name] = '\'' . substr($variable_name[0], 1) . '\'';
break;
}
}
$helper_args_str = '';
foreach ($helper_args as $arg_name => $arg_value)
{
$helper_args_str .= "'{$arg_name}' => {$arg_value}, ";
}
return '" . template_foreach_helper(array(' . $helper_args_str . '), ' . var_export($foreach_body, true) . ') . "';
}
Первый раз в жизни сделал
case без
break, и это не оказалось ошибкой. Регулярка для имени переменной из официальной документации, не бойтесь.
Фразы сами добавите.
TODO: проверять, что в
import действительно валидный список переменных, а не просто набирать их оттуда, а то ошибётся кто-нибудь по типу
import="$spacer,_open" и не заметит.
Куда-нибудь в
includes/functions.php пишем:
PHP Code:
function template_foreach_helper($__internalArgs, $__internalToEval)
{
if (isset($__internalArgs['import']))
{
extract($__internalArgs['import']);
}
$__internalReturn = '';
if (isset($__internalArgs['key']))
{
$__internalKeyName = $__internalArgs['key'];
}
else
{
$__internalKeyName = '__internalDummyKey';
}
if (isset($__internalArgs['value']))
{
$__internalValueName = $__internalArgs['value'];
}
else
{
$__internalValueName = '__internalDummyValue';
}
if (isset($__internalArgs['is_first']))
{
$__internalIsFirstName = $__internalArgs['is_first'];
}
else
{
$__internalIsFirstName = '__internalIsFirstValue';
}
if (isset($__internalArgs['is_last']))
{
$__internalIsLastName = $__internalArgs['is_last'];
}
else
{
$__internalIsLastName = '__internalIsLastValue';
}
$__internalTotal = count($__internalArgs['from']);
$__internalCounter = 0;
foreach ($__internalArgs['from'] as $$__internalKeyName => $$__internalValueName)
{
$$__internalIsFirstName = ($__internalCounter == 0);
$__internalCounter++;
$$__internalIsLastName = ($__internalCounter == $__internalTotal);
eval('$__internalReturn .= "' . $__internalToEval . '";');
}
return $__internalReturn;
}
Ну и собственно всё нормально! Вышеприведённый цикл превратится в:
PHP Code:
$output = "" . template_foreach_helper(array('from' => $pacany, 'key' => 'pacan_name', 'value' => 'pacan_nicknames', 'import' => compact(array (
0 => 'spacer_open',
1 => 'spacer_close',
)), 'is_first' => 'is_first_pacan', 'is_last' => 'is_last_pacan', ), '
".(($is_first_pacan) ? ("$spacer_open") : (""))."
<b>$pacan_name:</b>
" . template_foreach_helper(array(\'from\' => $pacan_nicknames, \'value\' => \'pacan_nickname\', \'is_last\' => \'is_last_nickname\', ), \'
$pacan_nickname".((!$is_last_nickname) ? (",") : (""))."
\') . "
".(($is_last_pacan) ? ("$spacer_close") : (""))."
') . "";
* — На самом деле, можно брать и весь контекст, просто я так и не нашёл как в PHP его получить, поэтому единственный мне доступный метод — не очень хороший: триггернуть какой-нибудь E_USER_NOTICE, повесив на него кастомный обработчик, и в этот обработчик нам передадут весь контекст.