Индексный доступ к Multibyte-строкам на PHP или изучение ООП на практике

в 14:22, , рубрики: php, ооп, метки: ,

Предыстория: вот, кажется, совсем недавно фирма, в которой я трудился Delphi программистом, издала последний вздох и развалилась. Юридически возможно и нет, но большое число сотрудников начало искать себе новое место работы, в том числе и я. Не буду дискутировать на счет востребованости сейчас desktop подхода, да и актуальности Delphi, но я решил воспользоваться сложившейся ситуацией для смены рода деятельности. А именно изучив предложения по трудоустройству в моем регионе (мелкий областной центр) я решил стать Web разработчиком на PHP. И в итоге мои НГ праздники прошли больше за книгой, нежели за праздничным столом.

Почти сразу я столкнулся с ситуацией меня немного смутившей: как язык, на котором крутится большинство сайтов интернета, в оном уже определилось абсолютное преимущество UTF-8, не имеет более-менее вменяемой его поддержки. Прочитав о стандартном методе решения – расширении mb_strings я успокоился, но некоторое неудобство в использовании оставило свой осадок. А именно: отсутствие метода доступа к символу как элементу массива и аналогов ряда стандартных функций в их мультибайт аналогах. Но задерживаться было нельзя, я и дальше штудировал литературу и по разным вопросам обращаясь к Google, но постоянно натыкался на топики начинающих о неудобстве работы с мультибайт строками и ожидании PHP6. Честно говоря, они мне даже поднадоели. Если в прямом виде аналогов в mb_strings нет, но все необходимое для собственной реализации было.

И вот, дойдя до темы о стандартных интерфейсы я увидел то, чего недоставало мне как новичку:

1. ООП подход для инкапсуляции информации о кодировке и методах обработки строки

class MBString
{
    private $string;
    private $encoding;

    public function __construct($string, $encoding = 'UTF-8') {
        $this->string = $string;
        $this->encoding = $encoding;
    }

    public function __toString() {
        return (string)$this->string;
    }

    /**
     * Длинна строки в символах
     * @return int
     */
    function Length() {
        return mb_strlen($this->string, $this->encoding);
    }

    /**
     * Размер данных в байтах
     * @return int
     */
    function Size() {
        return strlen($this->string);
    }

    /**
     * Возвращает кодировку строки
     * @return string
     */
    function getEncoding() {
        return $this->encoding;
    }

    /**
     * Изменяет кодировку строки
     * @param $encoding
     */
    function setEncoding($encoding) {
        $this->string = mb_convert_encoding($this->string, $encoding, $this->encoding);
        $this->encoding = $encoding;
    }

    /**
     * Перечисляет поддерживаемые кодировки
     * @return array
     */
    static function SupportEncodings() {
        return mb_list_encodings();
    }

    /**
     * Извлечение символа по индексу
     * @param int $i
     * @return string
     */
    function GetChar($i) {
        return mb_substr($this->string, $i, 1, $this->encoding);
    }

    /**
     * Устанавливает символ по индексу
     * @param int $i
     * @param string $char
     */
    function SetChar($i, $char) {
        $this->string = mb_substr($this->string, 0, $i, $this->encoding)
            .mb_substr($char, 0, 1, $this->encoding) //Защита на случай если передадут строку
            .mb_substr($this->string, $i+1, $this->Length()-($i+1), $this->encoding);
    }

    /**
     * Удаляет символ с индексом
     * @param int $i
     */
    function UnSetChar($i){
        $this->string = mb_substr($this->string, 0, $i, $this->encoding).mb_substr($this->string, $i+1, $this->Length()-($i+1), $this->encoding);
    }

    function UCFirst() {
        $this->SetChar(0, mb_strtoupper($this->GetChar(0), $this->encoding));
        return $this->string;
    }

    function UCWords() {
        return $this->string = mb_convert_case($this->string, MB_CASE_TITLE, $this->encoding);
    }
}

Ничего необычного в классе нет, он использует стандартные функции mb_strings для индексного доступа к символу строки, и далее уже на основе созданных методов приводится пример реализации пары функций недостающих в mb_strings: UCFirst и UCWords. В последствии класс можно дополнить всем чего не хватало именно вам. В классе реализован магический метод __toString() который оказался очень даже кстати для данного класса.

Пара замечаний:

  • так как при реализации подобного класса четко контролируются методы где размерность строки (в символах) может измениться, то рекомендуется реализовать метод Length кешируемым, т.е. хранить размер в приватной переменной и пересчитывать ее (mb_strlen($this->string, $this->encoding)) только в случае равенства null, об-null’ять же переменную во всех методах изменяющих размерность строки. По причине что вызовы Length частая операция, а функция mb_strlen не самая быстрая.
  • Можно добавить в класс функцию Add(MBString $string) для возможности объединения строк с учетом их кодировок (приводить присоединяемую строку к кодировке класса)
2. стандартного интерфейса Iterator (IteratorAggregate) для foreach

    protected $iterator_index = 0;

    public function rewind() {
        $this->iterator_index = 0;
    }

    public function current()
    {
        return $this->GetChar($this->iterator_index);
    }

    public function key() {
        return $this->iterator_index;
    }

    public function next() {
        ++$this->iterator_index;
    }

    public function valid() {
        return ($this->iterator_index < $this->Length());
    }

Интерфейс Iterator требует реализации ряда простых методов, но позволит проходиться по массиву всеми так полюбившимся оператором foreach.

Замечания:

  • Обновите заголовок класса к виду class MBString implements Iterator
  • Существует так же альтернативный интерфейс IteratorAggregate с аналогичной функциональностью преимущество его в том, что можно «выкинуть» часть дублирующихся методов за пределы класса, а в самом классе реализовать только функцию getIterator возвращающую класс посредник. Реализация его тривиальна и практически ничем (кроме ссылки на родительский класс) не отличается от перечисленных выше методов.
3. интерфейса ArrayAccess для индексного доступа

    function offsetExists($offset) {
        return ($offset < $this->Length());
    }

    public function offsetGet($offset) {
        return $this->GetChar($offset);
    }

    public function offsetSet($offset, $value) {
       $this->SetChar($offset, $value);
    }

    public function offsetUnset($offset) {
        $this->UnSetChar($offset);
    }

Всего пара строк кода, но ООП и PHP теперь позволяют обращаться к символу строки как элементу массива, причем не зависимо от кодировки!

Замечания:

  • Обновите заголовок класса к виду class MBString implements Iterator, ArrayAccess
  • При изменении единичного символа все же необходимо учитывать кодировку (присваиваемый символ должен иметь кодировку строки в классе)
4. ну и интерфейса Countable для более полной эмуляции массивов

    public function count() {
        return $this->Length();
    }

Теперь функцию count можно применять к классу и перебор строки циклом for будет сродни обходу массива.

Поовтрюсь: Обновите заголовок класса к виду class MBString implements Iterator, ArrayAccess, Countable

Пример использования

require_once('MBString.php');
$mbStr = new MBString('Здравствуй мир');

echo 'Использование магического __toString(): ',"$mbStr<br/>";
echo 'Length: ',$mbStr->Length(), ' Size: ', $mbStr->Size(), '<br/>';
echo '$mbStr[0]: ', $mbStr[0], '<br/>';
$mbStr->SetChar(0, 'z');
echo $mbStr, '<br/>';
echo 'UCFirst: ', $mbStr->UCFirst(), '<br/>';
echo 'UCWords: ', $mbStr->UCWords(), '<br/>';
foreach($mbStr as $k=>$v){
    echo $v, '-';
}
echo '<br>';
for ($i=0; $i< $mbStr->Length(); $i++){
    echo $mbStr[$i], '+';
}

Использование магического __toString(): Здравствуй мир
Length: 14 Size: 27
$mbStr[0]: З
zдравствуй мир
UCFirst: Zдравствуй мир
UCWords: Zдравствуй Мир
Z-д-р-а-в-с-т-в-у-й- -М-и-р-
Z+д+р+а+в+с+т+в+у+й+ +М+и+р+

P.S. Завтра… Вернее уже сегодня мое первое собеседование по PHP, так что всю критику и замечания учту позже.

Автор: salopot

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js