Hi, my name is Vitalii Rudnykh 👋

May 27, 2022

📔 Best practices для регулярок в PHP

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

lion

Не используй регулярные выражения, если это возможно. ✋

Регулярки это сложно. Поэтому, избегай их использования если это возможно. Ниже несколько примеров, когда регулярные выражения следует избегать, используя альтернативные решения:

ЗадачаРешение
Проверка адреса электронной почтыИспользуй filter_var() с FILTER_VALIDATE_EMAIL *
Проверка IP адресаИспользуй filter_var() с FILTER_VALIDATE_IP
Проверка URLИспользуй filter_var() с FILTER_VALIDATE_URL *
Проверка датыСм. эту тему на Stack Overflow.
Парсинг JSONИспользуй json_decode().
Парсинг HTML/XMLИспользуй готовый парсер. См. тему на Stack Overflow: How do you parse and process HTML/XML in PHP?
Парсинг CSVИспользуй str_getcsv() или fgetcsv().
Проверка наличия в строке подстрокиИспользуй strpos() или stripos().
Проверка диапазона числаИспользуй операторы сравнения.

* может не работать с национализированными доменными именами и адресами электронной почты

Знай доступные функции регулярных выражений. 🧠

Помимо классических preg_match() и preg_replace() есть и другие функции, о которых многие не знают.

preg_split()

Иногда требуется разбить текст по разделителю используя регулярку, вместо подобного кода:

$input = 'preg__split_for___fun';
if(preg_match_all('/[^_]+/', $input, $m)) {
    print_r($m[0]);
} else {
    echo 'no match';
}

Попробуй написать так:

$input = 'preg__split_for___fun';
$output = preg_split('/_+/', $input);
print_r($output);

preg_grep()

Если вам нужно просмотреть массив и сопоставить значения с определенным регулярным выражением, используйте функцию preg_grep().

$input = ['data1', 'data2', 'exclude', 'data3'];
$result = [];
foreach ($input as $v) {
    if(preg_match('/data\d+/', $v)) {
        $result[] = $v;
    }
}
print_r($result); // Array ( [0] => data1 [1] => data2 [2] => data3 )

Такой же результат можно получить таким образом:

$input = ['data1', 'data2', 'exclude', 'data3'];
$result = preg_grep('/data\d+/', $input);
print_r($result); // Array ( [0] => data1 [1] => data2 [3] => data3 )

Единственное что нужно учесть, что preg_grep() возвращает массив проиндексированный по ключам из входного массива.

preg_filter()

Согласно документации:

preg_filter() is identical to preg_replace() except it only returns the (possibly transformed) subjects where there was a match.

По сути, это то же самое, что и preg_grep(), но с опцией replace.

preg_replace_callback()

Функция замены с возможностью использования колл-бек функции. Например, нам нужно выбрать несколько слов и преобразовать их в верхний регистр:

$input = 'words are important';
$output = preg_replace_callback('/\w+/', function($m) {
    return strtoupper($m[0]);
}, $input);
echo $output;

preg_quote()

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

$user_input = isset($_GET['input']) ? (string) $_GET['input'] : '';
$haystack = 'List: pid1000, pid2000, pid3000...';
$regex = '/' . preg_quote($user_input, '/') . '\d+/';
if(preg_match_all($regex, $haystack, $m)) {
    print_r($m[0]);
} else {
    echo 'no match';
}

preg_last_error()

Эта функция может пригодиться для отладки, она возвращает код ошибки последнего выполнения PCRE Regex. Функция для преобразования кода ошибки в текст:

function preg_errtxt($errcode)
{
    static $errtext;
    if (!isset($errtxt))
    {
        $errtext = array();
        $constants = get_defined_constants(true);
        foreach ($constants['pcre'] as $c => $n) if (preg_match('/_ERROR$/', $c)) $errtext[$n] = $c;
    }
    return array_key_exists($errcode, $errtext)? $errtext[$errcode] : NULL;
}

О безопасности.

Модификатор «e»

e - обозначает evil eval. При использование его с preg_replace() функция выполнит подстановку по регулярному выражению и выполнит его как PHP-код.

$input = 'up this case!';
$output = preg_replace('/\w+/e', 'strtoupper($0)', $input);
echo $output; // UP THIS CASE!

Но, это уже история и с /e ты скорее всего столкнешься, только если будешь работать с древним легаси кодом! Использование этого модификатора было отмечено как deprecated начиная с PHP 5.5, а в PHP 7 его удалили полностью.

Вместо /e стоит использовать функцию preg_replace_callback(), о которой я писал ранее:

$input = 'up this case!';

$output = preg_replace_callback('/\w+/', function($m) {
    return strtoupper($m[1]);
}, $input);
echo $output; // UP THIS CASE!

Комментарии. 📝

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

eXtended mode

Это предпочтительный и наиболее распространенный способ реализации комментариев с помощью модификатора x:

// Regex for password validation
$regex = '/
^                 # start-of-string
(?=.*[0-9])       # a digit must occur at least once
(?=.*[a-z])       # a lower case letter must occur at least once
(?=.*[A-Z])       # an upper case letter must occur at least once
(?=.*[@#$%^&+=])  # a special character must occur at least once
(?=\S+$)          # no whitespace allowed in the entire string
.{8,}             # anything, at least eight places though
$                 # end-of-string
/x';

Все что идет после символа хештега игнорируется, так же игнорируются символы пробелы. Из-за этого возникает подводный камень: поскольку пробелы игнорируются, что делать, если нужно сматчить пробел? Есть несколько способов:

  • Экранировать пробел $regex = '/\ +/x';
  • Использовать символьный класс $regex = '/\s+/x

Модификаторы шаблонов.

Регулярные выражения в PHP имеют множество модификаторов. В нем есть даже модификаторы, которых нет в PCRE. Поэтому, настоятельно рекомендую ознакомиться с документацией.

Модификатор «i»

Если установлен этот модификатор, то он делает шаблон нечувствительным к регистру. Таким образом, если у вас есть регулярка вида /[a-zA-Z0-9]+/, вы можете упростить ее до /[a-z0-9]+/i. Возможно, вы не захотите использовать данный модификатор, чтобы сделать регулярку более гибкой.

Модификатор «s»

Так же известен как модификатор/режим DOTALL. Если установлен этот модификатор, то все символы точки . будут соответствовать ВСЕМУ, включая символу новой строки. Например /a.*b/ будет матчить:

a test b

Но, не сматчит

a test
test b

Однако, /a.*b/s сматчит оба варианта.

Обратите внимание, что точка в классе символов [.] теряет свое значение и она будет соответстовать буквально точке.

Модификатор «m»

Так же известен как модификатор/режим MULTILINE. Цитата из документации:

By default, PCRE treats the subject string as consisting of a single “line” of characters (even if it actually contains several newlines). The “start of line” metacharacter ^ matches only at the start of the string, while the “end of line” metacharacter $ matches only at the end of the string, or before a terminating newline (unless D modifier is set). This is the same as Perl. When this modifier is set, the “start of line” and “end of line” constructs match immediately following or immediately before any newline in the subject string, respectively, as well as at the very start and end. This is equivalent to Perl’s /m modifier. If there are no \n characters in a subject string, or no occurrences of ^ or $ in a pattern, setting this modifier has no effect.

Что это значит на практике? Допустим мы хотим найти одну или несколько цифр [0-9]+ в начале каждой строки. Регулярка будет выглядеть следующим образом /^[0-9]+/m. Без модификатора /m регулярка будет соответствовать только цифрам в первой строке.

123     # Matched by /^[0-9]+/ and /^[0-9]+/m
1234    # Matched by /^[0-9]+/m
12345   # Matched by /^[0-9]+/m

Модификатор «u»

Если этот модификатор установлен, то входная строка и regex обрабатываются как UTF-8. Это означает, что при работе со строками UTF-8 необходимо включить этот модификатор.

Этот модификатор стоит включить, если вы не уверены что будете работать с ASCII (или однобайтовыми наборами символов). Обратите внимание, что классы символов, такие как \w, \d, \s, \b, … становятся совместимыми с Unicode, когда этот модификатор установлен.

Выбор разделителя.

Обычно в качестве разделителя в регулярном выражение используется символ слеша (косой черты) /. Но, можно использовать и другие разделители, особенно если в вашем регулярном выражение есть много символов слеша.

// Backslash
$regex = '/^\/user\/(\d+)\/?/i';

// Clean
$regex = '~^/user/(\d+)/?~i';

Другими разделителями являются символы #,!,%,_,;. Используйте тот, который с меньшей вероятностью будет часто встречаться в вашем регулярном выражении. Например # может быть использован в режиме x для комментариев или когда у вас просто есть регулярка с одним из этих символов.

Одним из не очень известных, но интересных способов является использование ассиметричной пары разделителей, таких как ():

$regex = '(^/user/(\d+)/?)i';

Обратите внимание, скобки внутри регулярки не нужно экранировать. Вы можете рассматривать первые скобки как «группу 0», а вторые (внутренние скобки) как «группу 1».

Знай что нужно экранировать.

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

// Непонятно
$regex = '~\>\>user\d+\,\ \"\d+\-\d+\"~';

// Понятно
$regex = '~>>user\d+, "\d+-\d+"~';

Что не нужно экранировать?

  • Следующий символы <>@!#~=_,'", если они не используются в качестве разделителя.
    • /<>@!#~=_,'"/ сматчит <>@!#~=_,'".
  • Пробелы, если ты не используешь модификатор x.
  • Дефисы вне группы символов.
    • /-+/ сматчит один или более дефисов.
  • Дефисы внутри группы символов в начале и в конце.
    • /[a-z-]+/ будет соответствовать диапазону букв от a до z, включая дефис. Пример: abcde-fgh.
    • /[-a-z]+/ тоже самое что и выше.