Я собираюсь добавить ответ здесь, потому что ни один из текущих ответов действительно не разрезал горчицу на мой взгляд.Я нырять прямо и показать вам код, я хотел бы использовать, чтобы сделать это:
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()
, где обратного вызова точно обрабатывает две из этих последовательностей и рассматривает все остальное как операцию замены.
- Возврат результирующая строка
Регулярное выражение
Регулярное выражение совпадает с любым из этих трех последовательностей:
- Два вхождения экранирующего символа, за которым следует ноль или более вхождений из escape-символа, за которым следует открывающая фигурная скобка. Расходуются только первые два появления escape-символа. Это заменяется одним вхождением escape-символа.
- Единственное появление escape-символа, за которым следует открывающая фигурная скобка. Это заменяется буквальной открытой фигурной скобкой.
- Открытая фигурная скобка, за которой следуют один или несколько символов слова perl (альфа-число и символ подчеркивания), а затем закрывающая фигурная скобка. Это рассматривается как заполнитель, и поиск выполняется для имени между фигурными скобками в массиве
$variables
, если он найден, затем возвращает значение замены, если нет, то возвращает значение $errPlaceholder
- по умолчанию это null
, который обрабатывается как частный случай, и возвращается исходный заполнитель (т. е. строка не изменяется).
Почему это лучше?
Чтобы понять, почему это лучше, давайте посмотрим на подходы замены, принятые другими ответами. С one exception (единственный недостаток которого является совместимость с PHP 5.4 < и слегка неочевидное поведение), эти делятся на две категории:
strtr()
- Это не обеспечивает механизм для обработки экранирующего символа. Что делать, если ваша строка ввода нуждается в литерале {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()
. Это посещает любую часть строки, которая ищет совпадения ровно один раз, в пределах предоставленного регулярного выражения. Я добавляю этот определитель, потому что, если вы напишете выражение, которое вызывает catastrophic backtracking, оно будет значительно больше одного раза, но в этом случае это не должно быть проблемой (чтобы избежать этого, я сделал единственное повторение в выражении possessive).
Мы используем 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}
появляться буквально в результирующей строке. Использование экранирующего символа по умолчанию @
, мы бы изменить его на:
$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-символа, если он не появится сразу перед заполнителем.
Примечание об использовании массива в качестве аргумента
Ваш исходный код образца использует переменные с таким же именем, как и заполнителей в строке. Mine использует массив с именованными ключами. Есть две очень веские причины для этого:
- ясность и безопасность - это гораздо проще, чтобы посмотреть, что будет в конечном итоге замещено, и вы не рискуете случайно заменяющие переменные, которые вы не хотите подвергаться. Было бы неплохо, если бы кто-то мог просто прокормить
{dbPass}
и посмотреть ваш пароль базы данных, не так ли?
- Область применения - невозможно импортировать переменные из области вызова, если вызывающий объект не является глобальной областью. Это делает функцию бесполезной, если вызвана из другой функции, а импорт данных из другой области - очень плохая практика.
Если вы действительно хотите использовать имена переменных из текущей области (и я не рекомендовать это из-за вышеупомянутых проблем безопасности) вы можете передать результат вызова get_defined_vars()
ко второму аргументу ,
Примечание о выборе экранирующего символа
Вы заметите, что я выбрал @
в качестве эвакуационных частей по умолчанию. Вы можете использовать любой символ (или последовательность символов, его может быть несколько), передав его третьему аргументу - и у вас может возникнуть соблазн использовать \
, так как это то, что используют многие языки, , но держитесь, прежде чем делать это ,
Причина вы не хотите использовать \
является , потому что многие языки используют его в качестве своего собственного побега характер, что означает, что если вы хотите, чтобы указать свой экранирующий символ, скажем, в PHP строковый литерал, вы запустите в эту проблему:
$lang['example'] = '\\{X}'; // results in {X}
$lang['example'] = '\\\{X}'; // results in \Dany
$lang['example'] = '\\\\{X}'; // results in \Dany
Это может привести к читаемости кошмара, а некоторые неочевидные поведение со сложными узорами. Выберите escape-символ, который не используется каким-либо другим языком (например, если вы используете этот метод для генерации фрагментов HTML, не используйте &
в качестве escape-символа).
Резюмируя
Что вы делаете имеет край-кейсы. Чтобы решить проблему должным образом, вам нужно использовать инструмент, способный обрабатывать эти кромки - и когда дело доходит до строковых манипуляций, инструмент для работы чаще всего является регулярным выражением.
Отличный ответ, содержит все подробное объяснение, которое я искал, спасибо за то, что поделился своим опытом, и особенно с тем, почему это лучше, я очень ценю то время, которое вы вкладываете в пишу это :) –
@DanyKhalife Нет проблем, рад помочь :-) – DaveRandom
Это хороший ответ. @DaveRandom You noob, я потерял +15 за это! : P – Jimbo