Я работаю на многоязычном веб-сайте в PHP, и в файлах моих языков у меня часто есть строки, которые содержат несколько переменных, которые будут позже заполнены для завершения предложений.
В настоящее время я {VAR_NAME}
в строку и вручную заменяя каждое вхождение своим значением соответствия при его использовании.
Итак, в основном:
{X} created a thread on {Y}
становится:
Dany created a thread on Stack Overflow
Я уже думал о sprintf
но я нахожу это неудобным, потому что он зависит от порядка переменных, которые могут меняться от языка к другому.
И я уже проверил Как заменить переменную в строке со значением в php? и на данный момент я в основном использую этот метод.
Но мне интересно знать, есть ли встроенный (или, может быть, нет) удобный способ в PHP, чтобы сделать это, учитывая, что у меня уже есть переменные, названные в точности как X и Y в предыдущем примере, больше как $$ для переменной переменной ,
Поэтому вместо того, чтобы делать str_replace в строке, я мог бы вызвать такую функцию:
$X = 'Dany'; $Y = 'Stack Overflow'; $lang['example'] = '{X} created a thread on {Y}'; echo parse($lang['example']);
будет также распечатываться:
Dany created a thread on Stack Overflow
Благодаря!
редактировать
Строки служат в качестве шаблонов и могут использоваться несколько раз с различными входами.
Таким образом, в основном "{$X} ... {$Y}"
не будет делать трюк, потому что я потеряю шаблон, и строка будет инициализирована начальными значениями $X
и $Y
которые еще не определены ,
Я собираюсь добавить ответ здесь, потому что ни один из текущих ответов действительно не разрезал горчицу на мой взгляд. Я пойду прямо и покажу вам код, который я бы использовал для этого:
function parse( /* string */ $subject, array $variables, /* string */ $escapeChar = '@', /* string */ $errPlaceholder = null ) { $esc = preg_quote($escapeChar); $expr = "/ $esc$esc(?=$esc*+{) | $esc{ | {(\w+)} /x"; $callback = function($match) use($variables, $escapeChar, $errPlaceholder) { switch ($match[0]) { case $escapeChar . $escapeChar: return $escapeChar; case $escapeChar . '{': return '{'; default: if (isset($variables[$match[1]])) { return $variables[$match[1]]; } return isset($errPlaceholder) ? $errPlaceholder : $match[0]; } }; return preg_replace_callback($expr, $callback, $subject); }
Что это делает?
В двух словах:
preg_replace_callback()
это в preg_replace_callback()
, где обратный вызов обрабатывает две из этих последовательностей и обрабатывает все остальное как операцию замены. Регулярное выражение
Регулярное выражение соответствует любой из этих трех последовательностей:
$variables
, если он найден, тогда возвращаем значение замены, если нет, то возвращаем значение $errPlaceholder
– по умолчанию это значение равно null
, что рассматривается как частный случай, и возвращается исходный placeholder (т.е. строка не изменяется). Почему это лучше?
Чтобы понять, почему это лучше, давайте посмотрим на подходы к замене другими ответами. За одним исключением (единственным недостатком которого является совместимость с PHP <5.4 и слегка неочевидное поведение), они делятся на две категории:
strtr()
– Это не обеспечивает механизм для обработки escape-символа. Что делать, если ваша строка ввода нуждается в литерале {X}
? strtr()
не учитывает это, и он будет заменен значением $X
str_replace()
– это страдает от той же проблемы, что и strtr()
, и еще одна проблема. Когда вы вызываете str_replace()
аргументом массива для аргументов поиска / замены, он ведет себя так, как если бы вы вызывали его несколько раз – по одному для каждого из пар пар замены. Это означает, что если одна из ваших строк замены содержит значение, которое появляется позже в массиве поиска, вы также замените это. Чтобы продемонстрировать эту проблему с помощью str_replace()
, рассмотрите следующий код:
$pairs = array('A' => 'B', 'B' => 'C'); echo str_replace(array_keys($pairs), array_values($pairs), 'AB');
Теперь вы, вероятно, ожидаете, что вывод здесь будет BC
но на самом деле это будет CC
( demo ) – это потому, что первая итерация заменила A
на B
, а на второй итерации строка темы была BB
– поэтому оба этих события из B
были заменены на C
Эта проблема также выдает оценку производительности, которая может быть не сразу очевидной – поскольку каждая пара обрабатывается отдельно, операция O(n)
, для каждой пары замещений выполняется поиск всей строки и выполняется операция замены. Если у вас была очень большая сюжетная строка и много пар замен, это значительная операция, проходящая под капотом.
Возможно, это соображение производительности не является проблемой – вам понадобится очень большая строка и множество пар замен, прежде чем вы получите значимое замедление, но все равно стоит вспомнить. Также стоит помнить, что регулярное выражение имеет собственные штрафы за производительность, поэтому в целом это соображение не должно включаться в процесс принятия решений.
Вместо этого мы используем preg_replace_callback()
. Это посещает любую часть строки, которая ищет совпадения ровно один раз, в пределах предоставленного регулярного выражения. Я добавляю этот классификатор, потому что, если вы напишете выражение, которое вызывает катастрофическое обратное отслеживание, то оно будет значительно больше одного раза, но в этом случае это не должно быть проблемой (чтобы избежать этого, я сделал единственное повторение в выражении притяжательное ).
Мы используем preg_replace_callback()
вместо preg_replace()
чтобы мы могли применять пользовательскую логику, ища строку замены.
Что это позволяет делать
Исходный пример из вопроса
$X = 'Dany'; $Y = 'Stack Overflow'; $lang['example'] = '{X} created a thread on {Y}'; echo parse($lang['example']);
Это становится:
$pairs = array( 'X' = 'Dany', 'Y' = 'Stack Overflow', ); $lang['example'] = '{X} created a thread on {Y}'; echo parse($lang['example'], $pairs); // Dany created a thread on Stack Overflow
Что-то более продвинутое
Теперь предположим, что у нас есть:
$lang['example'] = '{X} created a thread on {Y} and it contained {X}'; // Dany created a thread on Stack Overflow and it contained Dany
… и мы хотим, чтобы второй {X}
появился буквально в результирующей строке. Используя escape-символ по умолчанию @
, мы бы изменили его на:
$lang['example'] = '{X} created a thread on {Y} and it contained @{X}'; // Dany created a thread on Stack Overflow and it contained {X}
Хорошо, хорошо выглядит до сих пор. Но что, если этот @
должен был быть буквальным?
$lang['example'] = '{X} created a thread on {Y} and it contained @@{X}'; // Dany created a thread on Stack Overflow and it contained @Dany
Обратите внимание, что регулярное выражение предназначено только для того, чтобы обратить внимание на escape-последовательности, которые непосредственно предшествуют открывающей фигурной скобке. Это означает, что вам не нужно избегать escape-символа, если он не появится сразу перед заполнителем.
Замечание об использовании массива в качестве аргумента
В исходном примере кода используются переменные, названные так же, как и заполнители в строке. Mine использует массив с именованными ключами. Для этого есть две очень веские причины:
{dbPass}
и посмотреть ваш пароль базы данных, не так ли? Если вы действительно хотите использовать именованные переменные из текущей области (и я не рекомендую это из-за вышеупомянутых проблем безопасности), вы можете передать результат вызова get_defined_vars()
во второй аргумент.
Заметка о выборе escape-символа
Вы заметите, что я выбрал @
как escape-символ по умолчанию. Вы можете использовать любой символ (или последовательность символов, его может быть несколько), передав его третьему аргументу – и у вас может возникнуть соблазн использовать \
так как это то, что используют многие языки, но держитесь, прежде чем делать это .
Причина, по которой вы не хотите использовать \
связана с тем, что многие языки используют ее как свой собственный escape-символ, а это означает, что когда вы хотите указать свой escape-символ, например, в строковом литерале PHP, вы столкнетесь с этой проблемой:
$lang['example'] = '\\{X}'; // results in {X} $lang['example'] = '\\\{X}'; // results in \Dany $lang['example'] = '\\\\{X}'; // results in \Dany
Это может привести к кошмару читаемости и некоторому неочевидному поведению со сложными шаблонами. Выберите escape-символ, который не используется каким-либо другим языком (например, если вы используете этот метод для генерации фрагментов HTML, не используйте &
как символ escape).
Подводить итоги
То, что вы делаете, имеет крайние случаи. Чтобы решить проблему должным образом, вам нужно использовать инструмент, способный обрабатывать эти кромки – и когда дело доходит до строковых манипуляций, инструмент для работы чаще всего является регулярным выражением.
Вот портативное решение, использующее переменные переменные. Ура!
$string = "I need to replace {X} and {Y}"; $X = 'something'; $Y = 'something else'; preg_match_all('/\{(.*?)\}/', $string, $matches); foreach ($matches[1] as $value) { $string = str_replace('{'.$value.'}', ${$value}, $string); }
Сначала вы настраиваете свою строку и свои замены. Затем вы выполняете регулярное выражение, чтобы получить массив совпадений (строки в {и}, включая эти скобки). Наконец, вы обходите вокруг них и заменяете их теми переменными, которые вы создали выше, используя переменные переменные. Прекрасный!
Просто подумал, что я обновил бы это с помощью другого варианта, даже если вы отметили его как правильное. Вам не нужно использовать переменные переменные, и массив можно использовать в нем.
$map = array( 'X' => 'something', 'Y' => 'something else' ); preg_match_all('/\{(.*?)\}/', $string, $matches); foreach ($matches[1] as $value) { $string = str_replace('{'.$value.'}', $map[$value], $string); }
Это позволит вам создать функцию со следующей подписью:
public function parse($string, $map); // Probably what I'd do tbh
Если вы используете 5.4, и вам bindTo()
возможность использования встроенной переменной интерполяции PHP в строке, вы можете использовать метод bindTo()
Closure
следующим образом:
// Strings use interpolation, but have to return themselves from an anon func $strings = [ 'en' => [ 'message_sent' => function() { return "You just sent a message to $this->recipient that said: $this->message."; } ], 'es' => [ 'message_sent' => function() { return "Acabas de enviar un mensaje a $this->recipient que dijo: $this->message."; } ] ]; class LocalizationScope { private $data; public function __construct($data) { $this->data = $data; } public function __get($param) { if(isset($this->data[$param])) { return $this->data[$param]; } return ''; } } // Bind the string anon func to an object of the array data passed in and invoke (returns string) function localize($stringCb, $data) { return $stringCb->bindTo(new LocalizationScope($data))->__invoke(); } // Demo foreach($strings as $str) { var_dump(localize($str['message_sent'], array( 'recipient' => 'Jeff Atwood', 'message' => 'The project should be done in 6 to 8 weeks.' ))); } //string(93) "You just sent a message to Jeff Atwood that said: The project should be done in 6 to 8 weeks." //string(95) "Acabas de enviar un mensaje a Jeff Atwood que dijo: The project should be done in 6 to 8 weeks."
( Демо-версия Codepad )
Возможно, он немного взломан, и мне не нравится использовать $this
в этом случае. Но вы получаете дополнительное преимущество от использования интерполяции переменных PHP (что позволяет вам делать такие вещи, как экранирование, которых трудно достичь с помощью регулярных выражений).
EDIT: добавлен LocalizationScope
, который добавляет еще одно преимущество: никаких предупреждений, если анонимные функции локализации пытаются получить доступ к данным, которые не были предоставлены.
strtr
, вероятно, лучший выбор для такого рода вещей, потому что он заменяет самые длинные ключи:
$repls = array( 'X' => 'Dany', 'Y' => 'Stack Overflow', ); foreach($data as $key => $value) $repls['{' . $key . '}'] = $value; $result = strtr($text, $repls);
(подумайте о ситуациях, когда у вас есть такие ключи, как XX и X)
И если вы не хотите использовать массив и вместо этого выставляете все переменные из текущей области:
$repls = get_defined_vars();
Если ваша единственная проблема с sprintf – это порядок аргументов, вы можете использовать опцию обмена аргументами.
Из документа ( http://php.net/manual/en/function.sprintf.php ):
$format = 'The %2$s contains %1$d monkeys'; echo sprintf($format, $num, $location);
gettext – широко используемая универсальная система локализации, которая делает именно то, что вы хотите. Существуют библиотеки для большинства языков программирования, а PHP имеет встроенный движок . Он управляется po-файлами, простым текстовым форматом, для которого существует множество редакторов, и совместим с синтаксисом sprintf.
У него даже есть некоторые функции для работы с такими вещами, как сложные множественные числа, которые есть у некоторых языков.
Вот несколько примеров того, что он делает. Обратите внимание, что _ () является псевдонимом для gettext ():
echo _('Hello world');
// будет выводить hello world на текущий выбранный язык echo sprintf(_("%s has created a thread on %s"), $name, $site);
// переводит строку и передает ее sprintf () echo sprintf(_("%2$s has created a thread on %1$s"), $site, $name);
// то же, что и выше, но с измененным порядком параметров. Если у вас больше нескольких строк, вы должны обязательно использовать существующий движок, а не писать свой собственный. Добавление нового языка – это всего лишь вопрос перевода списка строк, и большинство профессиональных инструментов перевода также могут работать с этим файловым форматом.
Проверьте Википедию и документацию по PHP для базового обзора того, как это работает:
Google обнаруживает, что кучи документации и ваш любимый репозиторий программного обеспечения, скорее всего, содержат несколько инструментов для управления po-файлами.
Некоторые из которых я использовал:
Почему бы не использовать str_replace? Если вы хотите его как шаблон.
echo str_replace(array('{X}', '{Y}'), array($X, $Y), $lang['example']);
для каждого случая этого, что вам нужно
str_replace был создан для этого в первую очередь.
Как насчет определения «переменных» частей как массива с ключами, соответствующими заполнителям в вашей строке?
$string = "{X} created a thread on {Y}"; $values = array( 'X' => "Danny", 'Y' => "Stack Overflow", ); echo str_replace( array_map(function($v) { return '{'.$v.'}'; }, array_keys($values)), array_values($values), $string );
Почему вы не можете использовать строку шаблона внутри функции?
function threadTemplate($x, $y) { return "{$x} created a thread on {$y}"; } echo threadTemplate($foo, $bar);
Просто:
$X = 'Dany'; $Y = 'Stack Overflow'; $lang['example'] = "{$X} created a thread on {$Y}";
Следовательно:
echo $lang['example'];
Вывод:
Dany created a thread on Stack Overflow
Как вы просили.
ОБНОВИТЬ:
Согласно комментариям OP о том, чтобы сделать решение более портативным:
Каждый раз, когда вы проводите разбор пар,
class MyParser { function parse($vstr) { return "{$x} created a thread on {$y}"; } }
Таким образом, если происходит следующее:
$X = 3; $Y = 4; $a = new MyParser(); $lang['example'] = $a->parse($X, $Y); echo $lang['example'];
Который вернется:
3 created a thread on 4;
И, двойная проверка:
$X = 'Steve'; $Y = 10.9; $lang['example'] = $a->parse($X, $Y);
Будет печать:
Steve created a thread on 10.9;
По желанию.
ОБНОВЛЕНИЕ 2:
В соответствии с комментариями OP о повышении переносимости:
class MyParser { function parse($vstr) { return "{$vstr}"; } } $a = new MyParser(); $X = 3; $Y = 4; $vstr = "{$X} created a thread on {$Y}"; $a = new MyParser(); $lang['example'] = $a->parse($vstr); echo $lang['example'];
Выведет результаты, приведенные ранее.
Пытаться
$lang['example'] = "$X created a thread on $Y";
EDIT: Основываясь на последней информации
Возможно, вам нужно посмотреть на функцию sprintf ()
Тогда вы могли бы определить строку шаблона как это
$template_string = '%s created a thread on %s'; $X = 'Fred'; $Y = 'Sunday'; echo sprintf( $template_string, $X, $Y );
$template_string
не изменяется, но позже в вашем коде, когда вы назначили разные значения для $X
и $Y
вы все равно можете использовать echo sprintf( $template_string, $X, $Y );
См. Руководство по PHP
просто бросая другое решение при использовании ассоциативных массивов. Это будет проходить через ассоциативный массив и либо заменить шаблон, либо оставить его пустым.
пример:
$list = array(); $list['X'] = 'Dany'; $list['Y'] = 'Stack Overflow'; $str = '{X} created a thread on {Y}'; $newstring = textReplaceContent($str,$list); function textReplaceContent($contents, $list) { while (list($key, $val) = each($list)) { $key = "{" . $key . "}"; if ($val) { $contents = str_replace($key, $val, $contents); } else { $contents = str_replace($key, "", $contents); } } $final = preg_replace('/\[\w+\]/', '', $contents); return ($final); }
возможно, для других кодеров нужна функция синтаксического анализа;
$X = 'Dany'; $Y = 'Stack Overflow'; $lang['example'] = "{X} created a thread on {Y}"; function parse($var) { $return_value = $var; $matches_value = preg_match_all('/\{(.*?)\}/',$var,$matches); if($matches_value > 0){ foreach($matches[1] as $match){ if(isset($GLOBALS[$match])){ // !empty($GLOBALS[$match]) -- you choose $return_value = str_replace('{'.$match.'}',$GLOBALS[$match],$return_value); } } } return $return_value; } echo parse($lang['example']);
если переменные $ X и $ Y заданы всегда. с eval:
$X = 'Dany'; $Y = 'Stack Overflow'; $lang['example'] = "{X} created a thread on {Y}"; $lang['example'] = preg_replace('/\\{(.*?)\\}/', '\\$\\1',$lang['example']); eval("\$return_value = \"".$lang['example']."\";"); echo $return_value;