
Под катом делюсь обзором своего самописного 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.
