Перейти к содержимому
Поиск
Php Разработка

PHP: полный мануал с примерами, функциями и ООП

Практический мануал PHP: от переменных и читаемого кода до безопасных форм, интеграций, базы данных и ООП на примере магазина. Учебный маршрут, поиск функций, подробные примеры, разбор ошибок и исходники для скачивания.

Александр НекрасовИзменено:
Практический мануал PHP: от переменных и функций к объектной модели интернет-магазина
PHP 8.5.11149 глав4458 карточек справочника
Дерево руководства

Глава 80 · От первого запуска к редким конструкциям

PCRE: от первого правила до полного справочника preg_*

Все функции и вложенные темы официального book.pcre: форма текста, Unicode, группы, диагностика, конфигурация, ограничения и современная PCRE2.

Сначала полезный результат: проверить код товара

Наглядная модель

Шаблон ищет нужную часть текста

Нужно извлечь номер из строки «Заказ #42», а не заставить читателя сначала выучить всю грамматику регулярных выражений.

  1. Текст: Заказ #42

    Исходная строка остаётся неизменной.

  2. Шаблон: /#([0-9]+)/

    # буквально; [0-9]+ — одна или больше цифр; скобки сохраняют найденное.

  3. preg_match → 1

    Совпадение найдено. 0 означает отсутствие, false — ошибку.

  4. $matches[1] = «42»

    Это ещё строка цифр; правила допустимого номера задаёт приложение.

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

В заявке пришёл код BR41. Нужно понять, начинается ли он с BR и заканчивается ли цифрами, затем получить эти цифры. Регулярное выражение — маленькое правило формы текста; preg_match применяет его к строке. Правило не знает, существует ли товар, и не заменяет проверку склада. Начнём с одного вызова, затем раскроем его запись и расширим задачу. Полный официальный раздел PCRE здесь включает все функции, настройки и вложенные темы языка; редкие конструкции идут после основ.

Сохраните пример в UTF-8-файл regex-demo.php без BOM и выполните установленным PHP CLI. <?php начинает PHP-код; $code — имя переменной, = присваивает ей значение, кавычки задают текст, ; завершает команду. Скобки после preg_match передают аргументы через запятую: сначала правило, затем исходную строку. $found сохраняет ответ; echo выводит его. "\n" в двойных кавычках — перевод строки. Первый запуск PHP показывает запуск по шагам.

Код из заявки → проверка формы
  1. $code хранит BR41.
  2. Правило ~\ABR[0-9]+\z~: начало, BR, одна или больше цифр, конец.
  3. preg_match возвращает 1: весь BR41 подходит.
  4. echo выводит 1; никаких внешних действий не выполняется.
Первая проверка кода товараСамостоятельный пример
<?php
$code = 'BR41';
$found = preg_match('~\ABR[0-9]+\z~', $code);
echo $found, "\n";
Ожидаемый результат
1

Тильды ограничивают тело шаблона; \A/\z привязывают его к целой строке. BR — обычные буквы, [0-9] выбирает цифру, + повторяет выбор хотя бы раз. Кавычки принадлежат PHP, не самому regex.

У preg_match три разных ответа: 1 найдено, 0 не найдено, false техническая ошибка. Оператор === сравнивает и значение, и тип. if (...) выполняет блок в { ... } только при истинном условии, elseif проверяет следующую ветвь, else — оставшийся случай. echo печатает своё сообщение: один голый false на экране был бы незаметен.

Понятный ответ для неверного кодаСамостоятельный пример
<?php
$code = 'BR?';
$found = preg_match('~\ABR[0-9]+\z~', $code);
if ($found === false) {
    echo "Проверка не выполнилась\n";
} elseif ($found === 1) {
    echo "Форма кода подходит\n";
} else {
    echo "После BR нужны цифры\n";
}
Ожидаемый результат
После BR нужны цифры

Неподходящий код — штатный ответ правила, не поломка regex. Сообщение не обещает, что товар найден в каталоге.

Дальше понадобятся массивы для нескольких совпадений, foreach для их обхода, function/return для повторного использования и callback-функции для вычисляемой замены. Текущие примеры вводят местный синтаксис до использования; ссылки помогают вернуться к языковой основе. Вложенные (...), условия рядом и рекурсия не нужны, чтобы выполнить первые два примера.

Как выбрать функцию и когда обойтись без regex

Выберите действие по результату, который действительно нужен. Буквальный поиск и разбиение проще без regex; регулярное правило окупается, когда форма меняется, есть поля или несколько видов разделителя.

НужноИнструмент
Первое совпадение / проверить формуpreg_match
Все совпадения с полямиpreg_match_all
Отобрать элементы массива без измененияpreg_grep
Разделить по правилуpreg_split
Вставить буквальные данные в patternpreg_quote
Заменить фиксированным текстом/группамиpreg_replace
Заменить и вернуть только затронутоеpreg_filter
Вычислить заменуpreg_replace_callback
Разным правилам разные вычисленияpreg_replace_callback_array
Разобрать технический отказpreg_last_error и preg_last_error_msg
Точная подстрока / разделитель / словарьstr_contains/strpos, explode, str_replace/strtr: полный раздел строковых функций
Структурированный форматJSON → json_decode, CSV → str_getcsv/fgetcsv, HTML → DOM/HTML parser, URL → parse_url + проверка договора. Regex не заменяет весь парсер.

Поиск идёт слева направо по subject, но внутри одной попытки возможен возврат — backtracking. Левейшее успешное совпадение не обязательно самое длинное. PHP сохраняет compiled patterns в cache до 4096 на thread, очищая старые неиспользуемые записи партиями; размер не настраивается. Это не cache результатов и не гарантия общей памяти между HTTP-запросами. Не собирайте бесконечно разные шаблоны, если можно вынести меняющиеся данные в обычный словарь.

Установка и версия: PHP, PCRE2 и JIT

pcre встроено в PHP и всегда доступно в стандартной поддерживаемой сборке. На Windows не нужно добавлять extension=pcre в php.ini. Сначала проверьте тот executable, который запускает задачу: PHP_VERSION и PCRE_VERSION — разные версии. Здесь новые run проверены на PHP8.5.11 / PCRE2 10.44; другой PHP может быть собран с другой библиотекой.

Диагностика окружения
  1. PHP CLI запускает ваш файл.
  2. PCRE_VERSION сообщает именно библиотеку правил.
  3. PCRE_JIT_SUPPORT сообщает возможность JIT; ini_get — текущую настройку.
  4. Версии и настройки сравнивают до переноса нового синтаксиса.
Посмотреть своё окружение без его измененияПример для указанного окружения
<?php
echo 'PHP ', PHP_VERSION, "\n";
echo 'PCRE ', PCRE_VERSION, "\n";
echo 'JIT support: ', PCRE_JIT_SUPPORT ? 'yes' : 'no', "\n";
echo 'JIT setting: ', ini_get('pcre.jit'), "\n";

Это environment-dependent диагностика, mode=lint: числа зависят от вашей сборки, универсальный exact stdout не заявлен. Локальная проверенная среда указана отдельно. Тернарный оператор условие ? да : нет выбирает одну из двух надписей.

Обычная сборка использует bundled PCRE2 и по умолчанию поддерживает JIT. На не-Windows сборщик может выбрать --with-external-pcre (минимум10.30), а --without-pcre-jit отключает JIT при сборке. На Windows используется bundled library. Это сведения для администратора сборки, не инструкция менять ваш компьютер. Установка CLI и отличие конфигурации веб-SAPI — в отдельной главе.

Версия PHPВстроенная PCRE по истории PHP.net
8.4.010.44
8.3.010.42
8.2.010.40
8.1.1 / 8.1.010.39 / 10.37
7.4.12 и 8.0.010.35
7.4.6 / 7.4.010.34 / 10.33
7.3.010.32, переход на PCRE2
7.2.0 / 7.0.3 / 7.0.0PCRE1 8.41 / 8.38 / 8.37; исторические неподдерживаемые PHP-ветви, не варианты для новой установки

Таблица — история обновлений bundled library, не обещание полного соответствия любого дистрибутива. Старые выпуски PCRE1 в PHP.net связаны с CVE-исправлениями; перенос приложения должен включать поддерживаемую PHP-ветвь целиком, а не ручную замену одного DLL. PHP8.4 добавляет r и новые возможности bundled10.44; PHP8.2 — внешний n. Для external PCRE смотрите реальный PCRE_VERSION. Устанавливать ничего для примеров этой главы не нужно.

Три настройки и отдельные пределы компиляции

Есть три настройки PCRE, все INI_ALL: их можно менять разрешёнными механизмами соответствующей SAPI, включая ini_set. Но это три разных ограничения/переключателя, а не одна «максимальная длина строки». Текущее ini_get может отличаться от factory default. Для CLI и FPM проверяйте каждую конфигурацию отдельно.

Директива / стандартное значениеСмысл и ограничения
pcre.backtrack_limit = 1000000Предел match-work; до PHP5.3.7 default100000. Не байты, не число найденных групп и не таймаут. Счётчик у interpreter/JIT устроен различно.
pcre.recursion_limit = 100000PHP передаёт depth limit PCRE2. В современном interpreter это глубина matching frames; JIT его не использует. Историческое предупреждение PHP.net о C-stack описывает прежнюю архитектуру, но расход памяти и другие стеки всё равно ограничены.
pcre.jit = 1Разрешает JIT при наличии поддержки и пригодного шаблона. Это не обещание, что каждый match действительно выполнится JIT. У него отдельный stack limit.
Локальное изменение с восстановлением
  1. ini_set возвращает прежнее значение либо false.
  2. Временно записываем известный небольшой лимит.
  3. Выполняем локальную проверку.
  4. finally возвращает прежнее значение даже при исключении.
Прочитать и временно задать лимит в своём процессеСамостоятельный пример
<?php
declare(strict_types=1);

$old = ini_set('pcre.backtrack_limit', '5000');
if ($old === false) { throw new RuntimeException('Setting is unavailable'); }
try { echo ini_get('pcre.backtrack_limit'), "\n"; }
finally { ini_set('pcre.backtrack_limit', $old); }
echo ini_get('pcre.backtrack_limit') === $old ? 'Восстановлено' : 'Не восстановлено';
echo "\n";
Ожидаемый результат
5000
Восстановлено

Exact stdout не зависит от исходного числа: мы временно задаём своё и проверяем восстановление, не меняя php.ini. Никакие настройки других процессов не затрагиваются.

Стартовые (*LIMIT_MATCH=n), (*LIMIT_DEPTH=n), (*LIMIT_HEAP=n) позволяют шаблону снизить соответствующие ограничения, не поднять лимиты вызывающей стороны. HEAP в KiB относится к interpreter, DEPTH не действует для JIT; исторический LIMIT_RECURSION остаётся alias DEPTH. PHP не предоставляет общую настройку таймаута regex. Не подставляйте отрицательные/огромные числа «для отключения защиты». Компиляция имеет собственные пределы: примерно64K code units compiled pattern при default2-byte linkage, до65535captures, default250 вложенных групп, variable lookbehind по умолчанию255. Эти величины могут зависеть от сборки, а исходная длина pattern не равна compiled size. Некоторые upstream ман-страницы от10.43 всё ещё пишут32 для имени capture, но tagged10.44 и PHP.net subpatterns фиксируют128; не переносите старую таблицу без версии.

Наблюдаемый контролируемый отказ при малом лимите — в разделе производительности. После отказа задача должна честно сообщить, что проверка не завершилась. Пытаться повторять всё с удвоенным лимитом по запросу клиента — плохая политика нагрузки.

Все константы: флаги, ошибки и версия

Константа — именованное значение PHP без знака $. Она объясняет намерение и избавляет от запоминания чисел. Есть флаги, коды ошибок и сведения о сборке: их нельзя смешивать, даже если внутреннее число случайно совпало. Оператор | объединяет совместимые биты флагов; это не логическое ||.

Константа / типНазначение
PREG_PATTERN_ORDERint: preg_match_all по группам; режим по умолчанию.
PREG_SET_ORDERint: preg_match_all по совпадениям; не объединять с предыдущим.
PREG_OFFSET_CAPTUREint: элементы captures становятся [текст, байтовая позиция].
PREG_UNMATCHED_AS_NULLint, PHP7.2+: отсутствовавшие captures → null; конечные у preg_match с PHP7.4; в паре с OFFSET_CAPTURE → [null,-1].
PREG_SPLIT_NO_EMPTYint: убрать пустые части, включая крайние.
PREG_SPLIT_DELIM_CAPTUREint: вернуть захватывающие группы разделителя.
PREG_SPLIT_OFFSET_CAPTUREint: пары [часть, byteOffset] именно у split; не подменять PREG_OFFSET_CAPTURE.
PREG_GREP_INVERTint: отобрать не совпавшие значения массива.
PREG_NO_ERRORint, PHP5.2+: технической ошибки нет, даже если совпадений 0.
PREG_INTERNAL_ERRORint, PHP5.2+: общий внутренний/compile отказ; точный compile-текст дополнительно приходит warning.
PREG_BACKTRACK_LIMIT_ERRORint, PHP5.2+: исчерпан match-work limit.
PREG_RECURSION_LIMIT_ERRORint, PHP5.2+: исчерпан depth limit; имя историческое.
PREG_BAD_UTF8_ERRORint, PHP5.2+: недопустимые UTF-8 байты subject.
PREG_BAD_UTF8_OFFSET_ERRORint, PHP5.3+: старт внутри UTF-8 code point.
PREG_JIT_STACKLIMIT_ERRORint, PHP7.0+: недостаточен отдельный стек JIT.
PCRE_VERSIONstring, PHP5.2.4+: версия и дата библиотеки, не PHP.
PCRE_VERSION_MAJOR / PCRE_VERSION_MINORint: две части версии PCRE; удобны для диагностической карты.
PCRE_JIT_SUPPORTbool: сборка поддерживает JIT; не доказательство его применения к каждому правилу.
Флаг меняет форму результата
  1. BR41 найден в строке x BR41.
  2. Без флага элемент [0] — "BR41".
  3. С OFFSET_CAPTURE элемент [0] — ["BR41",2].
  4. Код ошибок PREG_NO_ERROR читают отдельно после операции.
Одна находка, два формата ответаСамостоятельный пример
<?php
declare(strict_types=1);

preg_match('~BR[0-9]+~', 'x BR41', $plain);
preg_match('~BR[0-9]+~', 'x BR41', $position, PREG_OFFSET_CAPTURE);
echo $plain[0], "\n";
echo $position[0][0], '@', $position[0][1], "\n";
echo preg_last_error() === PREG_NO_ERROR ? 'Без ошибки' : 'Ошибка';
echo "\n";
Ожидаемый результат
BR41
BR41@2
Без ошибки

Номер byteOffset2 не является индексом группы: у этих двух чисел разные роли. Отдельная константа PREG_SPLIT_OFFSET_CAPTURE нужна именно preg_split.

Развёрнутые применения flags есть в карточках preg_match_all, preg_split и preg_grep. PREG_PATTERN_ORDER и PREG_SET_ORDER выбирают альтернативные формы, их не нужно объединять. Набор поддерживаемых flags зависит от функции: недопустимая комбинация в современном PHP может вызвать ValueError, поэтому не передавайте «все флаги сразу».

Четыре разных ситуации: найдено, не найдено, правило сломано, ввод повреждён

Перед сохранением кода заказа важно понимать, что проверка действительно состоялась. 0 — нормальное отсутствие совпадения; false/null зависят от выбранной функции и требуют диагностики. Неправильный regex вызывает warning при компиляции; неверный тип аргумента может бросить исключение PHP ещё до поиска. Ошибка runtime (UTF-8, лимит) обычно отражается return и последним PCRE-кодом.

Четыре разных ситуации: найдено, не найдено, правило сломано, ввод повреждён: путь данных
  1. Заведомо повреждённый ~[~ не компилируется.
  2. Локальный обработчик записывает сам факт E_WARNING.
  3. preg_match возвращает false; обычной проверки текста не произошло.
Отделить предупреждение о правиле от результатаСамостоятельный пример
<?php
declare(strict_types=1);

$warnings = 0;
set_error_handler(function (int $level) use (&$warnings): bool {
    if ($level === E_WARNING) { ++$warnings; return true; }
    return false;
});
try { $result = preg_match('~[~', 'BR41'); }
finally { restore_error_handler(); }
echo $result === false ? 'Правило не скомпилировано' : 'Готово';
echo '/', $warnings, "\n";
Ожидаемый результат
Правило не скомпилировано/1

Обработчик здесь нужен только для намеренно плохого учебного шаблона. Он возвращает true для обработанного warning, затем обязательно снимается. В рабочем коде исправьте собственный pattern; не прячьте ошибки через @.

Замену нельзя считать выполненной на повреждённом UTF-8Самостоятельный пример
<?php
declare(strict_types=1);

$result = preg_replace('~x~u', 'y', "\xFF");
$error = preg_last_error();
echo $result === null ? 'Нет результата' : 'Результат есть';
echo '/', $error === PREG_BAD_UTF8_ERROR ? 'bad-utf8' : 'другое', "\n";
Ожидаемый результат
Нет результата/bad-utf8

Технический отказ не означает, что исходный текст уже чистый. Не заменяйте null на пустую строку молча: иначе данные потеряются.

Сохраняйте preg_last_error и preg_last_error_msg немедленно: последующий regex, включая логгер или валидацию сообщения, может сменить состояние. Параметры с & — выходные переменные; не передавайте туда выражение. Неверные flags/типы относятся к PHP API и не обязаны менять last_error. Исключение callback также остаётся исключением. У preg_filter null может быть штатным отсутствием замен, поэтому смотрите его отдельный договор. Для журналов достаточно имени правила, кода, размера входа и обезличенного контекста; полный пользовательский текст часто избыточен.

Все модификаторы и их версии

Буквы после закрывающего разделителя выбирают режим всего шаблона. Они не передаются вместо PHP-flags аргумента. Например i меняет сравнение, а PREG_OFFSET_CAPTURE меняет структуру возвращённых данных. В модификаторах пробел/LF могут игнорироваться, но неизвестная буква даёт warning; писать лишнее оформление не стоит.

МодификаторДействие и версия
iCaseless; Unicode /u может сопоставлять разные code points с общим регистром.
m^/$ начинают/заканчивают строки внутри subject; не делает точку многострочной.
sТочка включает newline; не влияет на отрицательный класс, уже допускающий его.
xПробелы оформления и #комментарии вне классов игнорируются; экранируйте буквальные пробел/#.
AПопытка только на стартовой позиции offset; это не то же, что абсолютный \A.
DЗаставляет $ означать абсолютный конец; игнорируется с m, не меняет \Z.
SИсторический study hint; с PHP7.3 не имеет эффекта.
UПереворачивает жадность: обычный repeat становится lazy, с ? — greedy.
XИсторический EXTRA; в PHP8.5.11 внешний X no-op, неизвестные escapes ошибочны и без него. Внутренний (?X) запрещён с PHP7.3.
JРазрешить одинаковые имена captures; внешний модификатор с PHP7.2, внутренний (?J) существовал раньше.
uUTF-8 плюс UCP в PHP: pattern и subject должны быть корректными; offsets остаются байтовыми.
nPHP8.2+: обычные (...) не захватывают, именованные захватывают и получают номера.
rPHP8.4+: при iu исключает cross-ASCII/non-ASCII caseless, например K и Kelvin K.
Caseless и граница алфавита
  1. Unicode Kelvin K родственен латинской K при case folding.
  2. /iu допускает совпадение K с K.
  3. /iur запрещает переход между ASCII/non-ASCII, результат0.
Не разрешать Kelvin вместо ASCII KСамостоятельный пример
<?php
declare(strict_types=1);

echo preg_match('~\AK\z~iu', 'K'), '/', preg_match('~\AK\z~iur', 'K'), "\n";
echo preg_match('~\AГ\z~iur', 'г'), "\n";
Ожидаемый результат
1/0
1

r PHP8.4+ не отключает весь Unicode-регистр: Г/г остаются парой. Для ASCII идентификатора явный ASCII-алфавит без /i часто проще.

J сегодня допустим, S/X не добавляют возможностиСамостоятельный пример
<?php
declare(strict_types=1);

preg_match('~(?<code>BR)|(?<code>NT)~J', 'NT', $m);
echo $m['code'], "\n";
echo preg_match('~\ABR41\z~SX', 'BR41'), "\n";
Ожидаемый результат
NT
1

Официальная историческая страница examples называет /J ошибкой; с PHP7.2 это неверно. S/X в этой проверенной версии ничего не меняют — не используйте их как «ускорение» или дополнительную защиту.

Сочетания n/J и группа (?i:...) показаны в разделе групп и локальных настроек. При внешнем PCRE версия PHP сама по себе не гарантирует всякую новую возможность библиотеки. /u — не HTML-sanitizer, /i — не нормализация Unicode, /s — не разрешение опасного регулярного выражения.

Как проверять собственный шаблон на небольших примерах

До применения к файлу импорта составьте маленькую таблицу «ввод → ожидаемое решение». Включите обычный код, пустой ввод, лишний хвост, перевод строки и похожую букву другого алфавита. Это проверяет договор, а не только то, что один удачный пример когда-то прошёл.

Как проверять собственный шаблон на небольших примерах: путь данных
  1. BR41 ожидается1, BR? ожидается0.
  2. Цикл применяет одно правило к каждому случаю.
  3. При несовпадении с ожиданием пример сообщает конкретный статус.
Минимальная проверка правила кодаСамостоятельный пример
<?php
declare(strict_types=1);

$cases = [['BR41', 1], ['BR?', 0], ['', 0], ["BR41\n", 0], ['ВR41', 0]];
foreach ($cases as [$input, $expected]) {
    $actual = preg_match('~\ABR[0-9]+\z~', $input);
    echo $actual === $expected ? 'PASS' : 'FAIL';
    echo "\n";
}
Ожидаемый результат
PASS
PASS
PASS
PASS
PASS

В последнем вводе первая буква кириллическая В. Деструктуризация [$input,$expected] читает два элемента каждой пары; === обнаружит и false вместо ожидаемого0.

Та же проверка в прикладном действииСамостоятельный пример
<?php
declare(strict_types=1);

function codeForLabel(string $input): ?string
{
    $status = preg_match('~\A(BR|NT)([0-9]{1,4})\z~', $input, $m);
    if ($status === false) { throw new RuntimeException(preg_last_error_msg()); }
    return $status === 1 ? $m[1] . ' / ' . $m[2] : null;
}
echo codeForLabel('BR41') ?? 'Проверьте код', "\n";
echo codeForLabel('XX41') ?? 'Проверьте код', "\n";
Ожидаемый результат
BR / 41
Проверьте код

Функция связывает regex с полезным результатом — меткой для печати. На уровне UI остаётся экранирование HTML; здесь формируется обычный текст, не веб-страница.

Старая страница PCRE examples полезна как набор форм разделителей, но её вывод о J нужно читать с версией. Отсутствующий разделитель/закрытие и неизвестный модификатор остаются ошибками; пример с /J сегодня допустим. Исходный шаблон, ожидание и результат храните вместе, чтобы изменение требования не свелось к случайному добавлению ?.

Две оболочки: строка PHP и разделители шаблона

Нужно найти путь /catalog/bolts. Кавычки сначала создают строку PHP, затем preg-функция читает внутри неё разделитель ~ и тело правила. Разделитель — один неалфавитно-цифровой байт, не пробел, не обратный слеш и не NUL. Обычно это ~, # или /. Начальные пробелы перед корректным разделителем игнорируются, но яснее их не писать. UTF-8 знак § не подходит: он занимает несколько байтов.

Две оболочки: строка PHP и разделители шаблона: путь данных
  1. PHP читает строку "~ /catalog/... ~" без показанных пояснительных пробелов.
  2. Тильда ограничивает шаблон; слеши пути теперь обычные символы.
  3. После конечной тильды i разрешает различие регистра.
Найти путь без стены обратных слешейСамостоятельный пример
<?php
declare(strict_types=1);

echo preg_match('~/catalog/bolts~i', 'Go to /CATALOG/BOLTS'), "\n";
echo preg_match('/\/catalog\/bolts/i', 'Go to /CATALOG/BOLTS'), "\n";
Ожидаемый результат
1
1

Оба правила ищут одинаковый фрагмент. В варианте с / каждый внутренний слеш экранирован, чтобы PHP не принял его за конец шаблона.

Разделитель в данныхСамостоятельный пример
<?php
declare(strict_types=1);

$product = 'bolt~large';
$pattern = '~\A' . preg_quote($product, '~') . '\z~';
echo preg_match($pattern, 'bolt~large'), "\n";
echo preg_match('{\ABR[0-9]+\z}', 'BR41'), "\n";
Ожидаемый результат
1
1

В первом случае preg_quote знает выбранную тильду. Второй показывает парные разделители { и }; сами фигурные скобки-оболочки не означают повторение.

Допускаются парные (), {}, [], <>; PHP должен корректно найти закрывающий разделитель, а буквальные скобки внутри требуют осторожного экранирования. Для начала выбирайте ~. Даже внутри [...] и \Q...\E разделитель всё ещё обрабатывается PHP. Разделитель отсутствует, не закрыт или после него неизвестная буква — E_WARNING, а не нормальный поиск. Для буквального пользовательского фрагмента используйте preg_quote.

Метасимволы: выбрать один знак или последовательность

Формат артикула BR41 можно описать без перечисления каждого возможного номера. Метасимвол — знак, который задаёт действие в языке шаблона. Остальные символы сравниваются буквально. Положение важно: . вне класса означает любой допустимый знак, а [.] — именно точку. Класс [...] выбирает один знак, группа (...) объединяет целую последовательность.

ЗаписьКак читать
\Escape: буквальный метасимвол или специальная команда.
^ / $Позиция начала/конца; особенности перевода строки — в разделе якорей.
.Один знак, обычно кроме перевода строки.
[...]Один знак из множества; [^...] — вне множества.
|Альтернативная ветвь.
(...)Группа, обычно с сохранением текста.
* / + / ?0+, 1+, 0–1 повторений предыдущего элемента.
{n} / {n,m}Точное число либо диапазон повторений.
Метасимволы: выбрать один знак или последовательность: путь данных
  1. [A-Z] выбирает одну заглавную ASCII-букву.
  2. {2} повторяет этот выбор дважды → BR.
  3. [0-9]+ выбирает непустую последовательность цифр → 41.
Собрать правило из трёх частейСамостоятельный пример
<?php
declare(strict_types=1);

echo preg_match('~\A[A-Z]{2}[0-9]+\z~', 'BR41'), "\n";
echo preg_match('~\A[A-Z]{2}[0-9]+\z~', 'B41'), "\n";
Ожидаемый результат
1
0

Две буквы — условие продукта; одна буква недостаточна. Результат 0 означает только несоответствие этому договору.

Точка в версии файла должна быть точкойСамостоятельный пример
<?php
declare(strict_types=1);

echo preg_match('~\Av1.2\z~', 'v1x2'), "\n";
echo preg_match('~\Av1[.]2\z~', 'v1x2'), "\n";
echo preg_match('~\Av1\.2\z~', 'v1.2'), "\n";
Ожидаемый результат
1
0
1

Первый шаблон намеренно слишком широк. Два способа буквальной точки — [.] и \. — устраняют эту ошибку.

Внутри класса метасмысл имеют \, начальный ^, дефис диапазона -, закрывающий ], а [ участвует в специальных POSIX-записях. Все остальные знаки обычно буквальны; PHP-разделитель всё равно нужно экранировать. Вне класса ] и } без парного начала могут быть литералами, но намеренно экранированная пунктуация понятнее. Внешний ? также начинает специальные конструкции после (: (?:...), (?=...) и другие разобраны дальше.

Обратный слеш: PHP сначала, regex потом

Имя папки может содержать \, а строка отчёта — табуляцию. Слеш бывает командой PHP-строки и командой PCRE: эти два уровня нельзя смешивать. В одинарной PHP-строке специально обрабатываются \\ и \'; прочие сочетания вроде \d проходят к regex. Чтобы PCRE получил \\ для буквального слеша, в PHP-коде нужно написать четыре слеша. Nowdoc сохраняет текст без PHP-интерполяции и удобен для длинного правила.

ЗаписьКак читать
\a / \e / \f / \n / \r / \tBEL 07, ESC 1B, formfeed 0C, LF 0A, CR 0D, TAB 09.
\cxASCII control: например \cJ — LF; не произвольная Unicode-буква.
\xhh / \x{hhhh} / \N{U+hhhh}Hex-код: первые два hex digits или явная code point; последний вариант требует UTF.
\0dd / \o{ddd}Octal-код. Начальный 0 снимает смысл back reference; фигурная форма удобнее для длинного числа.
\d/\D, \w/\W, \s/\SЦифра/не цифра, word/не word, whitespace/не whitespace; /u меняет множества.
\h/\H, \v/\VГоризонтальный/вертикальный пробел и дополнение; не зависят от выбора newline для точки.
\R / \NПоследовательность перевода строки / один знак кроме newline; \R воспринимает CRLF целиком.
\b/\B, \A/\Z/\z/\GУсловия на позицию без потребления текста; не использовать внутри класса.
\p{...}/\P{...}/\XUnicode-свойство, его отрицание, графемный кластер.
Обратный слеш: PHP сначала, regex потом: путь данных
  1. PHP-литерал содержит четыре слеша между save и report.
  2. PCRE получает два: команду «один буквальный слеш».
  3. В тексте save\report найден один реальный слеш.
Сверить буквальное имя с обратным слешемСамостоятельный пример
<?php
declare(strict_types=1);

$path = 'save\report';
echo preg_match('~\Asave\\\\report\z~', $path), "\n";
echo bin2hex($path), "\n";
Ожидаемый результат
1
736176655c7265706f7274

Hex 5c — единственный байт обратного слеша. Он не является разделителем файла на диске: пример проверяет только текст.

Найти табуляцию и сохранить видимую меткуСамостоятельный пример
<?php
declare(strict_types=1);

$line = "BR41\t12\n";
echo preg_replace('~\t~', ' | ', $line);
echo preg_match('~\x{20AC}~u', 'Цена €'), "\n";
Ожидаемый результат
BR41 | 12
1

В двойной PHP-строке \t создаёт табуляцию; в одинарном шаблоне \t прочитает PCRE. \x{20AC} — euro code point, а /u выбирает UTF-8.

Буквальная цена и новая точка начала результатаСамостоятельный пример
<?php
declare(strict_types=1);

preg_match('~\Q$1.00\E~', 'Price $1.00', $price);
echo $price[0], "\n";
preg_match('~SKU:\K[A-Z]{2}[0-9]+~', 'SKU:BR41', $code, PREG_OFFSET_CAPTURE);
echo $code[0][0], '@', $code[0][1], "\n";
Ожидаемый результат
$1.00
BR41@4

\Q...\E делает метасимволы буквальными, но не защищает PHP-разделитель и встроенную \E из внешнего ввода. \K проверяет SKU:, затем сбрасывает начало сообщаемого совпадения; уже захваченные группы сохраняются.

Неизвестный escape вроде \q — ошибка в PCRE2. В числовых escapes старые короткие записи неоднозначны: \11 может быть группой 11 либо octal 11. Для ясности выбирайте \g{1} и \o{11}/\x09. В классе \b — backspace 08, снаружи — граница слова. Старые тексты про максимум 99 back references не описывают PCRE2: лимит 99 относится к числовым ссылкам replacement в preg_replace. \K внутри lookaround по умолчанию запрещён с PCRE2 10.38; не переносите туда этот приём. PHP поддерживает NUL в строках; историческое утверждение о NUL как обязательном конце шаблона к современному PHP не относится.

Байты, Unicode properties и видимые знаки

В слове «гайка» буквы должны проверяться как UTF-8, а цену € нельзя измерять одним байтом. В PHP модификатор /u включает UTF и Unicode properties (UCP). Поэтому \d означает Unicode-десятичную цифру, \s Unicode-пробелы, \w включает буквы/числа/соединители и с PCRE2 10.43 также Mn. Для протокола с ASCII-цифрами пишите [0-9]. Без /u строка разбирается побайтно, таблицы некоторых классов могут зависеть от locale. Отдельный (*UTF) в PCRE2 сам по себе не включает UCP; не приравнивайте его к PHP /u.

Байты, Unicode properties и видимые знаки: путь данных
  1. "гайка 12" содержит кириллицу и fullwidth digits.
  2. /u + \p{L}+ находит слово как буквы.
  3. \d принимает 12; [0-9] их отклоняет.
Unicode-текст и ASCII-договор — разные проверкиСамостоятельный пример
<?php
declare(strict_types=1);

preg_match('~\p{L}+~u', 'гайка 12', $m);
echo $m[0], "\n";
echo preg_match('~\A\d+\z~u', '12'), '/', preg_match('~\A[0-9]+\z~', '12'), "\n";
Ожидаемый результат
гайка
1/0

Код товара может требовать ASCII, хотя название допускает много языков. /u не означает «разрешить любой символ».

Один видимый знак из двух code pointsСамостоятельный пример
<?php
declare(strict_types=1);

$text = "e\u{0301}Я";
preg_match_all('~. ~ux', $text, $points);
preg_match_all('~\X~u', $text, $clusters);
echo strlen($text), '/', count($points[0]), '/', count($clusters[0]), "\n";
echo bin2hex($clusters[0][0]), "\n";
Ожидаемый результат
5/3/2
65cc81

x игнорирует пробел в учебном правиле, поэтому . считает code points. \X сохраняет e с акцентом одним кластером. Графема — модель текстового сегмента, не гарантия одного пиксельного глифа на любом шрифте.

Письменность и регистр свойствСамостоятельный пример
<?php
declare(strict_types=1);

echo preg_match('~\A\p{Cyrillic}+\z~u', 'Гайка'), "\n";
echo preg_match('~\A\p{Lu}\z~iu', 'г'), "\n";
echo preg_match('~\A\p{Lu}\z~iu', 'Г'), "\n";
Ожидаемый результат
1
0
1

Свойство Lu означает uppercase даже при /i; caseless не превращает его в произвольную букву. Cyrillic — письменность, а не доказательство русского языка или отсутствия визуальных подмен.

Каждая code point имеет general category. Однобуквенная L объединяет Ll/Lm/Lo/Lt/Lu; \pL сокращает \p{L}. \P{Lu} и \p{^Lu} — отрицание. Unicode blocks вроде InMusicalSymbols и произвольные Perl properties не поддерживаются. PCRE2 также понимает script/sc/scx, некоторые binary properties и Bidi_Class: сверяйте установленную библиотеку. Cs описывает суррогаты, но допустимый UTF-8 их не содержит. Ни /u, ни \X не нормализуют NFC/NFD: визуально похожие строки могут иметь разные байты. Для нормализации нужен отдельный осознанный шаг intl. Повреждённый UTF-8 приводит к false/PREG_BAD_UTF8_ERROR, а не штатному 0.

General categoryСмысл
Cпрочее
Ccуправляющий
Cfформатирующий
Cnне назначен
Coчастное использование
Csсуррогат
Lбуква
Llстрочная
Lmмодификатор-буква
Loдругая буква
Ltтитульная
Luпрописная
Mкомбинируемая метка
Mcзанимающая место
Meокружающая
Mnне занимающая место
Nчисловой знак
Ndдесятичная цифра
Nlбуквенное число
Noдругой числовой знак
Pпунктуация
Pcсоединитель
Pdтире
Peзакрывающая
Pfконечная кавычка
Piначальная кавычка
Poпрочая
Psоткрывающая
Sсимвол
Scвалюта
Skмодификатор
Smматематический
Soдругой, включая многие emoji
Zразделитель
Zlстрок
Zpабзацев
Zsпробел

Имена письменностей, перечисленные на официальной PHP-странице (не обещание, что это весь более новый Unicode-набор библиотеки): Arabic, Armenian, Avestan, Balinese, Bamum, Batak, Bengali, Bopomofo, Brahmi, Braille, Buginese, Buhid, Canadian_Aboriginal, Carian, Chakma, Cham, Cherokee, Common, Coptic, Cuneiform, Cypriot, Cyrillic, Deseret, Devanagari, Egyptian_Hieroglyphs, Ethiopic, Georgian, Glagolitic, Gothic, Greek, Gujarati, Gurmukhi, Han, Hangul, Hanunoo, Hebrew, Hiragana, Imperial_Aramaic, Inherited, Inscriptional_Pahlavi, Inscriptional_Parthian, Javanese, Kaithi, Kannada, Katakana, Kayah_Li, Kharoshthi, Khmer, Lao, Latin, Lepcha, Limbu, Linear_B, Lisu, Lycian, Lydian, Malayalam, Mandaic, Meetei_Mayek, Meroitic_Cursive, Meroitic_Hieroglyphs, Miao, Mongolian, Myanmar, New_Tai_Lue, Nko, Ogham, Old_Italic, Old_Persian, Old_South_Arabian, Old_Turkic, Ol_Chiki, Oriya, Osmanya, Phags_Pa, Phoenician, Rejang, Runic, Samaritan, Saurashtra, Sharada, Shavian, Sinhala, Sora_Sompeng, Sundanese, Syloti_Nagri, Syriac, Tagalog, Tagbanwa, Tai_Le, Tai_Tham, Tai_Viet, Takri, Tamil, Telugu, Thaana, Thai, Tibetan, Tifinagh, Ugaritic, Vai, Yi. Common объединяет общие знаки, Inherited — наследующие письменность контекста. Пробел/дефис в имени человека обычно нужно разрешить отдельно; список письменностей не является готовой политикой имени. До PCRE8.32 (bundled до PHP5.4.14) \X имел более узкую модель base+marks; современная версия использует extended grapheme cluster. Unicode-таблицы и их стоимость зависят от версии, поэтому не делайте универсальных заявлений «Unicode медленный».

Проверить всю строку, строку отчёта или точный offset

Код заказа не должен принимать скрытый перевод строки на конце. ^ и $ удобны для строк отчёта, но $ по умолчанию разрешает позицию перед последним newline. Для строгого поля используйте \A...\z. Все якоря проверяют позицию: сами символы не поглощают.

Проверить всю строку, строку отчёта или точный offset: путь данных
  1. Ввод "BR41\n" содержит завершающий LF.
  2. ^...$ допускает позицию перед LF.
  3. \A...\z требует дойти до абсолютного конца → 0.
Последний перевод строки имеет значениеСамостоятельный пример
<?php
declare(strict_types=1);

$code = "BR41\n";
echo preg_match('~^BR[0-9]+$~', $code), '/', preg_match('~\ABR[0-9]+\z~', $code), "\n";
echo preg_match('~^BR[0-9]+$~D', $code), "\n";
Ожидаемый результат
1/0
0

Модификатор D делает $ end-only без /m. \z выразительнее, когда нужен именно полный договор поля.

Найти целую строку внутри отчётаСамостоятельный пример
<?php
declare(strict_types=1);

$report = "NT8\nBR41\nNT9";
preg_match_all('~^BR[0-9]+$~m', $report, $m);
echo implode(',', $m[0]), "\n";
echo preg_match('~\ABR[0-9]+\z~m', $report), "\n";
Ожидаемый результат
BR41
0

/m меняет ^/$, но не \A/\z. /s имеет другую роль: он меняет точку, не якоря.

Начало поиска не становится началом строкиСамостоятельный пример
<?php
declare(strict_types=1);

$text = 'x BR41';
echo preg_match('~^BR~', $text, $m, 0, 2), '/', preg_match('~\GBR~', $text, $m, 0, 2), "\n";
echo preg_match('~BR~A', $text, $m, 0, 2), "\n";
echo preg_match('~(?<=x )BR~', $text, $m, 0, 2), "\n";
Ожидаемый результат
0/1
1
1

\G и A привязаны к старту текущего поиска; ^ без /m — к исходному началу. Lookbehind видит "x " до offset, чего не было бы после substr.

\Z разрешает конец или позицию перед финальным newline независимо от /m; \z — только конец. \b — граница между word/non-word либо соответствующий край строки, \B — её отсутствие. Это не универсальная граница языкового слова: дефис/апостроф и Unicode-модель требуют выбора. У всех альтернатив должно быть нужное ограничение: \A(?:BR|NT)\z, а не \ABR|NT\z. Выбор CR/LF/CRLF разбирается вместе с точкой.

Точка, переводы строк и запрет разрывать UTF-8

Поле заметки может занимать две строки. . без /s пропускает один символ, кроме выбранного newline; /s позволяет ему пройти перевод строки. Это не меняет ^/$ и не влияет на отрицательные классы: [^;] включает newline. В /u точка потребляет одну code point, а не графему.

Точка, переводы строк и запрет разрывать UTF-8: путь данных
  1. A, затем LF, затем B.
  2. A.B не проходит LF без /s.
  3. A.B с /s покрывает три code points.
Многострочная заметкаСамостоятельный пример
<?php
declare(strict_types=1);

$note = "A\nB";
echo preg_match('~\AA.B\z~', $note), '/', preg_match('~\AA.B\z~s', $note), "\n";
echo preg_match('~\AA[^;]B\z~', $note), "\n";
Ожидаемый результат
0/1
1

[^;] означает любой знак кроме ;, поэтому newline допустим и без /s. Если newline запрещён, исключите его явно.

Одна логическая граница CRLFСамостоятельный пример
<?php
declare(strict_types=1);

$report = "BR41\r\nNT8\n";
$rows = preg_split('~\R~u', $report, -1, PREG_SPLIT_NO_EMPTY);
echo implode('|', $rows), "\n";
echo preg_match('~(*CRLF)^NT8$~m', $report), "\n";
Ожидаемый результат
BR41|NT8
0

\R потребляет CRLF целиком и также LF. Во второй проверке режим CRLF меняет newline-якоря: одинокий последний LF уже не считается разрешённым финальным newline.

В начале тела шаблона (*CR), (*LF), (*CRLF), (*ANYCRLF), (*ANY), (*NUL) задают, что считают newline якоря и точка; если несколько — последнее выигрывает. Это отдельно от \R: (*BSR_ANYCRLF) ограничивает его CR/LF/CRLF, (*BSR_UNICODE) допускает Unicode-переводы. \N всегда исключает newline, даже при /s. Старое описание предлагает \C для одного байта внутри UTF-8; это может разорвать кодовую точку. PHP8.5.11 при /u явно запрещает \C (compile warning). Для бинарных байтов используйте byte-mode осознанно, для текста . или \X.

Класс символов и POSIX-наборы

Поле SKU допускает только большие латинские буквы, цифры и дефис. [...] описывает один разрешённый знак; + после класса — несколько. Начальный ^ инвертирует набор, но всё равно потребляет один знак, поэтому [^x] не совпадает с пустой строкой. Диапазоны — диапазоны кодов, не алфавитная сортировка.

ЗаписьКак читать
[:alnum:] / [:alpha:]Буквы и цифры / буквы. В классе пишут [[:alpha:]].
[:ascii:] / [:cntrl:]ASCII-коды 0…127 / управляющие знаки.
[:blank:] / [:space:]Горизонтальный пробел / пробельные; UCP может расширять набор.
[:digit:] / [:xdigit:]Десятичные / шестнадцатеричные цифры; для строгого ASCII всё же явный [0-9A-Fa-f].
[:graph:] / [:print:]Печатные без пробелов / печатные с пробелами.
[:lower:] / [:upper:]Строчные / прописные; /i влияет на matching.
[:punct:] / [:word:]Пунктуация / word-набор, историческое Perl-расширение.
Класс символов и POSIX-наборы: путь данных
  1. [A-Z0-9-] разрешает B,R,4,1 и дефис.
  2. + проверяет каждый знак BR-41.
  3. \A/\z не позволяют оставить запрещённый хвост за совпадением.
Допустимый алфавит идентификатораСамостоятельный пример
<?php
declare(strict_types=1);

echo preg_match('~\A[A-Z0-9-]+\z~', 'BR-41'), "\n";
echo preg_match('~\A[A-Z0-9-]+\z~', 'BR_41'), "\n";
Ожидаемый результат
1
0

Дефис в конце класса буквальный. Подчёркивание не разрешено бизнес-правилом, хотя входит в \w.

ASCII hex и пробельный символ VTСамостоятельный пример
<?php
declare(strict_types=1);

echo preg_match('~\A[[:xdigit:]]{6}\z~', 'A0ff19'), "\n";
echo preg_match('~\s~', "\x0B"), '/', preg_match('~[[:space:]]~', "\x0B"), "\n";
echo preg_match('~\A[[:alpha:]]+\z~u', 'Гайка'), "\n";
Ожидаемый результат
1
1/1
1

Современная PCRE2 включает VT в \s; старое различие PHP-страницы между \s и [:space:] историческое. /u меняет многие POSIX-наборы на Unicode properties.

Буквальный ] ставят первым элементом либо экранируют; буквальный ^ — не первым либо \^; - — первым/последним либо \-. Диапазон Z-a захватывает пунктуацию между кодами: не заменяет [A-Za-z]. \d и \p{L} можно включать в набор, но свойство не может быть концом диапазона. [^\W_] отбирает word кроме underscore; Unicode-границы всё равно определяет /u. [:^digit:] инвертирует вложенный POSIX-набор, например [[:^digit:]]; голое [:digit:] — не правильная оболочка POSIX. /i учитывает регистр и в положительных/отрицательных классах; /m и /s не меняют принадлежность newline классу.

Альтернативы: порядок и область якорей

Доставка бывает pickup или courier. Вертикальная черта разделяет ветви, а (?:...) объединяет выбор, не создавая отдельного сохранённого поля. Движок пробует слева направо; выбирает первую ветвь, которая позволяет завершить весь шаблон. Пустая ветвь допустима и совпадает с пустым текстом.

Альтернативы: порядок и область якорей: путь данных
  1. (?:pickup|courier) предлагает два конкретных значения.
  2. \A и \z ограничивают весь выбор.
  3. courier совпадает, fast-courier не проходит.
Проверить один из двух текстовых кодовСамостоятельный пример
<?php
declare(strict_types=1);

$pattern = '~\A(?:pickup|courier)\z~';
echo preg_match($pattern, 'courier'), '/', preg_match($pattern, 'fast-courier'), "\n";
Ожидаемый результат
1/0

Для простого перечня in_array(..., true) или enum может быть понятнее; regex полезен, когда ветви имеют разную структуру.

Узнать, какая ветвь распознала документСамостоятельный пример
<?php
declare(strict_types=1);

$pattern = '~\A(?:INV-[0-9]+(*MARK:invoice)|RET-[0-9]+(*:return))\z~';
preg_match($pattern, 'RET-12', $m);
echo $m[0], ':', $m['MARK'], "\n";
preg_match('~BR|BR41~', 'BR41', $short);
echo $short[0], "\n";
Ожидаемый результат
RET-12:return
BR

(*MARK:name) и (*:name) записывают последнюю достигнутую метку успешного пути в MARK. Во втором поиске короткая левая ветвь уже достаточна: longest match здесь не гарантируется.

Если продолжение после группы не проходит, движок может вернуться к другой ветви. Метки не обязаны быть уникальными; выбирайте их для понятного отчёта, а не как замену бизнес-проверкам. Для общей части выносите её за группу: BR(?:41|42). Подробнее о запрете возврата — atomic groups.

Настройки внутри шаблона: локальный регистр

Префикс документа может быть нечувствителен к регистру, а секретная часть — чувствительна. (?i:...) включает i только внутри группы; после закрытия скобок прежняя настройка восстанавливается. (?i) без двоеточия меняет оставшуюся часть текущего уровня, а не только следующий символ.

Настройки внутри шаблона: локальный регистр: путь данных
  1. (?i:sku) принимает SKU или sku.
  2. После группы двоеточие и BR41 снова case-sensitive.
  3. SKU:BR41 проходит; SKU:br41 нет.
Локальная нечувствительность к региструСамостоятельный пример
<?php
declare(strict_types=1);

$pattern = '~\A(?i:sku):BR41\z~';
echo preg_match($pattern, 'SKU:BR41'), '/', preg_match($pattern, 'SKU:br41'), "\n";
Ожидаемый результат
1/0

Двоеточие внутри (?i:...) — часть синтаксиса группы; двоеточие после неё — данные формата.

Настройка действует при компиляции ветвейСамостоятельный пример
<?php
declare(strict_types=1);

echo preg_match('~\A(a(?i)b|c)\z~', 'C'), "\n";
echo preg_match('~\A(?i:tag)(?-i:Q)\z~', 'TAGq'), "\n";
Ожидаемый результат
1
0

i, включённая в первой ветви, влияет на последующие ветви того же уровня уже при компиляции. (?-i:Q) явно выключает i на свою группу.

Доступны i,m,s,x,U,J; n — no-auto-capture (PHP8.2+), r — caseless restrict (PHP8.4+ при подходящей PCRE). (?im-sx) включает im и выключает sx; яснее не повторять одну букву с обеих сторон. (?xx) в PCRE2 дополнительно игнорирует пробел/TAB внутри классов; обычный x этого не делает. (*UTF)/(*UCP) — отдельные глобальные стартовые настройки, не обычные буквы. Старый (?X) больше не поддерживается с PHP7.3; внешний X в PHP8.5.11 принимается как no-op. Все внешние модификаторы и версии.

Группы: сохранить поле, сгруппировать, назвать

Из BR41=12 нужно получить код и количество, а не только ответ да/нет. Обычные (...) сохраняют совпавший текст. Открывающие скобки нумеруются слева направо с 1, [0] всегда всё совпадение. (?:...) только объединяет выражение. (?<sku>...) одновременно даёт понятное имя и обычный номер.

Группы: сохранить поле, сгруппировать, назвать: путь данных
  1. Вход BR41=12.
  2. Группа sku захватывает BR41, qty захватывает 12.
  3. Можно прочитать $m["sku"] и $m["qty"], не запоминая номера.
Извлечь два поля по имениСамостоятельный пример
<?php
declare(strict_types=1);

preg_match('~\A(?<sku>[A-Z]{2}[0-9]+)=(?<qty>[0-9]+)\z~', 'BR41=12', $m);
echo $m['sku'], '/', $m['qty'], '/', $m[1], "\n";
Ожидаемый результат
BR41/12/BR41

Количество ещё строка. Здесь нет проверки существования кода на складе и числового диапазона.

Одинаковое поле в двух вариантах записиСамостоятельный пример
<?php
declare(strict_types=1);

$pattern = '~\A(?|SKU:([A-Z]{2}[0-9]+)|([A-Z]{2}[0-9]+):SKU)\z~';
foreach (['SKU:BR41', 'NT8:SKU'] as $line) {
    preg_match($pattern, $line, $m);
    echo $m[1], "\n";
}
Ожидаемый результат
BR41
NT8

(?|...) сбрасывает номера групп на начало каждой ветви. Без | сразу после ? поле второй ветви имело бы другой номер.

Одна семантика имени в альтернативных ветвяхСамостоятельный пример
<?php
declare(strict_types=1);

$pattern = '~\A(?:(?<sku>BR[0-9]+)|(?<sku>NT[0-9]+))\z~J';
preg_match($pattern, 'NT8', $m);
echo $m['sku'], "\n";
preg_match('~(BR)(?<number>[0-9]+)~n', 'BR41', $named);
echo $named[1], '/', $named['number'], "\n";
Ожидаемый результат
NT8
41/41

J разрешает дублирующиеся имена, а n делает обычные группы незахватывающими. Имя number всё равно имеет номер 1; n не выключает вообще все числовые результаты.

Другие формы именования — (?P<name>...) и (?'name'...). Для переносимости выбирайте ASCII-имя из букв/цифр/underscore, не начинайте с цифры. В PCRE2 10.44, bundled PHP8.4+, максимальная длина имени 128 вместо прежних 32; внешний PCRE требует проверки версии. Максимум захватывающих групп 65535 не гарантирует, что такой шаблон скомпилируется: есть отдельные ограничения размера и вложенности. Не смешивайте разные имена с одинаковым номером в branch-reset: PCRE2 это отвергает. После branch-reset следующая группа получает номер после максимума всех ветвей; имена — не отдельные «лишние» захваты. preg_match_all объясняет неоднозначность duplicate names при PREG_PATTERN_ORDER.

Повторения: диапазон, жадность и последние значения

Количество в заказе содержит от одной до трёх ASCII-цифр. Квантификатор относится к элементу непосредственно слева: символу, классу, точке, группе или back reference. {1,3} ограничивает длину, но не числовое значение: 000 и 999 одинаково подходят по форме.

ЗаписьКак читать
* / + / ?0…∞ / 1…∞ / 0…1.
{n} / {n,m} / {n,}Точно n / от n до m / не меньше n.
*? / +? / ?? / {n,m}?Ленивый порядок проб, сохраняя тот же диапазон количества.
*+ / ++ / ?+ / {n,m}+Possessive: найденное количество не отдаётся назад.
Повторения: диапазон, жадность и последние значения: путь данных
  1. [0-9] — одна цифра.
  2. {1,3} — одна, две либо три таких цифры.
  3. 12 проходит целиком; 1234 не проходит якоря.
Ограничить число цифрСамостоятельный пример
<?php
declare(strict_types=1);

$pattern = '~\A[0-9]{1,3}\z~';
echo preg_match($pattern, '12'), '/', preg_match($pattern, '1234'), "\n";
echo preg_match('~\A[0-9]{,3}\z~', '12'), "\n";
Ожидаемый результат
1/0
1

{,3} означает {0,3} с PCRE2 10.43+, в bundled PHP8.4+. Для PHP8.2+ переносимый вариант — {0,3}. При пустой строке второе правило также пройдёт.

Выделить каждую метку в квадратных скобкахСамостоятельный пример
<?php
declare(strict_types=1);

$text = '[BR41] [NT8]';
preg_match('~\[.*\]~', $text, $greedy);
preg_match('~\[.*?\]~', $text, $lazy);
preg_match_all('~\[[^\]]*\]~', $text, $bounded);
echo $greedy[0], "\n", $lazy[0], "\n", implode('|', $bounded[0]), "\n";
Ожидаемый результат
[BR41] [NT8]
[BR41]
[BR41]|[NT8]

Жадный * сначала берёт максимум; *? пробует минимум. Явный класс «до закрывающей скобки» лучше отражает этот простой формат, если вложенных скобок нет.

Повторённая группа хранит последнюю итерациюСамостоятельный пример
<?php
declare(strict_types=1);

preg_match('~\A([A-Z]{2}[0-9]+,?)+\z~', 'BR41,NT8', $m);
echo $m[1], "\n";
preg_match('~\A(a|(b))+\z~', 'aba', $nested);
echo $nested[1], '/', $nested[2], "\n";
Ожидаемый результат
NT8
a/b

Одна повторяемая группа не создаёт массив всех итераций. Во вложенной группе b может остаться захват от предыдущей итерации, даже если последняя захватила a. Для списка полей нужен preg_match_all.

Числа в квантификаторах меньше 65536, минимум не больше максимума. {n} — точно n; {n,} — от n; {0} исключает элемент из сопоставления, но не удаляет его определение группы. Неправильные фигурные записи могут стать литералами либо ошибкой по контексту; буквальные скобки экранируйте. PCRE2 10.43+ допускает пробел/TAB вокруг чисел и запятой в { 1 , 3 }; старые версии — нет. Под /U жадность меняется местами, а ? после квантификатора снова её переворачивает. *+ и ++ запрещают возврат: это отдельная семантика, а не универсальная оптимизация. Пустая повторяемая группа не зацикливает движок бесконечно: нулевой прогресс обрабатывается специально. Большой конечный repeat группы увеличивает compiled pattern, поэтому лимит количества не заменяет оценку памяти. Начальное .* с /s часто допускает implicit anchoring, но исключения (backrefs/atomic/control verbs) не позволяют полагаться на это как на правило валидации.

Back reference: повторить именно найденный текст

Метка товара записана дважды для контрольного подтверждения: BR41/BR41. Back reference сравнивает с конкретным ранее захваченным текстом. Повторное написание того же шаблона лишь проверило бы форму и допустило BR41/NT8.

Back reference: повторить именно найденный текст: путь данных
  1. Группа code сохраняет BR41.
  2. После / ссылка \k требует тот же текст.
  3. BR41/NT8 отклоняется, хотя оба кода по отдельности правильны.
Два одинаковых кодаСамостоятельный пример
<?php
declare(strict_types=1);

$pattern = '~\A(?<code>[A-Z]{2}[0-9]+)/\k<code>\z~';
echo preg_match($pattern, 'BR41/BR41'), '/', preg_match($pattern, 'BR41/NT8'), "\n";
Ожидаемый результат
1/0

Ссылка — на значение группы, не на её имя и не на PHP-переменную.

Ссылка относительно текущего местаСамостоятельный пример
<?php
declare(strict_types=1);

echo preg_match('~\A(BR)(41):\g{-1}:\g{1}2\z~', 'BR41:41:BR2'), "\n";
echo preg_match('~\A((?i)br)\s+\1\z~', 'BR br'), "\n";
Ожидаемый результат
1
0

\g{-1} — последняя группа, \g{1} — абсолютная первая; фигурные скобки отделяют следующую буквальную цифру 2. Регистр back reference определяется в месте ссылки, поэтому локальная i внутри захвата не делает внешнюю ссылку caseless.

Числовые формы \1, \g1, \g{1} эквивалентны при правильном номере; относительные -1/-2 упрощают перемещение фрагмента. Формы имени: \k<name>, \k'name', \k{name}, \g{name}, (?P=name). Важное уточнение старой PHP-страницы: \g<name> и \g'name' в PCRE2 — subroutine calls, а не ссылки на тот же захваченный текст; это разобрано в рекурсии. Не участвовавшая группа заставляет ссылку провалиться; пустая участвовавшая группа — другой случай. Внутренняя ссылка на текущую группу на первой итерации ещё не имеет значения, но может использовать предыдущее значение на повторе. Forward references бывают полезны только при корректной структуре повторов; не используйте их для обычного поля. Внутри [...] цифровой escape обозначает символ, не back reference. Не переносите syntax pattern \g{1} в replacement: там другой язык $1/${1}.

Lookahead и lookbehind: условие рядом с совпадением

Нужно извлечь число только перед pcs, не включая единицу в результат. (?=...) проверяет текст впереди и не двигает конечную позицию совпадения. (?!...) требует отсутствия впереди; (?<=...) и (?<!...) проверяют позади. Это условия на одну позицию, не дополнительные части вывода.

Lookahead и lookbehind: условие рядом с совпадением: путь данных
  1. В строке "12 pcs, 5 kg" число 12 имеет подходящее продолжение.
  2. (?= pcs) подтверждает единицу без её потребления.
  3. Результат — только "12".
Число перед заданной единицейСамостоятельный пример
<?php
declare(strict_types=1);

preg_match_all('~[0-9]+(?= pcs)~', '12 pcs, 5 kg', $m);
echo implode(',', $m[0]), "\n";
echo preg_match('~\ABR(?!00)[0-9]{2}\z~', 'BR00'), "\n";
Ожидаемый результат
12
0

(?!00) запрещает конкретный суффикс сразу после BR. Если нужен диапазон числа, после извлечения обычно понятнее числовое сравнение.

Не удалять нужный контекст перед числомСамостоятельный пример
<?php
declare(strict_types=1);

preg_match('~(?<=qty:)[0-9]+~', 'qty:12', $m);
echo $m[0], "\n";
echo preg_match('~(?<!draft:)BR[0-9]+~', 'draft:BR41'), "\n";
preg_match('~(?<=qty: ?)[0-9]+~', 'qty: 12', $variable);
echo $variable[0], "\n";
Ожидаемый результат
12
0
12

Первый lookbehind фиксирован. Последний допускает 0–1 пробел: ограниченный variable lookbehind поддержан PCRE2 10.43+ (bundled PHP8.4+). Он не переносим на bundled PHP8.2/8.3.

Две проверки относятся к одной позицииСамостоятельный пример
<?php
declare(strict_types=1);

$pattern = '~(?<=ID:)(?!000)[0-9]{3}\b~';
echo preg_match($pattern, 'ID:042'), '/', preg_match($pattern, 'ID:000'), "\n";
Ожидаемый результат
1/0

Lookbehind подтверждает префикс слева, lookahead исключает три нуля справа, затем обычный класс потребляет три цифры. Проверки не «идут» последовательно по входу.

Фиксированные ветви lookbehind могут иметь разную длину: (?<=qty:|count:) работало и раньше. С PCRE2 10.43 variable branches имеют конечный вычислимый максимум, по умолчанию 255 символов; a* по-прежнему не подходит. Фиксированная ветвь имеет отдельный предел 65535; это не одна общая настройка. \X/\R в lookbehind не допускаются из-за переменной длины. Lookarounds можно вкладывать; позитивный захват внутри сохраняется, негативный обычно не даёт успешного захвата (особый случай — false-ветвь conditional). Сами assertion-группы не захватывают. Историческое «assertions нельзя повторять» слишком категорично: PCRE2 разрешает квантификатор lookaround, но (?!x){3} проверяет одну позицию, не следующие три символа. Привязки вроде \b повторять нельзя. Историческое число 200 вложенных групп не универсально: современный compile nesting limit обычно 250 и зависит от сборки, отдельно от match depth.

Atomic и possessive: когда нельзя отдавать уже взятое

При чтении суммы цифры должны закончиться перед пробелом и единицей. Возвращать последнюю цифру из [0-9]+ ради поиска обязательного пробела бессмысленно: цифра пробелом не станет. Atomic-группа (?>...) фиксирует выбранный вариант после выхода из неё; possessive [0-9]++ фиксирует повторение.

Atomic и possessive: когда нельзя отдавать уже взятое: путь данных
  1. 120 kg → повтор цифр забирает 120.
  2. Следующий символ — требуемый пробел.
  3. Текст проходит; повтор не должен отдавать цифры назад.
Не возвращать цифры перед фиксированным разделителемСамостоятельный пример
<?php
declare(strict_types=1);

echo preg_match('~\A[0-9]++ kg\z~', '120 kg'), "\n";
echo preg_match('~\A(?>[0-9]+) kg\z~', '120 kg'), "\n";
Ожидаемый результат
1
1

Для данного языка эти формы равноценны. Это не доказательство измеренного ускорения; реальный профиль зависит от оптимизаций движка.

Possessive может изменить допустимые данныеСамостоятельный пример
<?php
declare(strict_types=1);

echo preg_match('~\A[0-9]+[0-9]\z~', '120'), '/', preg_match('~\A[0-9]++[0-9]\z~', '120'), "\n";
echo preg_match('~\A(?>BR|BR41)\z~', 'BR41'), "\n";
Ожидаемый результат
1/0
0

В первой паре последняя цифра нужна следующему элементу, поэтому запрет возврата ломает совпадение. Во втором atomic зафиксировал короткую BR и уже не пробует BR41.

Atomic не захватывает сама, но внутренние (...) могут. Возврат за всю группу к более ранним элементам остаётся возможным; запрещён возврат внутрь уже завершённой группы. Группы можно вкладывать, использовать с lazy и lookbehind. Для проверки буквального суффикса str_ends_with проще, чем ^(?>.*)(?<=...). Старое утверждение «atomic — единственный способ устранить nested-repeat» неверно как рекомендация: чаще лучше убрать неоднозначность грамматики, ограничить вход или использовать парсер. См. ограниченный пример исчерпания лимита.

Условные группы: закрывающая скобка нужна только после открывающей

Один и тот же код разрешён как BR41 или [BR41], но одиночная скобка ошибочна. Conditional проверяет, участвовала ли группа, либо выполняет assertion, и выбирает yes/no ветвь. Это syntax regex, а не PHP if.

Условные группы: закрывающая скобка нужна только после открывающей: путь данных
  1. (\[)? сохраняет открывающую скобку в группу 1, если она есть.
  2. Основная часть читает BR41.
  3. (?(1)\]) требует закрыть скобку только при существовании первой.
Парные необязательные скобкиСамостоятельный пример
<?php
declare(strict_types=1);

$pattern = '~\A(\[)?[A-Z]{2}[0-9]+(?(1)\])\z~';
foreach (['BR41', '[BR41]', '[BR41', 'BR41]'] as $code) {
    echo preg_match($pattern, $code), "\n";
}
Ожидаемый результат
1
1
0
0

Без альтернативной no-ветви conditional не потребляет ничего. Якоря исключают лишнюю закрывающую скобку.

Формат выбирается по признаку впередиСамостоятельный пример
<?php
declare(strict_types=1);

$pattern = '~\A(?(?=INV-)INV-[0-9]{3}|RET-[0-9]{2})\z~';
echo preg_match($pattern, 'INV-104'), '/', preg_match($pattern, 'RET-12'), '/', preg_match($pattern, 'RET-104'), "\n";
Ожидаемый результат
1/1/0

(?=INV-) проверяется без потребления. Выбранная ветвь затем должна сама прочитать и префикс, и нужное число цифр.

Формы: (?(condition)yes) и (?(condition)yes|no); третья ветвь — compile error. Условие может ссылаться на номер или имя группы: (?(<open>)...), использовать положительный/отрицательный lookahead/lookbehind. (?(R)...) означает рекурсивный контекст, на верхнем уровне false; PCRE2 допускает уточнение рекурсивной группы. (?(DEFINE)...) хранит подпрограммы, не исполняя их на этом месте. Для двух простых форм обычная альтернатива нередко понятнее; conditional полезен для связи между ранее прочитанным и продолжением.

Комментарии: сделать проверяемое правило читаемым

Длинное правило нужно объяснять коллегам. Модификатор x позволяет расположить части на отдельных строках и подписать их # комментариями. PHP nowdoc сохраняет многострочный текст без подстановок; после закрывающей метки нужен ;. Комментарий объясняет причину ограничения, а не только повторяет символы.

Комментарии: сделать проверяемое правило читаемым: путь данных
  1. Правило разделено на начало, код, количество и конец.
  2. x исключает пробелы оформления и #комментарии вне классов.
  3. BR41=12 по-прежнему разбирается на поля.
Правило с короткими объяснениямиСамостоятельный пример
<?php
declare(strict_types=1);

$pattern = <<<'RX'
~\A
 (?<sku>[A-Z]{2}[0-9]+)  # код из выгрузки
 =
 (?<qty>[0-9]{1,3})     # в сообщении максимум три цифры
\z~x
RX;
preg_match($pattern, 'BR41=12', $m);
echo $m['sku'], '/', $m['qty'], "\n";
Ожидаемый результат
BR41/12

Пробел после кода в исходном шаблоне служит оформлению и не обязателен во входе. Это отличается от режима без x.

Буквальный # и пробел при xСамостоятельный пример
<?php
declare(strict_types=1);

echo preg_match('~\A\#[ ]BR41\z~x', '# BR41'), "\n";
echo preg_match('~\ABR(?# warehouse code)41\z~', 'BR41'), "\n";
Ожидаемый результат
1
1

\# и [ ] оставляют буквальные символы в x; (?#...) работает и без x. Внутренний комментарий заканчивается ближайшей ), вложенные скобки в нём не используйте.

Комментарий # продолжается до настоящего перевода строки в шаблоне; два текстовых символа \n в nowdoc не завершают его. При обычном x пробел внутри [...] сохраняется; (?xx) в PCRE2 меняет это правило. Динамический текст с # экранируйте preg_quote с актуальным разделителем. Не вставляйте сам разделитель в комментарий: PHP ищет конец шаблона до PCRE-разбора.

Рекурсивное правило и повторное использование подшаблона

Учебная метка группы записывается в круглых скобках и может содержать вложенную группу. Простое [^()]* допускает только один уровень. (?&group) вызывает именованный подшаблон снова; когда этот вызов находится внутри его определения, возникает рекурсия. Вход в примере короткий и заранее ограничен. Для JSON, HTML или языка программирования нужен настоящий парсер, а не расширение этого упражнения.

Рекурсивное правило и повторное использование подшаблона: путь данных
  1. Начальная ( открывает группу.
  2. Обычный текст читается целиком; при новой ( вызывается то же правило group.
  3. Каждый вызов обязан дойти до своей ); затем управление возвращается наружу.
Проверить небольшую вложенную меткуСамостоятельный пример
<?php
declare(strict_types=1);

$pattern = '~\A(?<group>\((?:[^()]++|(?&group))*\))\z~';
foreach (['(bolt(nut)washer)', '(bolt(nut)'] as $text) {
    echo preg_match($pattern, $text), "\n";
}
Ожидаемый результат
1
0

[^()]++ читает обычный текст без бессмысленного дробления между повторениями. Именованное определение group целиком занимает строку; рекурсивный вызов не повторяет внешние якоря.

Повторить форму кода, а не его значениеСамостоятельный пример
<?php
declare(strict_types=1);

$sameText = '~\A(?<code>[A-Z]{2}[0-9]+)/\k<code>\z~';
$sameRule = '~\A(?<code>[A-Z]{2}[0-9]+)/(?&code)\z~';
echo preg_match($sameText, 'BR41/NT8'), '/', preg_match($sameRule, 'BR41/NT8'), "\n";
Ожидаемый результат
0/1

Back reference требует BR41 снова; subroutine заново выполняет грамматику и принимает NT8. Это различие нельзя скрывать за одним словом «ссылка».

Вынести повторяемую грамматику в DEFINEСамостоятельный пример
<?php
declare(strict_types=1);

$pattern = '~\A(?&code)->(?&code)\z(?(DEFINE)(?<code>[A-Z]{2}[0-9]+))~';
echo preg_match($pattern, 'BR41->NT8'), "\n";
echo preg_match($pattern, 'BR41->?'), "\n";
Ожидаемый результат
1
0

DEFINE само не потребляет текст; (?&code) вызывает правило по имени. Это легальная ссылка на определение, написанное ниже, в отличие от старого ограничения PHP-страницы.

Другие вызовы: (?R) и (?0) — весь шаблон; (?1), (?2) — группа по номеру; (?P>name), \g<name>, \g'name' — подпрограмма по имени. Возможны относительные номера (?−1) в смысле записи (? -1) без пробела и с ASCII-минусом: фактически (?-1). Не перепутайте с back reference \g{-1}. Рекурсировать можно только с продвижением или достижимой остановкой; цикл на том же вводе способен закончиться PREG_INTERNAL_ERROR. Captures не образуют дерево всех вызовов: после возврата видны соответствующие внешние значения, а не история вложенности. С PCRE2 10.30 вызов подпрограммы больше не автоматически atomic; фиксируйте это явно, если нужно. Исторические PHP.net пояснения про 15 captures, pcre_malloc и C-stack не описывают современную архитектуру PCRE2. Отдельные лимиты match/depth/heap/JIT и compile nesting сохраняются; «неограниченная вложенность» означает форму грамматики, а не бесконечные ресурсы.

Предсказуемый отказ: лимит и однозначная грамматика

Техническая ошибка regex не должна подвесить обработку заявки. Неоднозначные вложенные повторения заставляют движок пробовать множество разбиений, особенно когда последний знак не подходит. Лимит backtrack ограничивает работу matcher, но не обещает время в миллисекундах. Начните с лимита размера входа и ясного алфавита; не принимайте пользовательский текст за готовый regex.

Предсказуемый отказ: лимит и однозначная грамматика: путь данных
  1. 20 букв A и завершающий !.
  2. Неудачное вложенное ([A-Z]+)+ пытается переразбить тот же префикс.
  3. Малый локальный лимит даёт контролируемый отказ; [A-Z]+ без вложения штатно возвращает 0.
Безопасно увидеть исчерпание небольшого лимитаСамостоятельный пример
<?php
declare(strict_types=1);

$old = ini_set('pcre.backtrack_limit', '100');
if ($old === false) { throw new RuntimeException('Cannot set test limit'); }
try {
    $text = str_repeat('A', 20) . '!';
    $result = preg_match('~(*NO_JIT)(*NO_AUTO_POSSESS)(*NO_START_OPT)\A([A-Z]+)+\z~', $text);
    echo $result === false && preg_last_error() === PREG_BACKTRACK_LIMIT_ERROR ? 'Лимит' : 'Иной результат';
    echo "\n", preg_match('~\A[A-Z]+\z~', $text), "\n";
} finally { ini_set('pcre.backtrack_limit', $old); }
Ожидаемый результат
Лимит
0

NO_JIT/NO_AUTO_POSSESS/NO_START_OPT здесь только делают учебный отказ наблюдаемым на малом вводе; это не настройки для реального сервиса. finally восстанавливает прежний лимит даже при исключении.

Ограничить вход до запуска regexСамостоятельный пример
<?php
declare(strict_types=1);

function isImportCode(string $input): bool
{
    if (strlen($input) > 16) { return false; }
    $result = preg_match('~\A[A-Z]{2}-[0-9]{1,10}\z~', $input);
    if ($result === false) { throw new RuntimeException(preg_last_error_msg()); }
    return $result === 1;
}
echo isImportCode('BR-104') ? 'Принят' : 'Отклонён';
echo "\n", isImportCode(str_repeat('A', 30)) ? 'Принят' : 'Отклонён', "\n";
Ожидаемый результат
Принят
Отклонён

strlen измеряет байты; для ASCII-договора это именно требуемая единица. Ограничение установлено до regex, а не после дорогого извлечения.

Класс [AEIOU] обычно проще, чем пять однобуквенных альтернатив, но измеряйте свой сценарий, а не объявляйте абсолютную скорость. Якорь убирает бессмысленные стартовые позиции; .*/s может позволять implicit anchoring лишь при определённых условиях. Обязательный буквальный суффикс иногда даёт ранний отказ через оптимизацию — это не устраняет проблему всех похожих шаблонов. Тестируйте успех, длинный провал, повреждённую кодировку и границы; не разворачивайте их до гигабайтов ради демонстрации. JIT ускоряет часть правил, но не исправляет неоднозначную грамматику и имеет свой стек. Для справочника тысяч буквальных значений используйте множество/индекс, а не огромную альтернативу. compiled-pattern limit не лечится повышением backtrack_limit.

Границы совместимости с Perl и оболочкой PHP

Имя Perl-compatible означает большое пересечение правил, а не идентичность всех версий. Официальная страница differences направляет к upstream. Здесь ориентир — tagged PCRE2 10.44 и его описание сравнений с Perl5.38; сам Perl в этой среде не запускался. Ни одна строка таблицы не выдаётся за испытание другого языка. Когда переносите правило из Perl/JavaScript/IDE, проверяйте и движок, и оболочку строк/разделителей/замены.

Различие / группа правилСледствие для переноса
Newline и точкаPCRE2 выбирает CR/LF/CRLF/NUL и др.; Perl иное правило LF. Не переносите /s или newline-политику без тестов.
Unicode-подмножествоНе все Perl properties/синтаксисы поддержаны: категория/скрипт/часть binary и Bidi, без произвольного Is/In... и длинного Letter.
Повтор assertionLookaround{3} проверяет одну позицию; \b* в PCRE2 нельзя.
Квантификатор без элементаКорректный {1,2} в начале ветви — compile error, а не гарантированный литерал как в некоторых Perl-контекстах.
Captures негативного lookaroundОбычно не устанавливаются; особое условие negative assertion с false-ветвью может сохранить capture. Не переносите Perl-ожидания.
\F \l \L \u \U и именованный \N{...}Perl-обработка строки не является PHP/PCRE-шаблоном; эти формы не поддерживаются в таком значении. \N и \N{U+...} имеют собственные допустимые значения.
\Q...\EPCRE не имеет Perl-переменных $/@ и не интерполирует их; PHP-строка уже была обработана отдельно.
(?{code}) / (??{code})Исполнение Perl-кода из шаблона не поддерживается. PCRE callout — C API, PHP preg API не предоставляет регистрацию PHP-callback для него.
РекурсияС10.30 subroutine не автоматически atomic; старые PCRE1-памятки про обязательную атомарность непригодны.
Control verbs внутри subroutineЭффект ограничен группой; особенно (*THEN) не обязан действовать как у Perl на окружающую альтернативу.
Несколько control verbsСрабатывает первый достигнутый при возврате: поведение сложных комбинаций может отличаться от Perl.
Захваты повторовВложенный capture может сохранить прежнюю итерацию; Perl мог очистить его.
Branch-reset и именаОдин номер с разными именами запрещён; у Perl возможности шире.
Комментарии и пробелыНельзя произвольно вставлять пробел между ( и ? даже под x; версии Perl тоже меняли правила.
Диапазон с классом[a-[:digit:]] — ошибка PCRE2, а не warning плюс буквальный дефис.
Lu/Ll с /iВ PCRE2 свойства сохраняют смысл upper/lower; Perl5.38 отличается.
\K в lookaroundС10.38 запрещён по умолчанию; не переносите старое исключение без явного API-контроля.
PCRE-расширенияD, U, anchored offset, ограничения \R, стартовые (*CR)/(*NO_JIT), относительные номера, non-atomic positive lookarounds — не универсальный regex-язык.
C API, не PHP preg APINOTBOL/NOTEOL/NOTEMPTY, partial matching, callouts и альтернативный DFA matcher не являются флагами preg_match. (*NOTEMPTY)/(*NOTEMPTY_ATSTART) можно задать в самом шаблоне по PCRE2-договору.
Пределы и памятьCompile-size/depth/capture/heap/JIT зависят от PCRE2 и оболочки. Interpreter с10.30 не использует прежнюю C-recursion для каждого match-frame.
Perl /d /a /uНе тот же набор модификаторов. PHP /u задаёт UTF+UCP; не копируйте Perl charset flags.
Рекурсивные пустые ветвиНекоторые формы, объявляемые Perl бесконечной рекурсией, PCRE2 способен обработать. Для приложения не полагайтесь на эти пограничные различия; обеспечьте продвижение/ограничения.
Проверить смысл ссылки до переноса
  1. Два кода BR41/NT8 имеют одинаковую форму.
  2. \g{code} требует ранее захваченное значение →0.
  3. \g в PCRE2 вызывает грамматику группы заново →1.
Похожие скобки означают разные операцииСамостоятельный пример
<?php
declare(strict_types=1);

$same = '~\A(?<code>[A-Z]{2}[0-9]+)/\g{code}\z~';
$again = '~\A(?<code>[A-Z]{2}[0-9]+)/\g<code>\z~';
echo preg_match($same, 'BR41/NT8'), '/', preg_match($again, 'BR41/NT8'), "\n";
Ожидаемый результат
0/1

Этот запуск проверяет только PHP/PCRE2, не Perl. Старый список back references PHP.net включает формы вызова подпрограммы; здесь различие показано явно и сверено с upstream10.44.

Не все расширения PCRE2 доступны через PHP. Например библиотека умеет DFA matching и callback-callouts, но preg_* предоставляет свой фиксированный API. Версия pattern-language и PHP-оболочки входят в ваш контракт вместе. Проверка версии и обработка отказа нужны до интеграции стороннего правила.

Практика: небольшая строка складского импорта

Практика объединяет прежние шаги: отберите записи вида BR41=12, сохраните код и количество, а неподходящий ввод верните пользователю без потери исходного текста. Предварительно ограничьте строку 32байтами. Договор ASCII; количество от1 до999. Здесь нет записи в базу, HTTP или сообщения внешнему сервису.

Учебная строка → данные или понятная причина
  1. Проверить byte-length до regex.
  2. Извлечь sku/qty только из полной строки.
  3. Преобразовать qty в int и проверить диапазон.
  4. Вернуть пригодный результат либо причину; техническую ошибку не маскировать.

Сначала попробуйте сами. В решении функция возвращает массив с полем ok; для успеха добавляет sku/qty, для штатного отказа — reason. Это не обязательная архитектура приложения, а способ явно увидеть разные исходы. Тип array не описывает саму форму такого массива — её договор показан здесь текстом.

Решение: одна строка импорта с разными исходамиСамостоятельный пример
<?php
declare(strict_types=1);

function parseStockLine(string $line): array
{
    if (strlen($line) > 32) { return ['ok' => false, 'reason' => 'слишком длинно']; }
    $status = preg_match('~\A(?<sku>[A-Z]{2}[0-9]{1,4})=(?<qty>[0-9]{1,3})\z~', $line, $m);
    if ($status === false) { throw new RuntimeException(preg_last_error_msg()); }
    if ($status === 0) { return ['ok' => false, 'reason' => 'нужна запись SKU=количество']; }
    $quantity = (int) $m['qty'];
    if ($quantity < 1) { return ['ok' => false, 'reason' => 'количество от 1']; }
    return ['ok' => true, 'sku' => $m['sku'], 'qty' => $quantity];
}
foreach (['BR41=12', 'NT8=0', 'BR41:12'] as $line) {
    $record = parseStockLine($line);
    echo $record['ok'] ? $record['sku'] . ' × ' . $record['qty'] : $record['reason'];
    echo "\n";
}
Ожидаемый результат
BR41 × 12
количество от 1
нужна запись SKU=количество

Regex проверяет структуру, затем обычное число проверяет диапазон. Короткая форма return останавливает функцию на конкретной причине. Ведущие нули qty в этом договоре разрешены и исчезают при преобразовании; SKU остаётся строкой.

Следующее изменение требования: количество может быть пустым и тогда требует уточнения. Сначала решите, действительно ли пустое значение допустимо; затем сделайте группу optional с PREG_UNMATCHED_AS_NULL и различайте null/"0". Не заменяйте это проверкой empty, которая смешает разные случаи. Если данные придут в JSON или CSV, начните с их парсера, а regex примените только к отдельному полю. Дальнейшие исходники полезных действий находятся в preg_match, preg_replace_callback и ограничении нагрузки.

Функции и методы этой темы

Откройте карточку, чтобы увидеть договор функции, примеры, ограничения и связанные инструменты.

preg_match_allСобрать все коды товаров из заметки, сохранив порядок появления.

PHP 4+ · pcre

preg_match_all(string $pattern, string $subject, array &$matches = null, int $flags = 0, int $offset = 0): int|false

[A-Z]{2} означает две латинские заглавные буквы, [0-9]+ — номер. $all[0] — список целых совпадений при обычном порядке. foreach поочерёдно читает этот список; echo печатает каждый код.

Аргумент / результатДоговор
pattern / subjectОдин шаблон и одна исходная строка. Поиск продолжается после предыдущего совпадения.
&matchesРезультат всех совпадений; формат зависит от flags. Именованные группы дублируются под именем и номером.
flagsPREG_PATTERN_ORDER по умолчанию: массив по группам; PREG_SET_ORDER: массив по совпадениям. Выбирайте один порядок. Добавляются | PREG_OFFSET_CAPTURE и/или PREG_UNMATCHED_AS_NULL.
offsetБайтовый старт, в том числе отрицательный от конца; контекст не обрезается.
РезультатКоличество полных совпадений, 0 или false при отказе. Не количество групп.
ОшибкиНекомпилируемый шаблон выдаёт E_WARNING; ошибка выполнения видна в preg_last_error сразу после вызова. Неверные типы/значения аргументов могут вызвать TypeError/ValueError. Не смешивайте технический отказ с отсутствием совпадения.
ВерсияPHP 4+; встроенное расширение pcre
Данные первого примера
  1. Записка "BR41, NT8, BR41".
  2. Три неперекрывающихся совпадения в исходном порядке.
  3. $all[0] → BR41 / NT8 / BR41; число 3.
Список упомянутых товаровСамостоятельный пример
<?php
declare(strict_types=1);

$count = preg_match_all('~[A-Z]{2}[0-9]+~', 'BR41, NT8, BR41', $all);
echo 'Найдено: ', $count, "\n";
foreach ($all[0] as $code) { echo $code, "\n"; }
Ожидаемый результат
Найдено: 3
BR41
NT8
BR41

Повторы сохраняются. Это извлечение из заметки, а не проверка формата всей заметки.

Каждому товару — строка результатаСамостоятельный пример
<?php
declare(strict_types=1);

$count = preg_match_all('~(?<sku>[A-Z]{2}[0-9]+)(?:=(?<qty>[0-9]+))?~',
    'BR41=12 NT8', $rows, PREG_SET_ORDER | PREG_UNMATCHED_AS_NULL);
if ($count === false) { throw new RuntimeException(preg_last_error_msg()); }
foreach ($rows as $row) {
    echo $row['sku'], ':', $row['qty'] ?? 'уточнить', "\n";
}
Ожидаемый результат
BR41:12
NT8:уточнить

В PREG_SET_ORDER сначала выбираем совпадение, затем его поля. Число 12 остаётся строкой; допустимое количество и существование SKU проверяет бизнес-логика.

Перекрывающиеся пары через lookaheadСамостоятельный пример
<?php
declare(strict_types=1);

preg_match_all('~(?=([0-9]{2}))~', '1234', $m, PREG_OFFSET_CAPTURE);
foreach ($m[1] as [$pair, $byte]) { echo $pair, '@', $byte, "\n"; }
Ожидаемый результат
12@0
23@1
34@2

Проверка (?=...) не потребляет текст, но внутренняя группа сохраняет пару. Движок сам продвигает поиск после пустого совпадения. Обычный шаблон [0-9]{2} нашёл бы только 12 и 34.

Глобальный поиск хранит весь результат в памяти: для большого файла обрабатывайте логические записи, если шаблон не переходит через их границы. Совпадения обычно не перекрываются. При duplicate named groups и PREG_PATTERN_ORDER именованный массив соответствует последней группе с этим именем, поэтому прежнее значение может оказаться пустым; для ясного контракта предпочитайте уникальные имена.

Попробуйте. Уберите =12 из заметки: обе строки получают «уточнить», но count остаётся 2.

Дальше: preg_match, preg_split, preg_last_error. Как устроен шаблон и вызов; ошибка или нет совпадения.

preg_grepОтобрать допустимые коды из списка и сохранить связь с исходными ключами.

PHP 4+ · pcre

preg_grep(string $pattern, array $array, int $flags = 0): array|false

Запись 14 => "BR41" связывает ключ строки импорта 14 с её кодом. preg_grep оставляет эту связь. foreach ($valid as $line => $code) читает ключ и значение, не перенумеровывая строки.

Аргумент / результатДоговор
patternОдно правило для значений массива, не для ключей.
arrayМассив проверяемых значений; в учебном договоре только строки. Вложенные структуры предварительно разбираются отдельно.
flags = 0PREG_GREP_INVERT возвращает значения, не соответствующие шаблону.
РезультатМассив с исходными ключами; [] если ничего не отобрано; false при отказе.
ОшибкиНекомпилируемый шаблон выдаёт E_WARNING; ошибка выполнения видна в preg_last_error сразу после вызова. Неверные типы/значения аргументов могут вызвать TypeError/ValueError. Не смешивайте технический отказ с отсутствием совпадения.
ВерсияPHP 4+; встроенное расширение pcre
Данные первого примера
  1. Строка 14: BR41; строка 20: ???.
  2. Правило две буквы + цифры принимает только BR41.
  3. Результат сохраняет ключ 14 → его можно показать в отчёте.
Оставить корректные коды импортаСамостоятельный пример
<?php
declare(strict_types=1);

$valid = preg_grep('~\A[A-Z]{2}[0-9]+\z~', [14 => 'BR41', 20 => '???']);
foreach ($valid as $line => $code) { echo $line, ':', $code, "\n"; }
Ожидаемый результат
14:BR41

Ключи — номера записей, а не byte-offset. Функция не преобразует значения.

Получить только строки для исправленияСамостоятельный пример
<?php
declare(strict_types=1);

function invalidCodes(array $codes): array
{
    $bad = preg_grep('~\A[A-Z]{2}[0-9]+\z~', $codes, PREG_GREP_INVERT);
    if ($bad === false) { throw new RuntimeException(preg_last_error_msg()); }
    return $bad;
}
foreach (invalidCodes(['a' => 'BR41', 'b' => 'BR?', 'c' => 'nt8']) as $key => $code) {
    echo $key, ':', $code, "\n";
}
Ожидаемый результат
b:BR?
c:nt8

Правило сознательно требует заглавные ASCII-буквы; функция не «исправляет» их автоматически. Реальная проверка массива должна отдельно гарантировать строковые элементы.

preg_grep не возвращает захваченные группы. Для сложной политики по ключу/значению используйте foreach/array_filter с callback, для извлечения полей — preg_match. PHP может приводить скалярные элементы к строке во время проверки, но в результате остаются исходные значения; не используйте это как проверку типов.

Попробуйте. Передайте список только правильных кодов: invalidCodes вернёт [], а не false.

Дальше: preg_match, preg_filter. Как устроен шаблон и вызов; ошибка или нет совпадения.

preg_quoteВставить название товара в шаблон как буквальный текст, даже если в нём есть точки, скобки и +.

PHP 4+ · pcre

preg_quote(string $str, ?string $delimiter = null): string

Вызов preg_quote($name, "~") создаёт безопасную буквальную часть; оператор . соединяет её с ~ и якорями. Экранируйте данные, а не весь готовый шаблон, иначе якоря потеряют смысл.

Аргумент / результатДоговор
strБуквальная строка, которую требуется найти.
delimiter = nullОдин фактически выбранный разделитель шаблона, например ~. Он тоже экранируется. null не добавляет специальный разделитель.
РезультатСтрока с обратными слешами перед метасимволами. Исходная переменная не меняется; функция binary-safe.
ОшибкиТиповые ошибки аргументов — TypeError. Это не проверка корректности UTF-8 и не экранирование HTML/SQL/replacement.
ВерсияPHP 4+; встроенное расширение pcre
Данные первого примера
  1. Название "M10+ (2)" содержит + и скобки.
  2. preg_quote превращает их в буквальные символы правила.
  3. Совпадает точное название, а "M100 2" не подходит.
Название со специальными символамиСамостоятельный пример
<?php
declare(strict_types=1);

$name = 'M10+ (2)';
$pattern = '~\A' . preg_quote($name, '~') . '\z~';
echo preg_match($pattern, 'M10+ (2)'), '/', preg_match($pattern, 'M100 2'), "\n";
Ожидаемый результат
1/0

Неэкранированный + означал бы повторение нуля, а скобки — группу. Здесь они данные.

Искать товар с допустимым числом штукСамостоятельный пример
<?php
declare(strict_types=1);

function quantityFor(string $name, string $line): ?string
{
    $pattern = '~\A' . preg_quote($name, '~') . ': ([0-9]+) pcs\z~u';
    $status = preg_match($pattern, $line, $m);
    if ($status === false) { throw new RuntimeException(preg_last_error_msg()); }
    return $status === 1 ? $m[1] : null;
}
echo quantityFor('BR~41', 'BR~41: 12 pcs'), "\n";
Ожидаемый результат
12

Разделитель ~ из названия экранируется вторым аргументом. Неизменяемая часть шаблона задаёт формат строки, данные задают только имя.

Полный набор обрабатываемых знаков: . \ + * ? [ ^ ] $ ( ) { } = ! < > | : - #. Символ / сам по себе не regex-метасимвол, поэтому для /-разделителя нужен второй аргумент. # экранируется с PHP7.3, delimiter допускает null с 7.2. Для поиска без окружающей грамматики достаточно str_contains; preg_quote не нужен. Для буквального динамического replacement используйте callback.

Попробуйте. Добавьте в название / и #: точное сравнение по-прежнему должно проходить без unknown modifier.

Дальше: preg_match, preg_replace_callback. Как устроен шаблон и вызов; ошибка или нет совпадения.

preg_filterОбновить только подходящие строки каталога и получить список затронутых записей.

PHP 5.3+ · pcre

preg_filter(string|array $pattern, string|array $replacement, string|array $subject, int $limit = -1, int &$count = null): string|array|null

В ~\Aold/~ префикс old/ разрешён только в начале строки. Замена new/ переносит подходящие пути в новый раздел. Обычный preg_replace сохранил бы и остальные записи; preg_filter исключает их.

Аргумент / результатДоговор
patternОдна строка-шаблон с разделителями либо массив шаблонов.
replacementСтрока либо массив замен, по порядку элементов pattern, не по совпадающим ключам. Если replacement — массив, pattern тоже должен быть массивом; иначе TypeError. Одна строка обслуживает все шаблоны; недостающие элементы массива означают пустую замену. $0 — всё совпадение, $1…$99 / \1…\99 — числовые группы; ${1}2 отделяет группу 1 от цифры 2.
subjectСтрока либо массив строк. Ключи массива сохраняются; правила применяются к каждому значению.
limit = -1Максимум замен на каждый шаблон в каждой строке. -1 — все, 0 — ни одной.
&count = nullНеобязательная переменная: общее число сделанных замен. & в сигнатуре означает запись результата в переданную переменную.
РезультатКак preg_replace, но возвращает лишь строки, где произведена замена. Массив сохраняет ключи; без замен [] для массива, null для скалярной строки. Поэтому null здесь может означать нормальное отсутствие результата.
ОшибкиНекомпилируемый шаблон выдаёт E_WARNING; ошибка выполнения видна в preg_last_error сразу после вызова. Неверные типы/значения аргументов могут вызвать TypeError/ValueError. Не смешивайте технический отказ с отсутствием совпадения.
ВерсияPHP 5.3+; встроенное расширение pcre
Данные первого примера
  1. a: old/bolt, b: keep/nut.
  2. В a заменяется начальный old/ → new/.
  3. Результат содержит только a: new/bolt.
Список изменённых адресовСамостоятельный пример
<?php
declare(strict_types=1);

$changed = preg_filter('~\Aold/~', 'new/', ['a' => 'old/bolt', 'b' => 'keep/nut']);
foreach ($changed as $key => $path) { echo $key, ':', $path, "\n"; }
Ожидаемый результат
a:new/bolt

Ключ позволяет связать результат с исходной записью. Никакие файлы и URL реально не меняются.

Пустая замена — всё ещё найденное значениеСамостоятельный пример
<?php
declare(strict_types=1);

$removed = preg_filter('~\Aobsolete\z~', '', 'obsolete', -1, $count);
echo '[', $removed, ']:', $count, "\n";
$absent = preg_filter('~\Aobsolete\z~', '', 'active');
echo $absent === null ? 'Нет замены' : 'Есть замена';
echo ':', preg_last_error() === PREG_NO_ERROR ? 'без ошибки' : 'ошибка', "\n";
Ожидаемый результат
[]:1
Нет замены:без ошибки

Пустая строка результата отличается от null. Сразу читаем ошибку: следующий preg-вызов может изменить её. При пакетной важной миграции проще явно проверять каждую строку.

Проверка if (!$result) теряет различие пустого текста, [] и null. При неверном шаблоне возможны warning и пустой/нулевой результат; не трактуйте «ничего не изменено» как доказательство успешного прогона. Для отбора без изменений используйте preg_grep. limit 0 означает отсутствие замен даже если правило могло бы совпасть.

Попробуйте. Замените obsolete на obsolete: результат всё равно включается, потому что выполнена замена, хотя байты те же.

Дальше: preg_replace, preg_grep, preg_last_error. Как устроен шаблон и вызов; ошибка или нет совпадения.

preg_replace_callbackВычислить замену для каждого совпадения: увеличить число или подставить буквальную цену из данных.

PHP 4.0.5+; flags PHP7.4+ · pcre

preg_replace_callback(string|array $pattern, callable $callback, string|array $subject, int $limit = -1, int &$count = null, int $flags = 0): string|array|null

function (array $m): string {...} — функция без имени, которую preg_replace_callback вызывает за нас. return отдаёт замену. (int) преобразует учебные цифры в целое, + увеличивает, (string) превращает число обратно в текст.

Аргумент / результатДоговор
patternОдин шаблон или массив, применяемый последовательно.
callbackВызываемая функция handler(array $matches): string. [0] — целое совпадение, далее группы. Возвращает буквальную замену, а не replacement-шаблон.
subjectСтрока либо массив строк; сохраняются ключи.
limit / &count-1 — все; максимум на шаблон/строку; count — сумма замен.
flags = 0С PHP7.4: PREG_OFFSET_CAPTURE и PREG_UNMATCHED_AS_NULL меняют формат matches так же, как у preg_match.
РезультатНовая строка/массив; без совпадений исходные значения; null при PCRE-ошибке. Исключение из callback распространяется наружу.
ОшибкиНекомпилируемый шаблон выдаёт E_WARNING; ошибка выполнения видна в preg_last_error сразу после вызова. Неверные типы/значения аргументов могут вызвать TypeError/ValueError. Не смешивайте технический отказ с отсутствием совпадения.
ВерсияPHP 4.0.5+; flags PHP7.4+; встроенное расширение pcre
Данные первого примера
  1. "batch=4" → группа 1 содержит 4.
  2. Callback вычисляет 4+1 и возвращает "batch=5".
  3. Текст получает новую метку; внешней записи нет.
Увеличить локальный номер партииСамостоятельный пример
<?php
declare(strict_types=1);

$next = preg_replace_callback('~batch=([0-9]{1,4})~', function (array $m): string {
    return 'batch=' . ((int) $m[1] + 1);
}, 'batch=4');
echo $next, "\n";
Ожидаемый результат
batch=5

Это преобразование текста; для уникальных номеров в базе нужна транзакционная политика. Ограничение четырьмя цифрами держит учебное число малым.

Подставить цену с $ без обратных ссылокСамостоятельный пример
<?php
declare(strict_types=1);

$values = ['price' => '$1.00', 'sku' => 'BR41'];
$result = preg_replace_callback('~\{\{([a-z]+)\}\}~',
    function (array $m) use ($values): string { return $values[$m[1]] ?? $m[0]; },
    '{{sku}} costs {{price}}; {{unknown}}');
echo $result, "\n";
Ожидаемый результат
BR41 costs $1.00; {{unknown}}

use копирует доступ к словарю в замыкание; ?? оставляет неизвестную метку. Возвращённый $1.00 не разбирается как ссылка на группу. Это текстовый шаблон; для HTML результат экранируют в контексте вывода.

В callback видны null и байтовые позицииСамостоятельный пример
<?php
declare(strict_types=1);

$result = preg_replace_callback('~(?<sku>BR[0-9]+)(?:=(?<qty>[0-9]+))?~',
    function (array $m): string {
        return $m['sku'][0] . '@' . $m['sku'][1] . ':' . ($m['qty'][0] ?? 'нет');
    }, 'x BR41', -1, $count, PREG_OFFSET_CAPTURE | PREG_UNMATCHED_AS_NULL);
echo $result, '/', $count, "\n";
Ожидаемый результат
x BR41@2:нет/1

Флаги меняют элементы с текстов на пары: [0] текст или null, [1] byte-offset. Чтение $m["sku"] как строки после этого ошибочно.

Callback не запускается при отсутствии совпадения. Сохраняйте возвращённый результат, а не меняйте захваченный массив. Не выполняйте PHP из пользовательского текста. Если callback вызывает другие preg-функции, сохраняйте их диагностику локально: один общий last_error не журнал всех операций. Контекстные изменения в БД/почте внутри callback потребовали бы отдельной идемпотентности; здесь их нет.

Попробуйте. Добавьте values["unknown"] = "готово": изменится только соответствующая метка.

Дальше: preg_replace, preg_replace_callback_array, preg_quote. Как устроен шаблон и вызов; ошибка или нет совпадения.

preg_replace_callback_arrayЗадать отдельное действие каждому типу метки в одном последовательном конвейере.

PHP 7.0+; flags PHP7.4+ · pcre

preg_replace_callback_array(array $pattern, string|array $subject, int $limit = -1, int &$count = null, int $flags = 0): string|array|null

Оператор => связывает шаблон с функцией. fn (array $m): string => выражение — короткая функция, возвращающая выражение. Правила выполняются в порядке вставки, а не по позиции первого совпадения в тексте.

Аргумент / результатДоговор
patternАссоциативный массив: строка-шаблон => callable. Ключи уникальны: повтор одинакового ключа массива перезапишет прежний callback.
subjectСтрока либо массив строк.
limit = -1 / &countЛимит на каждый шаблон/строку; суммарное число замен.
flags = 0PREG_OFFSET_CAPTURE | PREG_UNMATCHED_AS_NULL для формата matches; доступны с PHP7.4.
РезультатНовая строка/массив; при PCRE-ошибке null. Ошибка аргумента/callback или исключение обработчика не превращаются автоматически в null.
ОшибкиНекомпилируемый шаблон выдаёт E_WARNING; ошибка выполнения видна в preg_last_error сразу после вызова. Неверные типы/значения аргументов могут вызвать TypeError/ValueError. Не смешивайте технический отказ с отсутствием совпадения.
ВерсияPHP 7.0+; flags PHP7.4+; встроенное расширение pcre
Данные первого примера
  1. "sku:br41 qty:3" содержит две формы поля.
  2. Первое правило переводит код в верхний регистр.
  3. Второе оформляет количество → BR41 × 3.
Разные действия для кода и количестваСамостоятельный пример
<?php
declare(strict_types=1);

$rules = [
    '~sku:([a-z0-9]+)~' => fn (array $m): string => strtoupper($m[1]),
    '~qty:([0-9]+)~' => fn (array $m): string => '× ' . $m[1],
];
echo preg_replace_callback_array($rules, 'sku:br41 qty:3'), "\n";
Ожидаемый результат
BR41 × 3

strtoupper здесь предназначен только для ASCII-кода. Русские названия не проходят через него.

Следующее правило видит результат первогоСамостоятельный пример
<?php
declare(strict_types=1);

$rules = [
    '~\{sku\}~' => fn (array $m): string => 'BR41',
    '~BR([0-9]+)~' => fn (array $m): string => 'bolt-' . $m[1],
];
$result = preg_replace_callback_array($rules, ['label' => '{sku}'], -1, $count);
echo $result['label'], '/', $count, "\n";
Ожидаемый результат
bolt-41/2

Два совпадения в разных стадиях дают count=2. Если новые значения не должны повторно обрабатываться, используйте один общий шаблон и один dispatch-callback.

Функция не выбирает «первое из всех правил». Порядок влияет на результат; лимит не общий для конвейера. Для пустого массива правил результат остаётся прежним. Имена полей лучше разобрать одной грамматикой, если их значения могут содержать текст, похожий на другие метки.

Попробуйте. Поменяйте правила во втором примере местами: получите BR41/1.

Дальше: preg_replace_callback, preg_replace, preg_last_error. Как устроен шаблон и вызов; ошибка или нет совпадения.

preg_last_errorОтличить повреждённый UTF-8 и исчерпание ресурса от нормального ответа «не найдено».

PHP 5.2+ · pcre

preg_last_error(): int

Пустые скобки означают отсутствие аргументов. Вызов читает состояние последней PCRE-операции. Сохраните код сразу после результата, до следующей проверки или regex-логгера.

Аргумент / результатДоговор
РезультатОдин код: PREG_NO_ERROR, PREG_INTERNAL_ERROR, PREG_BACKTRACK_LIMIT_ERROR, PREG_RECURSION_LIMIT_ERROR, PREG_BAD_UTF8_ERROR, PREG_BAD_UTF8_OFFSET_ERROR или PREG_JIT_STACKLIMIT_ERROR.
ОшибкиНе сообщает ошибки PHP-типов/исключения callback. Сообщение компиляции обычно приходит отдельным E_WARNING.
ВерсияPHP 5.2+; встроенное расширение pcre
Данные первого примера
  1. Передан байт FF под режимом /u.
  2. preg_match возвращает false.
  3. Код BAD_UTF8 → вход не соответствует договору UTF-8.
Повреждённая входная строкаСамостоятельный пример
<?php
declare(strict_types=1);

$result = preg_match('~. sentinel~u', "\xFF");
$error = preg_last_error();
echo $result === false ? 'Отказ' : 'Готово';
echo ':', $error === PREG_BAD_UTF8_ERROR ? 'не UTF-8' : 'другая причина', "\n";
Ожидаемый результат
Отказ:не UTF-8

\xFF в двойных кавычках создаёт конкретный невалидный байт. Это не попытка автоматически угадать его настоящую кодировку.

Сохранить причину до другого поискаСамостоятельный пример
<?php
declare(strict_types=1);

preg_match('~.~u', 'Я', $m, 0, 1);
$saved = preg_last_error();
preg_match('~BR~', 'NT');
echo $saved === PREG_BAD_UTF8_OFFSET_ERROR ? 'Старт внутри UTF-8' : 'Иное';
echo "\n", preg_last_error() === PREG_NO_ERROR ? 'Последняя проверка штатная' : 'Ошибка', "\n";
Ожидаемый результат
Старт внутри UTF-8
Последняя проверка штатная

Offset 1 попадает во второй байт Я. После штатного отсутствия BR ошибка сбрасывается: состояние не хранит историю.

Не сравнивайте числовые коды с выдуманными литералами: используйте имена констант. Ограничения backtrack/depth не измеряются числом символов или миллисекундами. Не повышайте их автоматически на недоверенном вводе: сначала ограничьте размер/упростите правило и обработайте отказ.

Попробуйте. Передайте offset=0 во втором примере: сохранённый код станет PREG_NO_ERROR.

Дальше: preg_last_error_msg, preg_match. Как устроен шаблон и вызов; ошибка или нет совпадения.

preg_last_error_msgПолучить понятную диагностическую строку для разработчика, сохранив машинный код отдельно.

PHP 8.0+ · pcre

preg_last_error_msg(): string

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

Аргумент / результатДоговор
РезультатТекст последнего PCRE-состояния; без ошибки "No error". Строка предназначена для диагностики, не для выбора бизнес-ветки.
ОшибкиСообщение отражает общее состояние PCRE, а не полный текст compile-warning с позицией и не историю всех попыток.
ВерсияPHP 8.0+; встроенное расширение pcre
Данные первого примера
  1. "BR41" соответствует правилу.
  2. Последний PCRE-статус штатный.
  3. Диагностика: No error.
Штатная диагностика при отсутствии совпаденияСамостоятельный пример
<?php
declare(strict_types=1);

$found = preg_match('~\ABR[0-9]+\z~', 'NT8');
echo $found, ':', preg_last_error_msg(), "\n";
Ожидаемый результат
0:No error

Ноль — полезный результат проверки. Сообщение No error не утверждает, что совпадение найдено.

Сохранить диагноз рядом с результатомСамостоятельный пример
<?php
declare(strict_types=1);

$result = preg_match('~.~u', "\xFF");
$diagnostic = ['code' => preg_last_error(), 'message' => preg_last_error_msg()];
if ($result === false) {
    echo $diagnostic['code'] === PREG_BAD_UTF8_ERROR ? 'input-invalid' : 'regex-failed';
    echo "\n", $diagnostic['message'], "\n";
}
Ожидаемый результат
input-invalid
Malformed UTF-8 characters, possibly incorrectly encoded

Точная английская строка проверена на PHP8.5.11. Пользовательский ответ выбирается по стабильной константе; сообщение может измениться между версиями.

Вызовы preg_last_error/preg_last_error_msg сами не запускают regex, поэтому можно прочитать оба. Последующая preg-операция изменяет наблюдаемое состояние. Для PHP7 аналог строили через карту констант; сам этот API доступен с PHP8.

Попробуйте. Замените FF на обычное A: result станет 1, диагностический message — No error.

Дальше: preg_last_error, preg_match. Как устроен шаблон и вызов; ошибка или нет совпадения.

Источники и обсуждения

Код и руководство с собой

Скачайте самостоятельные примеры с исходными данными, инструкциями и ожидаемыми результатами. Примеры HTTP и фрагменты для проекта помечены отдельно: им нужно указанное окружение.

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

Материалы сверены 2026-10-12. Примеры и объяснения написаны для этого руководства; источники и обсуждения доступны в каждой главе.