
Под катом делюсь обзором своего самописного PHP-фреймворка Gy — попытки сделать легковесного «убийцу» Битрикса весом 350 Кб. Расскажу, как я реализовал вызов компонентов, зачем написал кастомный SQL-движок на текстовых файлах PhpFileSql.
Костыли, велосипеды, 3 года разработки по выходным, 315 коммитов, 14232 строки кода, поддержка практически всех версий PHP и ровно 0 пользователей.
Зачем, цели и какая связь с Битриксом
Проект начался в 2018 году, тогда же были сделаны первые коммиты. В то время на основной работе я взаимодействовал исключительно с 1С-Битрикс (бэкенд-разработка), причем со старым его ядром. Новое ядро D7 тогда уже вышло, но мне не попадалось ни одного живого проекта, где бы оно использовалось.
Параллельно у меня периодически появлялась подработка — то одному знакомому, то другому нужно было сделать простенький сайт на пару страниц с информацией и удобной админкой для редактирования данных. На Битриксе я мог собрать такое очень быстро. Сам компонентный подход и код API казались мне простыми в освоении и использовании. Но было жирное «но», Битрикс стоил денег и был гигантского размера, и чтобы купить лицензию, нужно было заморачиваться через партнеров.
Примерно тогда же на Хабре под какой-то очередной статьей, яростно хающей Битрикс, я наткнулся на комментарий в духе: «А почему никто не сделалает бесплатный аналог Битрикса?». И я подумал, а почему бы и нет?
Изначально цель была грандиозной — написать PHP-фреймворк с похожими вызовами компонентов и архитектурой. Идея заключалась в том, чтобы можно было в будущем легко переносить проекты с Битрикса на мой движок, практически не меняя код самого сайта. Конечно, Битрикс огромный, в нем очень много функционала, поэтому написать его полный аналог в одиночку было нереально. Тогда я сузил задачу, сделать легковесный инструмент, на котором можно с той же «битриксовой» легкостью собирать маленькие сайты (лендинги или информационные визитки) и удобно управлять данными через админку.
Второй важной целью было желание иметь собственный пет-проект. Место, где я сам принимаю архитектурные решения, а не согласовываю каждую кнопочку и строчку кода с менеджерами. Полная свобода, делаю то, что захочу и как захочу.
Ну и третья цель — это изучение веб-программирования и вызов самому себе. Смогу ли я в одиночку написать что-то подобное и реально работающее? Мне нужен был собственный полигон для испытаний, чтобы пробовать новые подходы, внедрять изученные технологии и тестировать безумные идеи на практике.
Разработка и отрыв от Битрикса
Так как в то время я плотно работал с Битриксом, у меня не было особого выбора или альтернативного опыта, на который можно было опереться. Я каждый день видел вызовы компонентов, подключение пролога, работал со старым API. При этом внутрь самого ядра коммерческой CMS я никогда не заглядывал и не изучал, как там всё устроено под капотом.
Видя только внешнюю, интерфейсную часть, я просто предположил, как эта магия могла бы работать изнутри. И попытался воссоздать похожую логику с нуля, так появились механизмы подключения ядра и вызова компонентов.
Скорее всего, внутреннее устройство моего фреймворка кардинально отличается от Битрикса, а в коде скрываются те еще архитектурные костыли. Но главное — система работает.
Некоторые вещи в итоге получились похожими на оригинал, но многие концепции я пересмотрел и сделал совершенно иначе. Например, у меня появилось что-то вроде модели для работы с данными, что в старом Битриксе не было очевидным. Поэтому любые совпадения архитектуры «под капотом» — это чистая случайность. Да и вряд ли они там найдутся.
Особенности процесса разработки
В режиме активного кодинга проект находился около 3 лет, начиная с 2018 года. После этого наступило относительное затишье, занялся более крупным проектом в физическом мире, требующий кучу моего времени. Я выпускал лишь небольшие доработки с одной целью, доделать необходимое для целостности, что бы была возможность минимальные сайты сделать полностью.
Фреймворк создавался по вечерам после основной работы, на выходных и даже во время отпусков. Порой код писался откровенно криво и на скорую руку. Мне хотелось поскорее закончить текущий тяжелый кусок логики, чтобы со спокойной душой переключиться на следующую интересную задачу.
Поэтому код фреймворка далек от идеала. Однако в процессе разработки я постоянно учился. Как только я узнавал какую-то новую технологию или концепцию, я тут же шел внедрять её в Gy. Так в проекте появились стандарты PSR, документирование кода, слой моделей для работы с данными, кастомные механизмы для БД и многое другое. Каждое такое внедрение приводило к масштабному рефакторингу, когда приходилось пересматривать и переписывать вообще все файлы в репозитории.
Параллельно с кодом я старался развивать и экосистему вокруг него. Я вел Wiki прямо на GitHub ( https://github.com/ssv32/gy/wiki ), развернул отдельный сайт ( https://asisg.ru/projects/gy/ ) с документацией и даже записал несколько видеоруководств по работе с фреймворком (https://rutube.ru/plst/466964?r=wd https://www.youtube.com/watch?v=SqHj6PZ62OM&list=PLaa5NmVx2nGjhKgdeBImncsf\_oIRcsZ2l). Сейчас за те записи мне стыдно — они получились довольно медленными, так что смотреть их стоит разве что на скорости х2. Но главное, что они есть, и по ним действительно можно понять, как развернуть сайт на моем движке.
Весь код можно увидеть тут https://github.com/ssv32/gy опубликован под лицензией GPL-3.0. Было добавлено 37 441 строк и убрано 23 209 строк, получается весь проект это 14 232 строк, но это не только php а html и css может и отступы.
Анатомия фреймворка Gy. Что внутри?
Весь фреймворк вместе с административной панелью и демо-данными весит всего 350 Кб. Для сравнения, это вес одной картинки среднего качества. При этом движок успешно тестировался на самых разных версиях PHP — от древней 5.6 до 7.2 (перед публикацией статьи протестировал на 8.3 и закрыл пару багов). Такая легковесность позволяет запускать систему буквально на «утюге» или самом дешевом копеечном хостинге.
Структура папок и архитектура
Структура файлов
gy
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── gy
│ ├── 404.php
│ ├── admin
│ │ ├── add-user.php
│ │ ├── ajax.php
│ │ ...
│ │ ├── header-admin.php
│ │ ├── index.php
│ │ ├── modules.php
│ │ ├── options.php
│ │ └── users.php
│ ├── classes
│ │ ├── Gy
│ │ │ ├── Core
│ │ │ │ ├── AbstractClasses
│ │ │ │ │ ├── Cache.php
│ │ │ │ │ └── Db.php
│ │ │ │ ├── App.php
│ │ │ │ ├── Cache
│ │ │ │ │ └── CacheFiles.php
│ │ │ │ ├── Capcha.php
│ │ │ │ ├── Component
│ │ │ │ │ ├── Component.php
│ │ │ │ │ └── Mvc
│ │ │ │ │ ├── Controller.php
│ │ │ │ │ ├── Model.php
│ │ │ │ │ └── Template.php
│ │ │ │ ├── Crypto.php
│ │ │ │ ├── Db
│ │ │ │ │ ├── MySql.php
│ │ │ │ │ ├── PgSql.php
│ │ │ │ │ └── PhpFileSqlClientForGy.php
│ │ │ │ ├── Image.php
│ │ │ │ ├── Lang.php
│ │ │ │ ├── Module.php
│ │ │ │ ├── Security.php
│ │ │ │ ├── Url.php
│ │ │ │ └── User
│ │ │ │ ├── AccessUserGroup.php
│ │ │ │ ├── GeneralUsersPropertys.php
│ │ │ │ └── User.php
│ │ │ └── Tools
│ │ │ └── Pagination.php
│ │ └── Psr
│ │ ├── Log
│ │ │ ├── AbstractLogger.php
│ │ │ ├── Logger
│ │ │ │ ├── FileRoute.php
│ │ │ │ ├── Logger.php
│ │ │ │ └── Route.php
│ │ │ ├── LoggerInterface.php
│ │ │ └── LogLevel.php
│ │ └── Psr4
│ │ └── Psr4AutoloaderClass.php
│ ├── component
│ │ ├── add_user
│ │ │ ├── componentInfo.php
│ │ │ ├── controller.php
│ │ │ ├── lang_componentInfo.php
│ │ │ ├── lang_controller.php
│ │ │ ├── model.php
│ │ │ └── teplates
│ │ │ └── 0
│ │ │ ├── lang_template.php
│ │ │ └── template.php
│ │ ├── admin
│ │ │ ├── componentInfo.php
│ │ │ ├── controller.php
│ │ │ ├── lang_componentInfo.php
│ │ │ ├── lang_controller.php
│ │ │ └── teplates
│ │ │ └── 0
│ │ │ ├── lang_template.php
│ │ │ └── template.php
│ │ ├── admin-button-public-site
│ │ │ ├── componentInfo.php
│ │ │ ├── controller.php
│ │ │ ├── lang_componentInfo.php
│ │ │ └── teplates
│ │ │ └── 0
│ │ │ ├── lang_template.php
│ │ │ ├── style.css
│ │ │ └── template.php
│ │ ├── capcha
│ │ │ ├── componentInfo.php
│ │ │ ├── controller.php
│ │ │ ├── lang_componentInfo.php
│ │ │ └── teplates
│ │ │ └── 0
│ │ │ ├── lang_template.php
│ │ │ └── template.php
│ │ ...
│ │ └── users_group_manager
│ │ ├── componentInfo.php
│ │ ├── controller.php
│ │ ├── lang_componentInfo.php
│ │ ├── lang_controller.php
│ │ └── teplates
│ │ └── 0
│ │ ├── lang_template.php
│ │ └── template.php
│ ├── config
│ │ └── gy_config.php
│ ├── fonts
│ │ └── 18018.otf
│ ├── gy.php
│ ├── images
│ │ ├── fon.png
│ │ └── gy-icons.jpg
│ ├── index.php
│ ├── install
│ │ ├── consoleInstallOptions.php
│ │ ├── installDataBaseTable.php
│ │ └── installDemoSite1.php
│ ├── js
│ │ └── main.js
│ ├── lang
│ │ ├── lang_404.php
│ │ ├── lang_component.php
│ │ └── lang_header-admin.php
│ ├── modules
│ │ ├── containerdata
│ │ │ ├── admin
│ │ │ │ ├── container-data-add.php
│ │ │ │ ├── container-data-edit.php
│ │ │ │ ├── container-data-element-list.php
│ │ │ │ ├── container-data-element-property.php
│ │ │ │ ├── container-data.php
│ │ │ │ └── container-data-property-edit.php
│ │ │ ├── classes
│ │ │ │ └── ContainerData.php
│ │ │ ├── component
│ │ │ │ ├── containerdata
│ │ │ │ │ ├── componentInfo.php
│ │ │ │ │ ├── controller.php
│ │ │ │ │ ├── lang_componentInfo.php
│ │ │ │ │ └── teplates
│ │ │ │ │ └── 0
│ │ │ │ │ ├── lang_template.php
│ │ │ │ │ └── template.php
│ │ │ │ ...
│ │ │ │
│ │ │ │ └── news
│ │ │ │ ├── componentInfo.php
│ │ │ │ ├── controller.php
│ │ │ │ ├── lang_componentInfo.php
│ │ │ │ └── teplates
│ │ │ │ └── 0
│ │ │ │ ├── lang_template.php
│ │ │ │ └── template.php
│ │ │ ├── init.php
│ │ │ ├── install
│ │ │ │ └── installDataBaseTable.php
│ │ │ └── lang_init.php
│ │ └── filemodule
│ │ ├── admin
│ │ │ └── work-page-site.php
│ │ ├── classes
│ │ │ ├── AppFromConstructorPageComponent.php
│ │ │ ├── Files.php
│ │ │ └── SitePages.php
│ │ ├── component
│ │ │ └── work_page_site
│ │ │ ├── componentInfo.php
│ │ │ ├── controller.php
│ │ │ ├── lang_componentInfo.php
│ │ │ └── teplates
│ │ │ └── 0
│ │ │ ├── lang_template.php
│ │ │ ├── style.css
│ │ │ └── template.php
│ │ ├── init.php
│ │ └── lang_init.php
│ ├── style
│ │ └── main.css
│ └── test
│ └── testLoger.php
├── LICENSE
├── README.md
└── SECURITY.mdВся логика движка изолирована в одной папке /gy/. По названиям внутренних директорий сразу понятно, где что находится: компоненты, шаблоны, системные классы и модули.
Основная фишка такой структуры — юридическая и архитектурная независимость кода:
· Лицензии подлежит только папка /gy/.
· Код страниц самого сайта (например, index.php), а также все кастомные наработки выносятся в отдельный раздел.
· В кастомном разделе (/customDir/ ) разработчик может переопределять стандартные классы, шаблоны, логику компонентов или создавать свои с нуля. При этом раскрывать этот код или отдавать его под лицензией движка не требуется.
Административная панель сосредоточена в папке /gy/admin/. При этом архитектура позволяет модулям иметь собственные независимые разделы админки, если это необходимо для управления их специфическими данными.

Админ панель, скриншоты
Тестирование (точнее, его отсутствие)
В репозитории можно найти папку /tests/, но пугаться или радоваться не стоит — тесты там так и не были написаны. Всё тестирование фреймворка проводилось исключительно вручную по мере разработки. Конечно, было бы круто покрыть код юнит-тестами, но дойти до 100% покрытия в одиночку — это колоссальный объем работы, на который банально не хватило времени.
Фронтенд без зависимостей
Финально в ядре фреймворка вообще не используется JavaScript. У разработчика есть возможность подключить файлы стилей (CSS) и скриптов (JS) для компонентов, а также стандартными средствами подключать. На ранних этапах разработки я внедрял jQuery, но позже сознательно полностью вырезал его, чтобы избавить систему от лишних внешних зависимостей и сохранить минимальный вес.
Слой моделей у компонентов
Полноценная архитектура подразумевает наличие слоя моделей (Model) у компонентов. Механика для этого создана, но написана она была уже на поздних этапах разработки. В качестве эксперимента и рабочего примера внедрил её всего в один компонент — add_user. Во всех остальных компонентах модель до сих пор сделана в виде заглушки, так как переписывать все компоненты фреймворка ради этого я не стал.
Установка и деплой
Для фреймворка написан отдельный установщик, репозиторий доступен на https://github.com/ssv32/install-gy-php-framework . Там же лежит скрипт, который автоматически собирает сам установщик. Развернуть Gy на хостинге можно тремя способами: через графический веб-интерфейс, напрямую через консоль или просто скопировав файлы из основного репозитория.

Скриншоты

Ещё скриншоты
Работа с СУБД и кастомная PhpFileSql
Поддержка баз данных реализована через наследование абстрактного класса Classes/Gy/Core/AbstractClasses/Db.php. Взаимодействие с БД строится на простейших операциях CRUD (создание, чтение, обновление, удаление). Благодаря отсутствию сложных перегруженных механик, подключить новую СУБД можно очень быстро. Сейчас движок «из коробки» поддерживает:
· MySQL / MariaDB
· PostgreSQL (добавил её чисто на кураже, сразу после посещения одной из IT-конференций).
· PhpFileSql
Отдельная фишка — поддержка кастомной мини-СУБД PhpFileSql (https://github.com/ssv32/PhpFileSql). Когда-то я наткнулся на Stack Overflow на обсуждение файловых БД и решил ради фана написать свою. Это один класс, данные хранятся в зашифрованном файле.
Когда я интегрировал её в Gy, сработал обратный эффект, СУБД пришлось экстренно дорабатывать под нужды фреймворка. Я добавил туда полноценный PRIMARY_KEY_AUTO_INCREMENT для автоматического увеличения номера строки и исправил пачку всплывших багов, чтобы обе системы работали корректно. Для маленьких лендингов это оказалось идеальным решением — база весит около 200 Кб и лежит прямо в папке проекта (на разделе выше публичного, для безопасности).
Пользователю Gy не нужно писать чистый SQL-код, в движке реализованы специальные выражения (выборки), через которые условия передаются в классы работы с БД. Это позволяет разворачивать проекты без глубоких знаний БД, достаточно один раз создать пустую базу. По такому же принципу абстракции заложена и система кэширования, хотя пока в ней реализован только файловый кэш.
// пример условия выборки данных
$dataElement3 = ContainerData::getElementContainerData(
array(
'AND' => array(
array( '=' => array( 'id_container_data', $dataContentContainerData[0]['id']) ),
array( '=' => array( 'code', "'title_h1'"))
)
)
);Готовые модули
На текущий момент в системе работают два базовых модуля:
· containerdata — отвечает за создание и управление «контейнерами данных» для хранения информации в админке.
· filemodule — берет на себя управление страницами сайта напрямую из панели администратора (можно менять код файла или графически добавлять перемещать компоненты).
Также написан набор стандартных компонентов для публичной части: вывод новостей, постраничная навигация (пагинация) и вывод данных из админки.
Безопасность и архитектурная гигиена
· Проверка окружения: Скрипты на страницах всегда проверяют, подключено ли ядро системы (defined("GY_CORE")), а в админке валидируются права текущего пользователя.
· Фильтрация данных: Все входящие данные глобально очищаются. Единственное исключение, текстовые поля в админке, куда контент-менеджер сознательно может вводить чистый HTML-код для страниц.
· Защита от брутфорса: Стандартный адрес админ-панели Gy можно скрыть за кастомным секретным URL-адресом.
Огромная часть фич так и осталась на стадии планов. Есть TODO-трекер проекта https://github.com/users/ssv32/projects/2 но возможно уже не актуальный.
Пример использования Gy
Пример подключения gy php framework
<?php
include $_SERVER["DOCUMENT_ROOT"]."/gy/gy.php"; // подключить ядро // include core
Пример проверки подключено ли ядро gy php framework
<?php
if (!defined("GY_CORE") && (GY_CORE !== true)) die( "gy: err include core" );Пример вызова компонента
<?php
include $_SERVER["DOCUMENT_ROOT"]."/gy/gy.php"; // подключить ядро // include core
// пример вызова компонента
$app->component(
'form_auth',
'0',
array(
'test' => 'asd',
'idComponent' => 1,
),
);Пример кода админ страницы, добавление пользователя
<?php
include "../../gy/gy.php"; // подключить ядро // include core
global $USER;
// проверим разрешено ли показывать админ панель текущему пользователю
if (Gy\Core\User\AccessUserGroup::accessThisUserByAction( 'show_admin_panel')) {
include "../../gy/admin/header-admin.php";
// Проверим разрешено ли работать с пользователями текущему пользователю
if (Gy\Core\User\AccessUserGroup::accessThisUserByAction( 'edit_users')) {
$APP->component(
'add_user',
'0',
array(
'back-url' => '/gy/admin/users.php'
)
);
}
include "../../gy/admin/footer-admin.php";
} else {
header( 'Location: /gy/admin/' );
}
Код контроллера компонента add_user
Скрытый текст
<?php
if (!defined("GY_CORE") && (GY_CORE !== true)) die( "gy: err include core" );
$data = $_REQUEST;
// подключить модель (файл php model этого компонента) // include model this component
if (isset($this->model)) {
$this->model->includeModel();
}
$this->model->backUrl = $this->arParam['back-url'];
$redirectUrl = str_replace('index.php', '', $_SERVER['SCRIPT_NAME']);
// взять все группы пользователей
$this->model->allUsersGroups = Gy\Core\User\AccessUserGroup::getAccessGroup();
function checkProperty($arr, $userProperty, $allUsersGroups){
$result = true;
foreach ($userProperty as $val) {
if (empty($arr[$val])) {
$result = false;
}
}
if ($result) {
foreach ($arr['groups'] as $value) { // TODO протестировать
if (empty($allUsersGroups[$value])) {
$result = false;
}
if (!empty($arr['groups']['admins']) && !$USER->isAdmin()) { // TODO протестировать
$result = false;
}
}
}
return $result;
}
if (!empty($data[$this->lang->getMessage('button')]) && ($data[$this->lang->getMessage('button')] == $this->lang->getMessage('button'))) {
if (checkProperty($data, $this->model->userProperty, $this->model->allUsersGroups)) {
// добавление пользователя
global $USER;
$arDaraUser = array();
foreach ($this->model->userProperty as $val) {
$arDaraUser[$val] = $data[$val];
}
// убрать группы из добавления
unset($arDaraUser['groups']);
if ($USER->addUsers($arDaraUser)) {
// найти id добавленного пользователя
global $DB;
global $CRYPTO;
$res = $DB->selectDb(
$USER->tableName,
array('*'),
array(
'AND' => array(
array('=' => array('login', "'".$arDaraUser['login']."'")),
array('=' => array('pass', "'".md5($arDaraUser['pass'].$CRYPTO->getSole())."'") )
)
)
);
$dataAddNewUser = $DB->fetch($res);
// добавить пользователя к указанным группам
Gy\Core\User\AccessUserGroup::deleteUserInAllGroups($dataAddNewUser['id']);
foreach ($data['groups'] as $value) {
Gy\Core\User\AccessUserGroup::addUserInGroup($dataAddNewUser['id'], $value);
}
$this->model->stat = 'ok';
} else {
$this->model->stat = 'err';
}
} else {
$this->model->statText = $this->lang->getMessage('err_property') ; // ! Не все поля заполнены
$this->model->stat = 'err';
}
} elseif ((!empty($this->model->stat) && ($this->model->stat != 'err')) || empty($this->model->stat)) {
$this->model->stat = 'add';
}
if (empty($data['stat'])) {
header( 'Location: '.$redirectUrl.'?stat='.$this->model->stat );
} else {
$this->model->stat = $data['stat'];
}
// установить модель этого компонента в шаблон (view)
$this->template->setModel($this->model);
// показать шаблон (view)
$this->template->show();Код шаблона компонента add_user
Скрытый текст
<?php
if (!defined("GY_CORE") && (GY_CORE !== true)) die( "gy: err include core" );
?>
<h3><?=$this->lang->getMessage('title-add');?></h3>
<?php
if (!empty($this->model->backUrl)) {?>
<br/>
<br/>
<a class="gy-admin-button" href="<?=$this->model->backUrl;?>"><?=$this->lang->getMessage('back');?></a>
<br/>
<br/>
<?php }?>
<?php if ($this->model->stat == 'add') {?>
<form>
<?php foreach ($this->model->userProperty as $key => $val) {?>
<?=$this->lang->getMessage($val);?>:<br/>
<?php if ($val != 'groups') {?>
<input type="<?=(($val == 'pass')? 'password': 'text');?>" name="<?=$val;?>" />
<?php } else {?>
<select multiple name="groups[]">
<?php foreach ($this->model->allUsersGroups as $value) { ?>
<option value="<?=$value['code'];?>">
<?=$value['name']?> (<?=$value['code'];?>)
</option>
<?php }?>
</select>
<?php }?>
<br/>
<?php }?>
<input class="gy-admin-button" type="submit" name="<?=$this->lang->getMessage('button');?>" value="<?=$this->lang->getMessage('button');?>" />
</form>
<?php } elseif ($this->model->stat == 'ok') {?>
<div class="gy-admin-good-message"><?=$this->lang->getMessage('add-ok');?></div>
<br/>
<a href="<?=$this->model->backUrl;?>" class="gy-admin-button"><?=$this->lang->getMessage('ok');?></a>
<?php } elseif ($this->model->stat == 'err') { ?>
<div class="gy-admin-error-message"><?=$this->lang->getMessage('add-err');?></div>
<?php if (!empty($this->model->statText)) {?>
<br/> <?=$this->model->statText;?>
<?php }?>
<br/>
<a href="<?=$this->model->backUrl;?>" class="gy-admin-button"><?=$this->lang->getMessage('ok');?></a>
<?php } Выводы
На разработку было потрачено прилично времени. Многое удалось реализовать, но еще больше так и осталось в планах. Чтобы три года работы не канули в лету окончательно, я и решил написать эту статью на Хабр — как минимум, чтобы зафиксировать этот опыт и узнать мнение сообщества.
Общий вывод: Проект может и не «взлететь», хотя полностью пока не доделан. Да, всегда есть бесплатные альтернативы вроде Drupal, но они весят больше, и изучать их приходится дольше.
Вывод для себя: Это было отличное времяпрепровождение. Я решал задачи по собственному видению, написал кучу кода и прокачался в веб-разработке. Одиночка действительно может создать всё что угодно — вопрос лишь во времени. Ну и, возможно, проект кому-то понравится и появится желание поконтрибьютить в репозиторий.
Что касается будущего, есть понимание, куда двигаться дальше и что нужно переделать (я не боюсь перелопатить хоть весь проект заново). Но вот стоит ли оно затраченного времени — большой вопрос.
Если пофантазировать, путей развития несколько:
· Интеграция сторонних решений: Подключить Composer и внедрить готовые компоненты от Symfony (например, для работы с Request/Response) или продолжать писать всё вручную.
· Публикация пакета: Добить код до минимального стабильного состояния, чтобы на движке можно было гарантированно развернуть сайт с нуля, и выкатить его в Packagist.
· Глубокий рефакторинг: Начать писать локальный аналог битриксового ядра D7 или просто переписать всё с нуля в Gy 2.0. Правда, если реализовать вообще все современные стандарты PSR, то на выходе получится аналог Symfony или Laravel, а делать их копии нет никакого смысла.
Также была забавная мысль, сделать на сайте фреймворка форму с тестом и в конце выдавать «Сертификат сертифицированного разработчика Gy». Который можно ради шутки добавить в свое резюме — пусть HR удивляются сертификату по абсолютно неизвестной CMS.

























