Начать стоит с дисклеймера — да, автор что-то страшное (как для мира PHP) употребляет и с удовольствием поделится. Это эксперимент, который может уйти в боевые проекты, а может и остаться здесь как пища для размышлений. Коль желаете-таки причаститься — приглашаю под кат.

Предисловие


Как человек, более-менее долго работавший с JS, я считаю, что представлять переменную любого типа как объект — это довольно удобно и читабельно. Не нужно очень долго искать подтверждения — достаточно взглянуть на блаженную альтернативу, которую нам предоставили C-подобные языки:

$string = trim($string);
$string = mb_strtolower($string);
$string = ucfirst($string);

Дабы не получить почётное звание индуса, многие делают вид, что всё в порядке, предлагая нам такие конструкции:

ucfirst(mb_strtolower(trim($string)));

Но знаете — честно говоря, это не намного лучше. Теперь мы сделали положение дел не столь плачевным, поскольку используем всего одну строку. Оно работает и в порядке вещей, но когда вспоминается JS, Python и прочие подобные языки, начинаешь думать: «Хочу сделать $string->trim()->toLower()->ucfirst()»! В таком варианте куда проще уловить последовательность действий и читать по привычке слева направо, не теряется и сама нить.

Сегодня моя цель — сделать эдакий закос под Python и другие языки с другим пониманием ООП, чем у моего родного PHP. Небольшие примеры находятся в репозитории.

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

Базовый класс


Для начала сделаем класс с бессменными функциями, которые будут базовыми для всех наших типов данных (комментарии, неймспейсы и некоторые детали упускаю, это всё будет в репозитории).

class Base
{
    protected $value;

    public function __construct($initial_value)
    {
        $this->value = $initial_value;
    }

    public function getNative()
    {
        return $this->value;
    }
}

Скелет очень прост — у нас есть некое значение, которое будет присваиваться при создании переменной-объекта. Базовый класс не обращает внимания на типы данных и кушает всё, что ему дают.

Булев тип


Булев тип — это очень просто и занимает мало памяти. И это главная причина, почему не хотелось его сюда вводить.

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

Чем булев тип будет отличаться от базового класса? В первую очередь тем, что $initial_value должен быть строго булевым, иначе всё сломается.

class TypeBool extends Base
{
    public function __construct(bool $initial_value)
    {
        parent::__construct($initial_value);
    }
}

А теперь то, ради чего всё затевалось — при любой манипуляции с «переменной в фантике» должен возвращаться объект. Это и предоставляет ту самую сладкую возможность строить длинные цепочки вызовов. Для булевого типа это может быть только так:

    public function not()
    {
        $this->value = !$this->value;
        return $this;
    }

Сделаем небольшой тест на коленке и посмотрим, как оно сработает:

    $bool = new TypeBool(true);
    var_dump($bool->not()->getNative()); // bool(false)
    var_dump($bool->not()->getNative()); // bool(true)

Если присмотреться к выводу — вспоминаем одну серьёзную особенность PHP (да и не только его) — объект лишь иллюзия ссылка на него. Это отлично вяжется с нашей сегодняшней концепцией, хотя и в некоторых случаях может вызвать путаницу. К этому вернёмся позже.

Давайте попробуем что-нибудь посложнее.

Строка


Конструктор сделаем по тому же принципу:

class TypeString extends Base
{
    public function __construct(string $initial_value)
    {
        parent::__construct($initial_value);
    }
}

А вот дальше веселье. Мы прекрасно помним, что такое строка в PHP — целый набор бубенно-танцевальных практик, связанных с кодировками. Дабы избежать некоторых мучений, я сделал опасный выбор — вкрутил сюда mb_* функции. Может это не лучший с точки зрения памяти выход, но зато универсальный.

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

    public function length(): int // int временно, до создания объектного варианта
    {
        return mb_strlen($this->value);
    }

    public function toLower()
    {
        $this->value = mb_strtolower($this->value);
        return $this;
    }

    public function toUpper()
    {
        $this->value = mb_strtoupper($this->value);
        return $this;
    }

Неплохо? Но главная фишка нашей строки будет не в этом.

Дело в том, что новая объектная модель немного резонирует со стандартными функциями PHP. А поскольку мы пока не собрались переписывать всё на своё — давайте добавим немного совместимости:

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

И о ура! Теперь наш объект-строку можно передавать в те функции, которые требуют строку! Давайте весь этот огород теперь проверим:

    $str = new \Type\TypeString('HeLlO WoRlD');

    var_dump(
        $str->toLower()->toUpper()->getNative(), // string(11) "HELLO WORLD"
        $str->length(), // int(11)
        md5($str) // string(32) "361fadf1c712e812d198c4cab5712a79"
    );

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

Кстати, в булев тип тоже можно добавить:

    public function __toString(): string
    {
        return $this->value ? "true" : "false";
    }

Массивы


Магия ООП спешит на помощь, дабы реализовать все наши извращения. Вкратце массив и управление им можно представить очень лаконично, попутно вспоминая про stdObject:

    public function __construct(array $initial_value)
    {
        parent::__construct($initial_value);
    }
    
    public function __set(string $name, $value)
    {
        $this->value[$name] = $value;
    }
    
    public function __get($name)
    {
        return $this->value[$name] ?? null;
    }

И немного излюбленных фокусов с $this:

    public function merge(array $array)
    {
        $this->value = array_merge($this->value, $array);
        return $this;
    }

Как и ожидается, с этим можно теперь сделать всякое:

    $array->merge(['Hello' => 'World'])->merge(['Something else' => 'esle gnihtemoS']);
    $array->var = 'val';

Заключение


Да, можно было сделать намного больше, но тогда статья превратилась бы в справочник по алиасам. Её задача была в том, чтобы пофантазировать и подать идею, а заниматься полноценной реализацией вряд ли имеет смысл. Таковы уж реалии языка, если не сделают подобного подхода на уровне ядра — работать будет криво и косо. Например, мы наш «тип данных» не сможем даже передать во внутреннюю функцию. А уж сколько сие хозяйство сожрёт памяти — страшно даже представлять.

Но если всё же есть желание дополнить или даже форкнуть — милости прошу на GitHub, туда запушены все эти примеры.

Комментарии (27)


  1. Shtucer
    06.05.2019 12:10

    1. mikechips Автор
      06.05.2019 12:11

      Вы так говорите, будто бы это что-то плохое



    1. mikechips Автор
      06.05.2019 12:17

      А вообще — на этой странице вики всё же немного не то. Там скорее про произвольные объекты, а не про обёртывание стандартных типов данных в объекты


      1. Shtucer
        06.05.2019 13:43

        Я не понимаю, почему вы не понимаете, что это одно и тоже. Вернее даже, "обёртывание стандартных типов данных в объекты" это частный случай "про произвольные объекты". А чтобы вам не было скучно, и поскольку вам теперь известен термин "Fluent Interface", можете помучить поисковик Хабра запросами типа "php fluent". Потому что в заголовке у вас про одно, а в статье — про другое. Почему? Потому что у вас есть только __toString и… да и всё. Никаких __toBool или __toArray… и "И о ура! Теперь наш объект-строку можно передавать в те функции, которые требуют строку! " и всё, на этом магия и заканчивается. И всё что осталось от статьи — fluent interface. Вот вы и остались со своими "произвольными объектами", у которых есть метод getValue(ой, простите, getReal, т.е. getNative), и ещё некоторые методы, которые "самое сладкое" возвращают $this. Кстати, это сладкое не такое уж и сладкое, за исключением парочки случаев.


  1. Tatikoma
    06.05.2019 12:39

    Именование getReal немного сбило с толку (real больше ассоциируется с вещественным типом данных), кажется оно должно называться getNative.


    1. mikechips Автор
      06.05.2019 12:40

      Да, вы правы, в частности выходцы из Pascal будут в культурном шоке. Поправил в статье.


      1. MrMYSTIC
        06.05.2019 13:41

        В репозитории тоже стоило бы исправить.


  1. Tatikoma
    06.05.2019 12:42

    getReal на картинке осталось. Нужен новый скриншот, иначе сбивает ещё больше с толку… (промазал с веткой комментариев)


    1. Tatikoma
      06.05.2019 12:55

      Кстати про картинку.
      Что за костыль в виде var_dump($array->getReal())?
      Должно быть:
      $array->var_dump(), или скорее $array->dump();


  1. serginhold
    06.05.2019 12:43

    перед открытием статьи ожидаешь раскрытие темы ValueObject, типичные проблемы, обработка ошибок,
    а все свелось к тому что js-программист в php полез.


    Для строки как минимум должно быть еще одно свойство с кодировкой, иначе все эти $this->{method} работают только с кодировкой из настроек сервера.


    // class TypeBool extends Base
    public function __toString(): string
    {
    return $this->value ? "true" : "false";
    }

    и не надо так делать, я не представляю где это даже примерно может потребоваться


    1. Tatikoma
      06.05.2019 12:49

      $bool = new TypeBool(true);
      $sql = "INSERT INTO tableName VALUES(" . (string)$bool . ")";

      PS: На правах шутки, разумеется.


  1. abyrvalg
    06.05.2019 13:26

    К сожалению, пока в php не появится перегрузка операторов, всё это не более, чем баловство.


    1. mikechips Автор
      06.05.2019 13:32

      С перегрузкой вообще бы новая эра пришла.


      А пример на большее, чем баловство и не претендует. Ресурсы не позволяют


  1. dvmedvedev
    06.05.2019 13:29

    В данном случае объекты иммутабельными делать надо.


    1. evgwed
      06.05.2019 16:18

      В php нет поддержки иммутабельности из коробки, но можно делать что-то подобное: habr.com/ru/company/mailru/blog/301004


  1. Samouvazhektra
    06.05.2019 16:22

    И совсем ни разу не древние, и причем иммутабельные
    https://github.com/voku/Stringy/


    https://github.com/tightenco/collect


    https://github.com/markrogoyski/math-php


    https://github.com/moneyphp/money


    1. Tatikoma
      06.05.2019 18:32

      math-php напомнил:
      github.com/Herzult/SimplePHPEasyPlus


  1. vdem
    06.05.2019 20:24

    Можно еще извратиться и сделать __call и __invoke как-то так:

    class Value
    {
        private $value;
    
        public function __construct($value)
        {
            $this->value = $value;
        }
    
        public function __call(string $name, array $args)
        {
            return new self(call_user_func_array($name, array_merge([$this->value], $args)));
        }
    
        public function __invoke()
        {
            return $this->value;
        }
    }
    

    И тогда
    var_dump(new Value('Abra Cadabra'))->strtolower()->ucfirst()()); // string(12) "Abra cadabra"
    

    Так, чисто по приколу. Не надо объявлять каждый метод, соответствующий функции в PHP. Ну и вроде как иммутабельно.
    :D

    P.S. А, не сработает с некоторыми функциями, которые самый главный аргумент (собственно строку или другой тип) принимают не первым значением, типа explode.

    P.P.S. Так чуть короче.
        public function __call()
        {
            return new self(call_user_func_array($name, func_get_args()));
        }
    


    1. vdem
      07.05.2019 01:22

          public function __call()
          {
              return new self(call_user_func_array($name, func_get_args()));
          }
      

      Упс, ошибка. Первый вариант рабочий должен быть.


      1. mikechips Автор
        07.05.2019 05:55

        О, я так тоже делал.


        Чует душа, если бы такое предложил в статье — карма бы уже утонула


  1. Compolomus
    07.05.2019 00:02

    Помоему рано или поздно каждый в своём парке велосипедов изобретает подобное)))
    Кстати статей про магию в php тут тоже вагон и маленькая тележка.


  1. VolCh
    07.05.2019 08:12

    Для book toString несовместима с нативным фильтром. И serialize надо переопределить, и...


    1. mikechips Автор
      07.05.2019 09:14

      … и весь язык переписать :(


  1. ideological
    07.05.2019 09:21

    Тут самое время пропиарить Python :)
    Сфер применения больше.
    Заодно код будет без фигурных скобок более читабельным.


    1. MrMYSTIC
      07.05.2019 11:38

      Вы ещё скажите, что там долларов не будет!


      1. mikechips Автор
        07.05.2019 16:30

        Лучше доллар в кармане, чем в коде!