Начать стоит с дисклеймера — да, автор что-то страшное (как для мира 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)
serginhold
06.05.2019 12:43перед открытием статьи ожидаешь раскрытие темы ValueObject, типичные проблемы, обработка ошибок,
а все свелось к тому что js-программист в php полез.
Для строки как минимум должно быть еще одно свойство с кодировкой, иначе все эти
$this->{method}
работают только с кодировкой из настроек сервера.
// class TypeBool extends Base public function __toString(): string { return $this->value ? "true" : "false"; }
и не надо так делать, я не представляю где это даже примерно может потребоваться
Tatikoma
06.05.2019 12:49$bool = new TypeBool(true); $sql = "INSERT INTO tableName VALUES(" . (string)$bool . ")";
PS: На правах шутки, разумеется.
dvmedvedev
06.05.2019 13:29В данном случае объекты иммутабельными делать надо.
evgwed
06.05.2019 16:18В php нет поддержки иммутабельности из коробки, но можно делать что-то подобное: habr.com/ru/company/mailru/blog/301004
Samouvazhektra
06.05.2019 16:22И совсем ни разу не древние, и причем иммутабельные
https://github.com/voku/Stringy/
https://github.com/tightenco/collect
https://github.com/markrogoyski/math-php
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())); }
Compolomus
07.05.2019 00:02Помоему рано или поздно каждый в своём парке велосипедов изобретает подобное)))
Кстати статей про магию в php тут тоже вагон и маленькая тележка.
ideological
07.05.2019 09:21Тут самое время пропиарить Python :)
Сфер применения больше.
Заодно код будет без фигурных скобок более читабельным.
Shtucer
И всё это ради
https://en.wikipedia.org/wiki/Fluent_interface#PHP
mikechips Автор
Вы так говорите, будто бы это что-то плохое
OnYourLips
ocramius.github.io/blog/fluent-interfaces-are-evil
www.yegor256.com/2018/03/13/fluent-interfaces.html
mikechips Автор
А вообще — на этой странице вики всё же немного не то. Там скорее про произвольные объекты, а не про обёртывание стандартных типов данных в объекты
Shtucer
Я не понимаю, почему вы не понимаете, что это одно и тоже. Вернее даже, "обёртывание стандартных типов данных в объекты" это частный случай "про произвольные объекты". А чтобы вам не было скучно, и поскольку вам теперь известен термин "Fluent Interface", можете помучить поисковик Хабра запросами типа "php fluent". Потому что в заголовке у вас про одно, а в статье — про другое. Почему? Потому что у вас есть только
__toString
и… да и всё. Никаких__toBool
или__toArray
… и "И о ура! Теперь наш объект-строку можно передавать в те функции, которые требуют строку! " и всё, на этом магия и заканчивается. И всё что осталось от статьи — fluent interface. Вот вы и остались со своими "произвольными объектами", у которых есть метод getValue(ой, простите, getReal, т.е. getNative), и ещё некоторые методы, которые "самое сладкое" возвращают $this. Кстати, это сладкое не такое уж и сладкое, за исключением парочки случаев.