Пишу, по большей части, про историю, свою жизнь и немного про программирование.

PHP и UTF-8: первый этап

Я решил описать переход проекта на UTF-8, как это у меня получается. Иногда будут достаточно больше куски кода. На предыдущем («нулевом») этапе я систематизировал все знания, которые получил на этапе исследования. На этом этапе я хочу посмотреть сколько же функций нужно будет переписать. Из всего богатства PHP проект использует далеко не все функции, так что нет никакого смысла переписывать их все.

Основная идея — я добавлю в проект статический класс UTF. В этом классе будет переключатель (я назвал его «ON»), если он в состоянии «false», то все статические методы класса вызывают функции, работающие с однобайтовыми кодировками, иначе — с UTF-8.

Выглядит он примерно так:

class UTF
{
    const ON = false;

    static public function strpos($haystack, $needle, $offset = 0)
    {
        if (self::ON) {
            return mb_strpos($haystack, $needle, $offset, 'UTF-8');
        }

        return strpos($haystack, $needle, $offset);
    }

    // … и так далее
}

Для чего нужен переключатель ON? Он позволяет переключить разом весь проект в состояние «UTF-8», посмотреть что не заработало и откатиться, если нужно. Кроме того, это позволяет заменять функции даже в том (моём) случае, если с проектом работают другие люди — пока переключатель выключен, остальные члены команды не заметят изменений.

Итак, нам надо посмотреть что же нужно переписывать и заменять в проекте. Я поступил просто — открыл в руководстве по PHP разделы строковых функций, функций PCRE и регулярных выражений в формате POSIX, скопировал их себе в программу и оформил как регулярное выражение:

<?
$funclist = <<<LIST
addcslashes — Quote string with slashes in a C style
chop — Alias of rtrim
chr — Return a specific character
chunk_split — Split a string into smaller chunks
convert_cyr_string — Convert from one Cyrillic character set to another
count_chars — Return information about characters used in a string
hebrev — Convert logical Hebrew text to visual text
hebrevc — Convert logical Hebrew text to visual text with newline conversion
html_entity_decode — Convert all HTML entities to their applicable characters
htmlentities — Convert all applicable characters to HTML entities
htmlspecialchars_decode — Convert special HTML entities back to characters
htmlspecialchars — Convert special characters to HTML entities
lcfirst — Make a string`s first character lowercase
levenshtein — Calculate Levenshtein distance between two strings
localeconv — Get numeric formatting information
ltrim — Strip whitespace (or other characters) from the beginning of a string
metaphone — Calculate the metaphone key of a string
money_format — Formats a number as a currency string
nl_langinfo — Query language and locale information
ord — Return ASCII value of character
rtrim — Strip whitespace (or other characters) from the end of a string
setlocale — Set locale information
similar_text — Calculate the similarity between two strings
soundex — Calculate the soundex key of a string
sscanf — Parses input from a string according to a format
str_getcsv — Parse a CSV string into an array
str_ireplace — Case-insensitive version of str_replace.
str_pad — Pad a string to a certain length with another string
str_rot13 — Perform the rot13 transform on a string
str_shuffle — Randomly shuffles a string
str_split — Convert a string to an array
str_word_count — Return information about words used in a string
strcasecmp — Binary safe case-insensitive string comparison
strcmp — Binary safe string comparison
strcoll — Locale based string comparison
strcspn — Find length of initial segment not matching mask
stripcslashes — Un-quote string quoted with addcslashes
stripos — Find position of first occurrence of a case-insensitive string
stristr — Case-insensitive strstr
strlen — Get string length
strnatcasecmp — Case insensitive string comparisons using a "natural order" algorithm
strnatcmp — String comparisons using a "natural order" algorithm
strncasecmp — Binary safe case-insensitive string comparison of the first n characters
strncmp — Binary safe string comparison of the first n characters
strpbrk — Search a string for any of a set of characters
strpos — Find position of first occurrence of a string
strrchr — Find the last occurrence of a character in a string
strrev — Reverse a string
strripos — Find position of last occurrence of a case-insensitive string in a string
strrpos — Find position of last occurrence of a char in a string
strspn — Finds the length of the first segment of a string consisting entirely of characters contained within a given mask.
strtolower — Make a string lowercase
strtoupper — Make a string uppercase
strtr — Translate certain characters
substr_compare — Binary safe comparison of two strings from an offset, up to length characters
substr_count — Count the number of substring occurrences
substr_replace — Replace text within a portion of a string
substr — Return part of a string
trim — Strip whitespace (or other characters) from the beginning and end of a string
ucfirst — Make a string`s first character uppercase
ucwords — Uppercase the first character of each word in a string
wordwrap — Wraps a string to a given number of characters
preg_filter — Perform a regular expression search and replace
preg_grep — Return array entries that match the pattern
preg_last_error — Returns the error code of the last PCRE regex execution
preg_match_all — Perform a global regular expression match
preg_match — Perform a regular expression match
preg_quote — Quote regular expression characters
preg_replace_callback — Perform a regular expression search and replace using a callback
preg_replace — Perform a regular expression search and replace
preg_split — Split string by a regular expression
ereg_replace — Replace regular expression
ereg — Regular expression match
eregi_replace — Replace regular expression case insensitive
eregi — Case insensitive regular expression match
split — Split string into array by regular expression
spliti — Split string into array by regular expression case insensitive
sql_regcase — Make regular expression for case insensitive match
ctype_alnum — Check for alphanumeric character(s)
ctype_alpha — Check for alphabetic character(s)
ctype_cntrl — Check for control character(s)
ctype_digit — Check for numeric character(s)
ctype_graph — Check for any printable character(s) except space
ctype_lower — Check for lowercase character(s)
ctype_print — Check for printable character(s)
ctype_punct — Check for any printable character which is not whitespace or an alphanumeric character
ctype_space — Check for whitespace character(s)
ctype_upper — Check for uppercase character(s)
array_change_key_case — Changes all keys in an array
array_multisort — Sort multiple or multi-dimensional arrays
arsort — Sort an array in reverse order and maintain index association
asort — Sort an array and maintain index association
krsort — Sort an array by key in reverse order
ksort — Sort an array by key
natcasesort — Sort an array using a case insensitive "natural order" algorithm
natsort — Sort an array using a "natural order" algorithm
rsort — Sort an array in reverse order
sort — Sort an array
LIST;

$funcs = array();

foreach (explode("\n", $funclist) as $line) {
    list($line) = explode(" ", $line, 2);

    $funcs[] = $line;
}

$regexp = '/(?<!UTF::)\b(' . implode('|', $funcs) . ')\s*\(/si';

set_time_limit(0);

$it = new RecursiveIteratorIterator(new RecursiveDirectoryIterator('.'));

for (;$it->valid(); $it->next()) {
    $ext = pathinfo($it->getBaseName(), PATHINFO_EXTENSION);

    if ($ext == 'php') {
        if (preg_match_all($regexp, file_get_contents($it->key()), $matches)) {
            echo $it->key(), "\n";

            echo '    ', implode("\n    ", array_map('strtolower', $matches[1])), "\n\n";
        }
    }
}

Список функций я проредил — удалил оттуда те, у которых проблем с UTF-8 нет (str_replace, explode, md5 и прочие), после запуска у меня получился список файлов с которым придётся работать и список функций, которые надо заменять (то есть те, которые должны быть описаны в классе UTF):

bolk@dev:~/daproject$ grep ' ' tool.log | sort | uniq -c | sort -rn
    225     substr
    195     preg_replace
    174     trim
    132     preg_match
    132     htmlspecialchars
    127     strpos
    113     strtolower
     88     strlen
     50     preg_replace_callback
     40     rtrim
      …

bolk@dev:~/daproject$ grep './' tool.log
./tool.php
./htdocs/_file.php
./htdocs/file1.php
./htdocs/dir/file1.php
./htdocs/dir/file2.php
./htdocs/dir/file3.php
./htdocs/dir/file4.php
./htdocs/dir/file5.php
./htdocs/class.php
./htdocs/_ajax.php
…

На следующем этапе я заменю эти функции на их аналоги. Кстати, если у вас используются оба алиаса одной функции (например, chop и trim), то подумайте над тем, чтобы привести всё к единому виду.

Отдельно надо будет что-то решать с файловыми функциями — они работают с байтами, а не с символами UTF-8, пока я откладываю их в сторону, попробую решить это позже.

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

2 комментария
Тормоз (brokenbrake.biz) 2011

Функцию similar_text тоже заменять надо, если важна точность. Т. к. в UTF-8 русские слова она будет сравнивать некорректно (считать одну букву за две).

Евгений Степанищев (bolknote.ru) 2011

Комментарий для brokenbrake.biz:

Спасибо!