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

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

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

170152b2fa2d4ffab43fb6b1af339435.PNG

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

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

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

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

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

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

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


55c42347a1d7494ea878f01dd2dfa195.png

  • Фирменное наименование: Runn Me!
  • Вендор: Runn
  • GitHub: github.com/RunnMe
  • Composer: packagist.org/packages/runn

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


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

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

Лет шесть назад руководство компании, в которой я тогда работал, поставило задачу: разработать свой собственный фреймворк. Сделать легковесный 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-каркас

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

d905e8471188421890f60543d1c16b5b.jpg

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

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

©


КДПВ © Mart Virkus 2016
Картинка в заключении статьи из гуглопоиска картинок

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

  • 1 мая 2017 в 15:37

    +2

    Я тоже недавно пытаясь разобраться в Yii2 плюнул на это, и написал себе небольшой MVC с нуля.

    А еще ранее, когда только вышел ZF, произошло то же самое, но где тот первый движок — я уже и не помню : D

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

    Так что мысленно поддерживаю вашу инициативу!

    //И да, всегда с трепетным уважением смотрю на людей, сумевших разобраться в Symfony например. У них наверное какой-то особенно огромный мозг, или еще чего такое в этом же духе:)

    • 1 мая 2017 в 15:40 (комментарий был изменён)

      0

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

      Откройте для себя Github))

      Кстати, в чём-то я с вами согласен. Недовольство именно Yii, который пришлось несколько раз использовать, стало одним из поводов делать что-то своё. Мне просто стало интересно — сможем ли мы с ребятами сделать что-то вроде Yii 3, не таща за собой его фатальные архитектурные ошибки?

      Оказалось, что смогли :)

  • 1 мая 2017 в 16:21

    +1

    «Недовольство именно Yii, который пришлось несколько раз использовать, стало одним из поводов делать что-то своё.»
    Допустим, я выбираю фреймворк, на котором реализовать проект. В чем преимущества Runn Me перед тем же Yii2?
    • 1 мая 2017 в 16:26 (комментарий был изменён)

      –2

      Сложно сейчас сформулировать преимущества, когда фреймворк еще в самой ранней стадии сборки. Я планировал раскрывать этот вопрос постепенно, по мере выхода статей и подготовки кода к публикации.

      Если кратко, то:
       — БОльшая архитектурная стройность, например у нас точно не будет никогда $this→breadcrumbs в контроллерах (!)
       — Возможность замены любой части, любого компонента на другой, имеющий аналогичный интерфейс. Например вы можете целиком заменить Renderer с нативного на другой, на базе Twig. Насколько я помню, в Yii это крайне сложно. Например конфиг можно сохранять в файл, в базу или в кэш, просто подставив нужную зависимость. Даже класс приложения (контейнера служб) можно вполне заменить на свой.
       — Нет тяжести поддержки старых версий, сразу ориентируемся на 7.1, поэтому нет таких вещей, как «поведения», которые в общем-то в современном PHP не нужны
       — Мы сразу закладываем возможность работать с несколькими БД, включая даже возможность строить в рамках ORM relations (разумеется lazy load) между разными базами данных

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

      Следите за публикациями!

      • 1 мая 2017 в 17:15

        0

        не будет $this→breadcrumbs в контроллерах (!)

        А хде?
        • 1 мая 2017 в 17:17

          0

          Разумеется в шаблонах :)

          Зачем контроллеру вообще что-то знать о том, как будет отдан ответ пользователю? Контроллер готовит данные, представление — форматирует.

      • 1 мая 2017 в 17:29

        0

        поэтому нет таких вещей, как «поведения», которые в общем-то в современном PHP не нужны

        Как вы заменяете такую конструкцию?


        [
                'class' => TimestampBehavior::className(),
                'attributes' => [
                    ActiveRecord::EVENT_BEFORE_INSERT => ['created_at', 'updated_at'],
                    ActiveRecord::EVENT_BEFORE_UPDATE => ['updated_at'],
                ],
                'value' => function () { return date('Y-m-d H:i:s'); },
        ]
        • 1 мая 2017 в 17:34

          +1

          Никак. Вам нужно срочно убрать это из ORM либо в триггер, либо, что лучше, в конкретный компонент бизнес-процесса.
  • 1 мая 2017 в 16:43

    +1

    Ладно еще коллекции, но какой прок от объекта, которому динамически можно свойств накидывать? Чем вам тогда простой stdClass не подошел? Вы всеравно же не знаете какие в итоге свойства у итогового объекта.
    • 1 мая 2017 в 16:49 (комментарий был изменён)

      0

      Во-первых это chaining и то, что я называю innerCast. Обычный stdClass не умеет выстраивать цепочки свойств на лету и превращать «промежуточные» свойства в объекты этого же класса.

      Во-вторых управляемость. В базовом классе Std, например, можно задать список обязательных свойств и, если они будут отсутствовать при создании объекта — возникнет ошибка.

      Далее перехват присваивания, валидация и санитация данных, которые вы передаете в свойства. Умный конструктор и умный merge (), которые умеют собирать все ошибки валидации в коллекцию ошибок.

      Всё это проще сразу заложить в базовый класс. И потом расширять, при необходимости.

      P.S. Можете почитать код тестов, например https://github.com/RunnMe/Core/blob/master/tests/Core/StdTest.php и https://github.com/RunnMe/Core/blob/master/tests/Core/StdGetSetValidateSanitizeTraitTest.php — там многое написано понятнее, чем я рассказываю))

© Habrahabr.ru