«О нет!», воскликнет читатель, утомлённый разными мини-микро-слим-фреймворками и QueryBuilder-ами и будет прав.
Нет ничего скучнее, чем очередной фреймворк на PHP. Разве что «принципиально новая» CMS или новый дейтинг.
Так зачем же я с упорством, достойным лучшего применения, шагаю по неудобным подводным камням и выставляю на потеху публике суд товарищей своё творение? Заранее зная, что гнев критиков, как мощное цунами обрушится на этот пост и похоронит его на самом днище Хабра?
Не знаю. Как не знал в своё время Колумб, зачем он отплывает от уютных берегов Испании. Надеялся ли он найти путь в Индию? Конечно да. Но не знал точно — доплывёт ли?
Видимо и у программистов на PHP, к которым я вот уже 13 лет себя причисляю, есть такая же внутренняя потребность — выставлять свой код и зажмуривать глаза, ожидая реакции коллег.
Что вас ждет под катом?
- Открытый исходный код, лицензия LGPL
- Код, полностью совместимый с PHP 7.0-7.2
- 100% покрытие юнит-тестами
- Библиотеки, проверенные временем в реальных проектах (и только проклятая прокрастинация мешала мне опубликовать их ранее!)
Ну и, разумеется, история изобретения очередного велосипеда на костыльном приводе фреймворка*!
* вообще говоря это пока еще не фреймворк, а просто набор библиотек, фреймворком он станет чуть позже
- Фирменное наименование: Runn Me!
- Вендор: Runn
- GitHub: github.com/RunnMe
- Composer: packagist.org/packages/runn
Немного истории или Откуда взялась идея «написать еще один фреймворк?»
Да, собственно говоря, ниоткуда. Она всегда была.
Разные интересные проекты, в которых довелось поучаствовать за время программистской карьеры, клали в личную копилку стандартные решения для стандартных же задач — так у меня, как и у любого нормального программиста, скапливалась своя библиотека функций, классов и библиотечек.
Лет шесть назад руководство компании, в которой я тогда работал, поставило задачу: разработать свой собственный фреймворк. Сделать легковесный MVC-каркас, взяв только самое необходимое, добавить к нему специфичные библиотеки предметной области (поверьте — очень специфичные!) и собрать некое универсальное решение. Решение, надо отметить, получилось, но специфичность предметной области не позволила ему стать массовым — код не публиковался, продавались инсталляции на площадку клиента. А жаль. Некоторые вещи действительно опережали своё время: достаточно сказать, что пусть примитивное, но всё-таки довольно похожее подобие composer мы с командой сделали тогда совершенно самостоятельно и немного раньше, чем появился, собственно стабильный публичный composer :)
Благодаря этому опыту мне довелось изучить практически все существовавшие тогда в экосистеме PHP фреймворки. Попутно произошло еще одно событие, очередной «переход количества в качество» — я стал преподавать программирование. Сначала в одной известной онлайн-школе, потом сосредоточился на развитии своего собственного сервиса. Стал «обрастать» методиками преподавания, учебными материалами и, конечно же, студентами. В этой тусовке и возникла идея некоего «учебного фреймворка», намеренно упрощенного для понимания начинающими, но при этом позволяющего всё-таки успешно разрабатывать несложные веб-приложения в соответствии с современными стандартами и тенденциями.
Три года назад, как реализация этой идеи «учебного фреймворка», родился небольшой MVC-фреймворк под названием «T4»*. В названии нет ничего особенного, просто сокращение от «Технологический макет, версия 4». Думаю понятно, что предыдущие три версии вышли неудачными и только с четвертой попытки нам, вместе с тогдашними моими студентами, удалось создать что-то действительно интересное.
* позже я узнал, что так в третьем рейхе называлась программа по стерилизации и умерщвлению неизлечимо больных людей… конечно же сразу встал вопрос о смене названия
T4 благополучно развивался и рос, стал известным, как говорится, «в узких кругах» (очень узких), на нём был сделан ряд довольно крупных проектов, но росло и внутреннее недовольство этим решением.
В начале этого года я окончательно созрел для переформатирования накопившегося кода. Вместе с группой единомышленников, которые тоже активно использовали T4, мы приняли ряд базовых принципов построения нового фреймворка:
- Делаем его слабосвязанным набором библиотек, так, чтобы каждую либу можно было подключить и использовать отдельно.
- Стараемся сохранять здоровый минимализм там, где это возможно
- Сам каркас для веб- и консольных приложений — тоже одна из библиотек, тем самым мы избегаем монолитности.
- Стараемся не изобретать велосипеды и максимально сохраняем те подходы и тот код, которые уже зарекомендовали себя в T4.
- Отказываемся от поддержки устаревших версий PHP, пишем код под самую актуальную версию.
- Стараемся делать код максимально гибким. Если можно — вместо классов и наследования используем интерфейсы, трейты и композицию кода, оставляя пользователям фреймворка возможность заменить эталонную реализацию любого компонента своей.
- Покрываем код тестами, добиваясь 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. И каждый второй эту идею реализует. Не стали исключением и я с моей командой: ваше знакомство с библиотекой RunnCore я хочу начать с рассказа о концепции ObjectAsArray.
Делай раз: определи интерфейс, который позволит тебе кастить твой объект к массиву и обратно: массив превращать в объект, не забыв в этом интерфейсе пару полезных методов (merge() для слияния объекта с внешними данными и рекурсивный кастинг к массиву)
github.com/RunnMe/Core/blob/master/src/Core/ArrayCastingInterface.php
namespace RunnCore;
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 RunnCore;
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 RunnCoreObjectAsArrayInterface
{
use RunnCoreObjectAsArrayTrait;
}
$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 RunnCoreObjectAsArrayInterface
{
use RunnCoreObjectAsArrayTrait;
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 RunnCoreObjectAsArrayInterface
{
use RunnCoreObjectAsArrayTrait;
}
$obj = new someObjAsArray;
assert(false === isset($obj['foo']));
assert(null === $obj['foo']);
$obj['foo'] = null;
assert(true === isset($obj['foo']));
assert(null === $obj['foo']);
И зачем это всё?
Ну как же! Всё, о чем я рассказывал выше — это только начало. От интерфейса RunnCoreObjectAsArrayInterface наследуются другие интерфейсы и имплементируют классы, дающие жизнь двум «веткам классов»: Collection и Std.
Коллекции
Коллекции в Runn Me! — это объекты-массивы, снабженные большим количеством дополнительных полезных методов:
namespace RunnCore;
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, так и готовый к использованию (или наследованию) класс RunnCoreCollection, добавляющий к реализации методов из трейта удобный конструктор.
С использованием коллекций становится возможным писать примерно такой код:
$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!'),
),
]
*/
Что важно знать о коллекциях?
- Большинство методов не изменяют исходную коллекцию, а возвращают новую.
- Большинство методов не гарантирует сохранение ключей элементов.
- Наилучшее применение коллекций — хранение в них множеств однородных или подобных объектов.
Типизированные коллекции
Кроме «обычных» коллекций в библиотеку RunnCore включен интересный инструмент, позволяющий полностью контролировать объекты, которые могут содержаться в коллекции. Это типизированные коллекции.
Всё очень и очень просто:
class UsersCollection extends RunnCoreTypedCollection
{
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 RunnCore;
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 )
Делай три: собери из «запчастей» класс, опирающийся на StdGetSteInterface с множеством дополнительных возможностей.
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
Картинка в заключении статьи из гуглопоиска картинок
Автор: Альберт Степанцев