Краткое содержание: используйте mb_* функции. Не используйте доступ к строке по индексу ($str[0]). В регулярных выражениях используйте флаг u (он говорит, что используется utf-8, а не однобайтовая кодировка).
Некоторые функции PHP (вроде strlen, substr, а также обращение к строке как к массиву: $str[0]) не работают с текстами в многобайтовых кодировках (вроде utf-8). Такие функции нормально работают со строкой из латинских букв, но если мы попытаемся передать строку с кирилицей, то буквы превращаются в непонятные символы или теряются.
Вот пример неправильно написанного кода:
var_dump(strlen("азъ")); // выводит int(6) вместо 3
var_dump(strrev("hello")); // выводит string(5) "olleh"
var_dump(strrev("аякс")); // выводит string(8) "�ѺЏѰ�" вместо "скяа"
$str = "хор";
var_dump($str[0]); // выводит string(1) "�" вместо первой буквы "х"Чтобы понять, почему так происходит, придется вспомнить историю. В компьютерах данные в памяти хранятся в виде байтов, где байт можно представить целым числом от 0 до 255. Соответственно, текст тоже хранится в памяти в виде последовательности байтов. Первоначально для этого использовались однобайтовые кодировки вроде ASCII, в которых символы кодировались числами от 0 до 255, и таким образом один символ занимал в памяти ровно один байт. Написанный в то время код опирался на это предположение. Например, функция для определения длины строки просто смотрела, сколько байтов памяти она занимает и возвращала это число, не глядя на содержимое этих байтов.
К примеру, строка "hello" кодируется в ASCII как последовательность из 5 байтов 104, 101, 108, 108, 111.
Когда компьютеры стали распространяться по всему миру, 256 кодов стало недостаточно, чтобы представить десятки тысяч символов из различных языков и в 90-е годы пришлось переходить на Юникод (кодировка, которая пытается присвоить коды всем существующим символам из любых языков) и многобайтовые кодировки вроде utf-8. В utf-8 символ может кодироваться последовательностью длиной от 1 до 4 байтов. Латинские символы в utf-8 кодируются одним байтом с точно таким же кодом, как и в ASCII, а символы кириллицы - двумя.
Вот как кодируется в utf-8 слово "азъ": 208, 176, 208, 183, 209, 138. Подробно про кодировки и их историю я написал в отдельной статье про способ кодирования строк в памяти.
Старый код, рассчитанный на однобайтовые кодировки, не работает с utf-8. Например, функция определения длины строки, которая просто считает число байт в ней, вернет 6 вместо 3 для строки "азъ". Функция, которая отрезает первый символ строки, вернет один байт 208 вместо пары байт 208, 176, которые представляют букву "а". Функция, которая переворачивает строку, перепутает байты местами.
В PHP, к сожалению, функции вроде strlen используют такой устаревший код, и потому дают некорретные результаты. Вместо них надо использовать функции, которые "знают" о многобайтовых кодировках и корректно обрабатывают тексты в них. В PHP такие функции содержатся в расширении mbstring и имеют имена, начинающиеся с mb_, например, mb_strlen, mb_substr.
Для корректной работы этих функций надо сообщить им о том, какая кодировка используется. Это делается либо опцией default_charset в файле php.ini, либо функцией mb_internal_encoding в начале программы:
mb_internal_encoding('utf-8');В большинстве случаев utf-8 уже задана как кодировка по умолчанию, и делать ничего не требуется, но описанные выше советы позволяют гарантировать, что кодировка задана правильно.
К сожалению, некоторые учебники или статьи могут до сих пор использовать давно устаревшие функции. Вот таблица, показывающая, какие функции стоит использовать вместо них:
| Не поддерживает utf-8 | Поддерживает utf-8 | Примечания |
|---|---|---|
Взятие символа по индексу: $str[0] |
mb_substr($str, 0, 1) | |
| chr | mb_chr | |
| lcfirst | нету аналога | Можно отрезать первую букву с помощью mb_substr(), перевести ее в нижний регистр mb_strtolower() и приклеить остаток строки |
| ord | mb_ord | ord() возвращает значение первого байта в строке от 0 до 255, а mb_ord() - код первого символа |
| str_pad | нету аналога | |
| str_shuffle | нету аналога | Можно разбить строку на массив символов, использовать shuffle() и собрать обратно в строку |
| strcasecmp | нету аналога | Можно привести обе строки в нижний регистр с помощью mb_strtolower() и сравнить с помощью класса Collator из расширения intl |
| strcmp, strcoll | нету аналога | Можно использовать класс Collator из расширения intl для сравнения и сортировки строк с учетом правил нужного языка, смотрите урок про сравнение строк |
| strlen | mb_strlen | |
| strpos | mb_strpos | |
| strrev | нету аналога | Можно разбить строку на массив символов, перевернуть массив с помощью array_reverse и склеить обратно |
| strtolower | mb_strtolower | |
| strtoupper | mb_strtoupper | |
| substr | mb_substr | |
| ucfirst | нету аналога | Можно отрезать первый символ с помощью mb_substr(), перевести в верхний регистр с помощью mb_strtoupper() и приклеить остаток строки |
| ucwords | mb_convert_case с опцией MB_CASE_TITLE |
Вот список менее популярных функций, которые тоже не поддерживают utf-8 и которые не стоит использовать: chunk_split, count_chars, levenshtein, similar_text, str_ireplace, stripos, str_split, str_word_count, strchr, strcspn, stristr, strnatcasecmp, strnatcmp, strncasecmp, strncmp, strpbrk, strrchr, strripos, strspn, strstr, strtok, substr_compare, substr_count, substr_replace, wordwrap.
Латиница и цифры кодируются в utf-8 одним байтом, с ними устаревшие функции работают, но все равно, не стоит использовать эти функции — это слишком ненадежно и легко сделать ошибку.
Эти функции работают корректно с utf-8: addslashes(), stripslashes(), explode(), implode(), htmlspecialchars(), nl2br(), number_format(), str_repeat(), strtr() при использовании с 2 аргументами (с массивом, а не со строками).
Функция trim() (и ltrim(), rtrim()) работает корректно с utf-8 только если мы отрезаем символы из ASCII, кодирующиеся одним байтом (например, перенос строки, пробел или латинскую букву). В других случаях, если, например, написать trim('миша вова', 'м'), она воспринимает букву м, закодированную двумя байтами, как два символа, и корежит исходную строку.
Функции из семейства printf/scanf (fprintf, vprintf, sprint, sscanf и тд) ошибочно считают длины строк в байтах, а не в символах. Например, printf("%10s", "азъ") посчитает, что "азъ" состоит из 6 символов и добавит 4 пробела для выравнивания, а не 7.
Чтобы работать с кирилицей (и другими нелатинскими) буквами в регулярках, надо ставить в конце флаг u: preg_match("/[абвг]/u", $string). Иначе preg_match будет думать, что работает с однобайтной кодировкой latin-1 и будет видеть не одну русскую букву, а 2 символа (так как русская буква кодируется как 2 байта). Например, буква л, кодирующаяся как 208 187, будет восприниматься как 2 символа с кодами 208 и 187, то есть л (в кодировке latin-1). Регулярка будет работать некорректно.
Строку в utf-8 можно разбить на массив символов таким кодом:
$str = "Тест";
$chars = preg_split("//u", $str, null, PREG_SPLIT_NO_EMPTY);
var_dump($chars); // Массив ["Т", "е", "с", "т"]Пустое регулярное выражение "срабатывает" на границах между буквами, а опция PREG_SPLIT_NO_EMPTY удаляет из массива два пустых элемента в начале и конце. Это позволяет использовать функции работы с массивами вроде array_reverse(). Собрать строку обратно из массива можно с помощью $str = implode("", $chars);.
Обычное сравнение строк в PHP (if ($s1 > $s2)) просто сравнивает байты, из которых они состоят, и не учитывает правила сортировки строк, которые зависят от языка. В этом уроке про корректную сортировку строк описано, как можно для этого использовать класс Collator из расширения intl.
В Юникоде, кроме обычных, есть комбинирующие символы, которые позволяют добавлять какой-нибудь диакритический знак идущему перед ними символу. Например, буква m̊ составлена из символов m и знака кружочка над буквой. В utf-8 это кодируется как 109 (буква m), 204, 138 (знак кружочка). При печати эти 2 "символа" (которые правильно называть code points) комбинируются в одну графему (сгенерировать такие символы можно на сайте https://symbols.typeit.org/ ).
Если мы попробуем использовать функцию strlen("m̊ "), она вернет 3 - число байт, mb_strlen() вернет 2 - число использованных code points. Это может приводить к проблемам, например, если мы попробуем отрезать первый символ с помощью mb_substr(), то мы получим лишь букву m, а символ кружочка останется отдельно.
Для решения этой проблемы в PHP в расширении intl есть функции работы с графемами. Вот пример их использования:
var_dump(grapheme_strlen('m̊')); // корректно выводит int(1)
var_dump(grapheme_substr('m̊x̂', 0, 1)); // string(3) "m̊"В некоторых (неграмотных) учебниках можно увидеть совет включить опцию mbstring.func_overload (подробнее про нее: http://php.net/manual/ru/mbstring.overload.php ). Она подменяет часть строковых функций вроде strlen на их mb-аналоги. Не стоит так делать, так как это изначально неправильно спроектированная опция. Она не решает проблему, для которой задумывалась (включить в старом приложении, использующем функции вроде strlen, поддержку utf-8), а лишь создает путаницу. Например, при ее включении strlen заменяется на поддерживающую utf-8 mb_strlen, но ucfirst, trim или sprintf ни на что не заменяется и не работает.