Подтвердить что ты не робот

Замена переменных в строке

Я работаю над многоязычным веб-сайтом в 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

Спасибо!

Edit

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

Таким образом, в основном выполнение "{$X} ... {$Y}" не приведет к трюку, потому что я потеряю шаблон, и строка будет инициализирована начальными значениями $X и $Y, которые еще не определены.

4b9b3361

Ответ 1

Я собираюсь добавить ответ здесь, потому что ни один из текущих ответов действительно не разрезал горчицу на мой взгляд. Я пойду прямо и покажу вам код, который я бы использовал для этого:

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);
}

Что это делает?

В двух словах:

  • Создайте регулярное выражение, используя указанный escape-символ, который будет соответствовать одной из трех последовательностей (подробнее об этом ниже)
  • Загрузите это в preg_replace_callback(), где обратный вызов обрабатывает две из этих последовательностей и обрабатывает все остальное как операцию замены.
  • Возвращает результирующую строку

Регулярное выражение

Регулярное выражение соответствует любой из этих трех последовательностей:

  • Два вхождения escape-символа, за которым следуют ноль или более символов escape-символа, за которым следует открывающая фигурная скобка. Расходуются только первые два появления escape-символа. Это заменяется одним вхождением escape-символа.
  • Единственное появление escape-символа, за которым следует открывающая фигурная скобка. Это заменяется буквальной открытой фигурной скобкой.
  • Открывающая фигурная скобка, за которой следуют один или несколько символов слова perl (альфа-число и символ подчеркивания), за которыми следует закрывающая фигурная скобка. Это рассматривается как местозаполнитель, и поиск выполняется для имени между фигурными скобками в массиве $variables, если он найден, затем возвращает значение замены, если не возвращать значение $errPlaceholder - по умолчанию это null, который рассматривается как частный случай и возвращается исходный заполнитель (т.е. строка не изменяется).

Почему это лучше?

Чтобы понять, почему это лучше, взгляните на подходы замещения, принятые другими ответами. С одним исключением (единственным недостатком которого является совместимость с 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(). Это посещает любую часть строки, которая ищет совпадения ровно один раз, в пределах предоставленного регулярного выражения. Я добавляю этот определитель, потому что, если вы напишете выражение, которое вызывает катастрофическое обратное отслеживание, то это будет значительно больше одного раза, но в этом случае, (чтобы избежать этого, я сделал единственное повторение в выражении 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 Qaru and it contained Dany

... и мы хотим, чтобы второй {X} отображался буквально в результирующей строке. Используя escape-символ по умолчанию @, мы бы изменили его на:

$lang['example'] = '{X} created a thread on {Y} and it contained @{X}';
// Dany created a thread on Qaru and it contained {X}

Хорошо, хорошо выглядит до сих пор. Но что, если этот @ должен был быть буквальным?

$lang['example'] = '{X} created a thread on {Y} and it contained @@{X}';
// Dany created a thread on Qaru 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-символ).

Подводя итоги

То, что вы делаете, имеет крайние случаи. Чтобы решить проблему должным образом, вам необходимо использовать инструмент, способный обрабатывать эти краевые кейсы - а когда дело доходит до строковой манипуляции, инструмент для задания чаще всего представляет собой регулярное выражение.

Ответ 2

Вот портативное решение, использующее переменные переменные. Ура!

$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

Ответ 3

Если вы используете 5.4, и вам небезразлична возможность использования встроенной переменной 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, что добавляет еще одно преимущество: никаких предупреждений, если анонимные функции локализации пытаются получить доступ к данным, которые не были предоставлены.

Ответ 4

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();

Ответ 5

Если ваша единственная проблема с sprintf - это порядок аргументов, вы можете использовать свопинг аргументов.

Из документа (http://php.net/manual/en/function.sprintf.php):

$format = 'The %2$s contains %1$d monkeys';
echo sprintf($format, $num, $location);

Ответ 6

gettext - широко используемая универсальная система локализации, которая делает именно то, что вы хотите. Существуют библиотеки для большинства языков программирования, а PHP имеет встроенный движок . Он управляется po файлами, простым текстовым форматом, для которого существует множество редакторов, и он совместим с синтаксисом sprintf.

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

Вот несколько примеров того, что он делает. Обратите внимание, что _() является псевдонимом для gettext():

  • echo _('Hello world');//выводит мир hello на текущем выбранном языке
  • 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);//то же, что и выше, но с измененным порядком параметров.

Если у вас больше нескольких строк, вы должны обязательно использовать существующий движок, а не писать свой собственный. Добавление нового языка - это вопрос перевода списка строк, и большинство профессиональных инструментов перевода также могут работать с этим файловым форматом.

Ознакомьтесь с Wikipedia и документацией PHP для базового обзора того, как это работает:

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

Некоторые из которых я использовал:

  • poedit: очень легкий и простой. Хорошо, если у вас нет слишком много материала для перевода и вы не хотите тратить время на размышления о том, как это работает.
  • Virtaal: немного сложнее и имеет немного кривой обучения, но также некоторые приятные функции, которые облегчают вашу жизнь. Хорошо, если вам нужно много перевести.
  • GlotPress - это веб-приложение (от wordpress people), которое позволяет осуществлять совместное редактирование файлов базы данных перевода.

Ответ 7

Почему бы не использовать str_replace? Если вы хотите его как шаблон.

echo str_replace(array('{X}', '{Y}'), array($X, $Y), $lang['example']);

для каждого случая этого, что вам нужно

str_replace был создан для этого в первую очередь.

Ответ 8

Простой:

$X = 'Dany';
$Y = 'Stack Overflow';
$lang['example'] = "{$X} created a thread on {$Y}";

Следовательно:

echo $lang['example'];

Будет выводиться:

Dany created a thread on Stack Overflow

Как вы просили.

UPDATE:

В соответствии с комментариями 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'];

Выведет результаты, приведенные ранее.

Ответ 9

Try

$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

Ответ 10

Как определить "переменную" части как массив с ключами, соответствующими заполнителям в вашей строке?

$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
);

Ответ 11

Почему вы не можете просто использовать строку шаблона внутри функции?

function threadTemplate($x, $y) {
    return "{$x} created a thread on {$y}";
}
echo threadTemplate($foo, $bar);

Ответ 12

просто бросает другое решение при использовании ассоциативных массивов. Это будет проходить через ассоциативный массив и либо заменить шаблон, либо оставить его пустым.

Пример:

$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);
            }

Ответ 13

возможно, что другие функции кодирования нуждаются в функции синтаксического анализа;

$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;