1 May 2017

Уточка говорит «кря-кря», коровка говорит «му-му», «Runn Me!» — говорит нам очередной фреймворк* на PHP. Часть 1

PHPSymfonyZend FrameworkYiiLaravel
«О нет!», воскликнет читатель, утомлённый разными мини-микро-слим-фреймворками и QueryBuilder-ами и будет прав.

Нет ничего скучнее, чем очередной фреймворк на PHP. Разве что «принципиально новая» CMS или новый дейтинг.



Так зачем же я с упорством, достойным лучшего применения, шагаю по неудобным подводным камням и выставляю на потеху публике суд товарищей своё творение? Заранее зная, что гнев критиков, как мощное цунами обрушится на этот пост и похоронит его на самом днище Хабра?

Не знаю. Как не знал в своё время Колумб, зачем он отплывает от уютных берегов Испании. Надеялся ли он найти путь в Индию? Конечно да. Но не знал точно — доплывёт ли?

Видимо и у программистов на PHP, к которым я вот уже 13 лет себя причисляю, есть такая же внутренняя потребность — выставлять свой код и зажмуривать глаза, ожидая реакции коллег.

Что вас ждет под катом?

  • Открытый исходный код, лицензия LGPL
  • Код, полностью совместимый с PHP 7.0-7.2
  • 100% покрытие юнит-тестами
  • Библиотеки, проверенные временем в реальных проектах (и только проклятая прокрастинация мешала мне опубликовать их ранее!)

Ну и, разумеется, история изобретения очередного велосипеда на костыльном приводе фреймворка*!

* вообще говоря это пока еще не фреймворк, а просто набор библиотек, фреймворком он станет чуть позже



Немного истории или Откуда взялась идея «написать еще один фреймворк?»


Да, собственно говоря, ниоткуда. Она всегда была.

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

Лет шесть назад руководство компании, в которой я тогда работал, поставило задачу: разработать свой собственный фреймворк. Сделать легковесный MVC-каркас, взяв только самое необходимое, добавить к нему специфичные библиотеки предметной области (поверьте — очень специфичные!) и собрать некое универсальное решение. Решение, надо отметить, получилось, но специфичность предметной области не позволила ему стать массовым — код не публиковался, продавались инсталляции на площадку клиента. А жаль. Некоторые вещи действительно опережали своё время: достаточно сказать, что пусть примитивное, но всё-таки довольно похожее подобие composer мы с командой сделали тогда совершенно самостоятельно и немного раньше, чем появился, собственно стабильный публичный composer :)

Благодаря этому опыту мне довелось изучить практически все существовавшие тогда в экосистеме PHP фреймворки. Попутно произошло еще одно событие, очередной «переход количества в качество» — я стал преподавать программирование. Сначала в одной известной онлайн-школе, потом сосредоточился на развитии своего собственного сервиса. Стал «обрастать» методиками преподавания, учебными материалами и, конечно же, студентами. В этой тусовке и возникла идея некоего «учебного фреймворка», намеренно упрощенного для понимания начинающими, но при этом позволяющего всё-таки успешно разрабатывать несложные веб-приложения в соответствии с современными стандартами и тенденциями.

Три года назад, как реализация этой идеи «учебного фреймворка», родился небольшой MVC-фреймворк под названием «T4»*. В названии нет ничего особенного, просто сокращение от «Технологический макет, версия 4». Думаю понятно, что предыдущие три версии вышли неудачными и только с четвертой попытки нам, вместе с тогдашними моими студентами, удалось создать что-то действительно интересное.

* позже я узнал, что так в третьем рейхе называлась программа по стерилизации и умерщвлению неизлечимо больных людей… конечно же сразу встал вопрос о смене названия

T4 благополучно развивался и рос, стал известным, как говорится, «в узких кругах» (очень узких), на нём был сделан ряд довольно крупных проектов, но росло и внутреннее недовольство этим решением.

В начале этого года я окончательно созрел для переформатирования накопившегося кода. Вместе с группой единомышленников, которые тоже активно использовали T4, мы приняли ряд базовых принципов построения нового фреймворка:

  1. Делаем его слабосвязанным набором библиотек, так, чтобы каждую либу можно было подключить и использовать отдельно.
  2. Стараемся сохранять здоровый минимализм там, где это возможно
  3. Сам каркас для веб- и консольных приложений — тоже одна из библиотек, тем самым мы избегаем монолитности.
  4. Стараемся не изобретать велосипеды и максимально сохраняем те подходы и тот код, которые уже зарекомендовали себя в T4.
  5. Отказываемся от поддержки устаревших версий PHP, пишем код под самую актуальную версию.
  6. Стараемся делать код максимально гибким. Если можно — вместо классов и наследования используем интерфейсы, трейты и композицию кода, оставляя пользователям фреймворка возможность заменить эталонную реализацию любого компонента своей.
  7. Покрываем код тестами, добиваясь 100% покрытия.

Так родился проект, который сначала назвали «Running.FM», а потом окончательно уже переименовали в «Runn Me!»

Именно его я сегодня и представляю.

Кстати, слово «runn» сконструировано искусственно: с одной стороны чтобы быть понятным всем и вызывать ассоциации с «run», с другой — чтобы не совпадало ни с одним из словарных слов. Мне вообще нравится буквосочетание «run»: я еще в RunCMS в своё время успел поучаствовать :)

В данный момент проект «Runn Me!» находится в середине пути — какие-то библиотеки уже можно применять в продакшне, какие-то в процессе переноса из старых проектов и рефакторинга, а какие-то мы еще не начали переносить.

В начале было Core


Уместить в один пост рассказ о каждой библиотеке проекта «Runn Me!» невозможно: их много, хочется подробно поведать о каждой, ну и к тому же это живой проект, в котором всё изменяется к лучшему буквально ежедневно :)

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

  • Назначение: реализация базовых классов фреймворка
  • GitHub: github.com/RunnMe/Core
  • Composer: github.com/RunnMe/Core
  • Установка: командой composer require runn/core
  • Версии: как и в любой другой библиотеке проекта «Runn Me!», поддерживаются три версии, соответствующие предыдущей, актуальной и будущей версиям PHP:
    7.0.*, 7.1.* и 7.2.*

Массив? Объект? Или всё вместе?


Благодатная идея объекта, состоящего из произвольных свойств, которые можно создавать и удалять «на лету», как элементы в массиве, приходит в голову каждому программисту на PHP. И каждый второй эту идею реализует. Не стали исключением и я с моей командой: ваше знакомство с библиотекой Runn\Core я хочу начать с рассказа о концепции ObjectAsArray.

Делай раз: определи интерфейс, который позволит тебе кастить твой объект к массиву и обратно: массив превращать в объект, не забыв в этом интерфейсе пару полезных методов (merge() для слияния объекта с внешними данными и рекурсивный кастинг к массиву)

github.com/RunnMe/Core/blob/master/src/Core/ArrayCastingInterface.php
namespace Runn\Core;

interface ArrayCastingInterface
{
    public function fromArray(iterable $data);
    public function merge(iterable $data);
    public function toArray(): array;
    public function toArrayRecursive(): array;
}

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

github.com/RunnMe/Core/blob/master/src/Core/ObjectAsArrayInterface.php
namespace Runn\Core;

interface ObjectAsArrayInterface
  extends \ArrayAccess, \Countable, \Iterator, ArrayCastingInterface, HasInnerCastingInterface, \Serializable, \JsonSerializable
{
  ...
}

Делай три: напиши трейт, который станет эталонной реализацией мегаинтерфейса. См. github.com/RunnMe/Core/blob/master/src/Core/ObjectAsArrayTrait.php

В результате мы получили полноценную реализацию «объекта-как-массива». Использование интерфейса ObjectAsArrayInterface и трейта ObjectAsArrayTrait позволяет делать нам примерно так:

class someObjAsArray implements \Runn\Core\ObjectAsArrayInterface 
{
  use \Runn\Core\ObjectAsArrayTrait;
}

$obj = (new someObjAsArray)->fromArray([1 => 'foo', 2 => 'bar']);
$obj[] = 'baz';
$obj[4] = 'bla';

assert(4 === count($obj));
assert([1 => 'foo', 2 => 'bar', 3 => 'baz', 4 => 'bla'] === $obj->values());

foreach ($obj as $key => $val) {
  // ...
}

assert('{"1":"foo","2":"bar","3":"baz","4":"bla"}' === json_encode($obj));

Кроме базовых возможностей в ObjectAsArrayTrait реализована возможность перехвата присваивания и чтения «элементов объекта-массива» с помощью кастомных сеттеров-геттеров, этакий задел для будущих классов:

class customObjAsArray implements \Runn\Core\ObjectAsArrayInterface 
{

  use \Runn\Core\ObjectAsArrayTrait;

  protected function getFoo() 
  {
    return 42;
  }

  protected function setBar($value)
  {
    echo $value;
  }

}

$obj = new customObjAsArray;
assert(42 === $obj['foo']);

$obj['bar'] = 13; // выводит 13, присваивания не происходит

Важно: null is set!


Да, элемент объекта-массива, чье значение null, считается определенным.

Это решение вызвало немало споров, но всё-таки было принято. Поверьте, на то есть серьезные причины, о которых будет рассказано дальше, в повествовании о библиотеке ORM:

class someObjAsArray implements \Runn\Core\ObjectAsArrayInterface 
{
  use \Runn\Core\ObjectAsArrayTrait;
}

$obj = new someObjAsArray;
assert(false === isset($obj['foo']));
assert(null === $obj['foo']);

$obj['foo'] = null;
assert(true === isset($obj['foo']));
assert(null === $obj['foo']);

И зачем это всё?


Ну как же! Всё, о чем я рассказывал выше — это только начало. От интерфейса \Runn\Core\ObjectAsArrayInterface наследуются другие интерфейсы и имплементируют классы, дающие жизнь двум «веткам классов»: Collection и Std.

Коллекции


Коллекции в Runn Me! — это объекты-массивы, снабженные большим количеством дополнительных полезных методов:

Тут можно увидеть их все
namespace Runn\Core;

interface CollectionInterface
    extends ObjectAsArrayInterface
{
    public function add($value);
    public function prepend($value);
    public function append($value);
    public function slice(int $offset, int $length = null);
    public function first();
    public function last();
    public function existsElementByAttributes(iterable $attributes);
    public function findAllByAttributes(iterable $attributes);
    public function findByAttributes(iterable $attributes);
    public function asort();
    public function ksort();
    public function uasort(callable $callback);
    public function uksort(callable $callback);
    public function natsort();
    public function natcasesort();
    public function sort(callable $callback);
    public function reverse();
    public function map(callable $callback);
    public function filter(callable $callback);
    public function reduce($start, callable $callback);
    public function collect($what);
    public function group($by);
    public function __call(string $method, array $params = []);
}


Разумеется, сразу же в распоряжении разработчика имеется как эталонная реализация этого интерфейса в виде трейта CollectionTrait, так и готовый к использованию (или наследованию) класс \Runn\Core\Collection, добавляющий к реализации методов из трейта удобный конструктор.

С использованием коллекций становится возможным писать примерно такой код:

$collection = new Collection([1 => 'foo', 2 => 'bar', 3 => 'baz']);
$collection->prepend('bla');

$collection
  ->reverse()
  ->map(function ($x) { 
    return $x . '!'; 
  })
  ->group(function ($x) {
    return substr($x, 0, 1);
  });

/*
получится что-то вроде
[
  'b' => new Collection([0 => 'baz!', 1 => 'bar!', 2 => 'bla!']),
  'f' => new Collection([0 => 'foo!'),
),
]
*/

Что важно знать о коллекциях?

  1. Большинство методов не изменяют исходную коллекцию, а возвращают новую.
  2. Большинство методов не гарантирует сохранение ключей элементов.
  3. Наилучшее применение коллекций — хранение в них множеств однородных или подобных объектов.

Типизированные коллекции


Кроме «обычных» коллекций в библиотеку Runn\Core включен интересный инструмент, позволяющий полностью контролировать объекты, которые могут содержаться в коллекции. Это типизированные коллекции.

Всё очень и очень просто:

class UsersCollection extends \Runn\Core\TypedCollection 
{
  public static function getType()
  {
    return User::class; // тут может быть и название скалярного типа, кстати
  }  
}

$collection = new UsersCollection;

$collection[] = 42; // Exception: Typed collection type mismatch
$collection->prepend(new stdClass); // Exception: Typed collection type mismatch

$collection->append(new User); // Success!

Std


Вторая «ветка» кода, в чём-то противоположная коллекциям, называется «Стандартный объект». Строится он также пошагово:

Делай раз: определи интерфейс для «магии».

namespace Runn\Core;

interface StdGetSetInterface
{
    public function __isset($key);
    public function __unset($key);
    public function __get($key);
    public function __set($key, $val);
}

Делай два: добавь ему стандартную реализацию (см. github.com/RunnMe/Core/blob/master/src/Core/StdGetSetTrait.php )

Делай три: собери из «запчастей» класс, опирающийся на StdGetSetInterface с множеством дополнительных возможностей. github.com/RunnMe/Core/blob/master/src/Core/Std.php

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

$obj = new Std(['foo' => 42, 'bar' => 'bla-bla', 'baz' => [1, 2, 3]]);
assert(3 === count($obj));

assert(42 === $obj->foo);
assert(42 === $obj['foo']);

assert(Std::class == get_class($obj->baz));
assert([1, 2, 3] === $obj->baz->values());

// о, да, реализация вот этой штуки весьма монструозна:
$obj = new Std;
$obj->foo->bar = 42;
assert(Std::class === get_class($obj->foo));
assert(42 === $obj->foo->bar);

Разумеется, «умения» класса Std не исчерпываются chaining-ом, доступом к свойствам, как к элементам массива и наоборот, кастингом к самому классу. Он умеет гораздо больше: валидировать и очищать данные, отслеживать обязательные к заполнению свойства и т.д. Но об этом позже, в других статьях цикла.

А дальше?


Всё только начинается! Впереди нас ждут рассказы о:

  • Мультиисключениях
  • Валидаторах и санитайзерах
  • О хранилищах, сериализаторах и конфигах
  • О реализации Value Objects и Entities
  • Об HTML и представлении форм на стороне сервера
  • О собственной библиотеке DBAL, включая, конечно же, QueryBuilder!
  • Библиотека ORM
  • и как финал — MVC-каркас

Но это всё в будущих статьях. А пока что с праздником, товарищи! Мир, труд, код! :)



P.S. Детального плана со сроками у нас нет, как нет и желания успеть к какой-то очередной дате. Поэтому не спрашивайте «когда». По мере готовности отдельных библиотек будут выходить статьи о них.

P.P.S. С благодарностью приму сведения об ошибках или опечатках в личные сообщения.

©


КДПВ (с) Mart Virkus 2016
Картинка в заключении статьи из гуглопоиска картинок
Tags: php php 7 framework library lgpl composer github
Hubs: PHP Symfony Zend Framework Yii Laravel
+18
19.2k 72
Comments 226
Ads
Top of the day