javascript

Я потратил 10 лет на создание легкого PHP фреймворка для разработки WEB-приложений

  • суббота, 18 апреля 2026 г. в 00:00:09
https://habr.com/ru/articles/1024496/

Привет, All!

Как вам идея, отказаться от тегов вообще и делать WEB-проекты исключительно на классах? А еще, чтобы и клиентский, и серверный коды шли рядом, как в десктопном приложениии. И чтобы с одними и теми же переменными можно было работать и в PHP, и в JavaScript.

«Зачем?» - сапросит кто-то. Отвечу: чтобы можно было строить не DOM-элементы, а объекты предметной области бизнес-процессов, которые автоматизирует мое приложение. И чтобы не тратить время на разные async, promise, ajax и т.д., пусть за это отвечает фреймворк!

Я говорю примерно вот о таком построении WEB-приложения:

incoming = new Docunent(base->incoming);
incoming->head([‘date’, ‘buyer’, ‘comment’]);
goodsincoming = incoming->subtable(base->goodsincoming)
goodsincoming->columns([‘art’, ’goods’, ‘quantity’, ‘price’, ‘total’);
button = new Button(‘Приходная накладная’);  
button->click(incoming->open());
content = new Grid();
content->add(button);
content->build();

Конечно, на PHP все будет выглядть чуть иначе. Думаете, идея бредовая?

После "Infostart Events 2016" я серьезно загорелся идеей создать такой инструмент. Где-то через пол года у меня был первый вариант, достаточно корявый, потом второй, третий... На N-й стабильной версии я начал собирать WEB-приложения, благо, клиентов было много. А полтора года назад российское издательство «Наука» заказало мне книгу на тему разработки WEB-приложений для бизнеса. И это был повод все переделать с нуля.

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

https://github.com/O-Planet/LOTIS/

А я тем временем расскажу, к какому решению пришел за эти годы.

LOTIS (Low Time Script)

В основе архитектуры своего фреймворка я заложил принцип, который назвал CMA (Construct – Metadata – Assembly). Все просто: приложение строится исключительно на объектах, реальзующих общий базовый интерфейс Construct. Объекты генерируют метаданные. Применение метаданных может быть самым разным, от построения DOM, реализации реактивности или API, до создания продакшн без исходного кода объектов. Класс Space отвечает за построение WEB-приложения.

Для наглядности давайте разберем пару рабочих примеров.

1. Минимальный рабочий пример.

$div = LTS::Div()->capt("Привет из LOTIS!");
LTS::Space()->build($div);

Тут все просто: создаем контейнер, выводим текст.

Но важно понять вот что: выполнив LTS::Div() это еще не <div></div> на странице. Это объект, который может иметь потомков и сам стать чьим-то потомком. Но только когда ты передашь объект во вселенную, он превратится в WEB-страницу. Этот подход дает ощущение свободы при разработке: ты можешь менять вложенность компонентов простым перераспределением наследования. И никаких тебе шаблонов и разметки, словно ты работаешь в старом добром Borland C 3!

2. Кусок из действующего WEB-приложения

Давайте рассмотрим более осмысленный кусок кода из рабочего приложения «Трекер».

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

// Подключаем LOTIS
include_once 'newlotis/lotis.php';

// Описываем таблицы базы данных
$base = LTS::MySql('mybase', 'localhost', 'root', 'root');

$users = $base->table('users');
$users->string('name', 100);
$users->float('total');

$kassa = $base->table('kassa');
$kassa->date('date');

$kassatable = $base->table('kassatable');
$kassatable->parent($kassa);
$kassatable->table('user', $users);
$kassatable->string('message', 100);
$kassatable->float('pay');

$money = $base->table('money');
$money->int('doc');
$money->date('date');
$money->table('user', $users);
$money->float('pay');

// Создаем документ
$maindiv = LTS::DataView();

// Привязываем DataView к таблице kassa базы данных 
$maindiv->bindtodb($kassa, [
    // Колонки таблицы документов
    'head' => ['sel' => '', 'date' => 'Дата'],
    // Поля редактора шапки документа
    'inputs' => [
        ['name' => 'id',  'type' => 'hidden'],
        ['name' => 'date', 'type' => 'date', 'caption' => 'Дата'],
        ['name' => 'save', 'caption' => 'Записать', 'type' => 'button'],
        ['name' => 'close', 'caption' => 'Отмена', 'type' => 'button']
    ],
    // Группировка полей редактора
    'cells' => ['save, close', 'date'],
    // Поля окна отбора документов
    'filter' => [['name' => 'date', 'type' => 'date', 'caption' => 'Дата']],
    // Колонки, по которым можно производить сортировку таблицы документов
    'sort' => ['date as date']
]);

// Хак на вывод строки в таблицу документов
$maindiv->table->out(
<<<JS
    function (row, obj) { 
        // Если строка была отмечена
        row.find('td.Column_sel').text(obj.sel ? '✅' : '☐'); 
        // Форматируем вывод даты, отсекаем время
        row.find('td.Column_date').text(obj.date.substr(0, 10)); 
    }
JS
);

// Определяем табличную часть документа
$subtable = $maindiv->subtable('kassasubtable', $kassatable, [
    // Колонки табличной части документа
    'head' => [
        'sel' => '',
        'name' => 'Сотрудник',
        'pay' => 'Получено',
        'del' => ''
    ],
    // Явно задаем поля, которые будут читаться из kassatable
    'fields' => 'user, user.name as name, message, pay',
    // Область сетки, куда будет помещена табличная часть
    'area' => 'element',
    // Поля редактора строки табличной части документа
    'inputs' => [
        ['name' => 'ltsDataId', 'type' => 'hidden'],
        ['name' => 'user', 'type' => 'table', 'dbtable' => $users, 'caption' => 'Сотрудник'],
        ['name' => 'message', 'caption' => 'Назначение'],
        ['name' => 'pay', 'type' => 'numeric', 'caption' => 'Выдано'],
        ['name' => 'save', 'type' => 'button', 'caption' => 'Ок'],
        ['name' => 'close', 'type' => 'button', 'caption' => 'Отмена']
    ],
    // Группировка полей редактора строки
    'cells' => ['save, close', 'user', 'message', 'pay'],
    // Фильтр отключаем
    'filter' => null
]);

// Связь поля выбора сотрудника из базы данных со строкой табличной части
$userfield = $subtable->element->field('user');
$userfield->head(['name' => 'Сотрудник', 'total' => 'Получено всего']);
$userfield->fieldmap(['id' => 'user', 'name' => 'name']);

// Хак на вывод строки в табличную часть
$subtable->table->out(
<<<JS
function (row, obj) {
    row.find('td.Column_sel').text(obj.sel ? '✅' : '☐'); 
    row.find('td.Column_del').html('<input type="button" class="ltsRowDelbutton" value="x">');
} 
JS
);

// Проверки перед окончанием редактирования строки
$subtable->method('checkrowsave(values)',
<<<JS
    if(! LTS(userfield).selected) {
        alert('Не выбран сотрудник!');
        return false;
    }
    if(values.pay == 0) {
        alert('Сумма не должна равняться нулю!');
        return false;
    }
    values.name = LTS(userfield).selected.name; 
    return true;
JS
);

// Перезапись стоков данных при сохранении документа
$maindiv->onsave(function ($args, $result) {
    global $money, $users;
    
    if(! $result['result'])
        return $result;

    // Получаем строки табличной части
    $paytable = $args['subtables']['kassasubtable'];
    // Получаем дату документа
    $date = $args['date'];
    // Добавляем дату в каждую строку
    $paytable = array_map(function ($item) use ($date) { 
        $item['date'] = $date; 
        return $item; }, 
        $paytable);

    // Открываем сток money
    $stock = LTS::Stock($money);
    // Обновляем поле total у users данными из табличной части
    $stock->collector($users, 'user', ['total' => 'pay']);
    // Обновляем записи стока
    $stock->update(['doc' => $result['data']['id']], $paytable); 

    return $result;
});

// Подключаем стили Из файла index.css
$maindiv->CSS()->add('index.css');

// Построение страницы
LTS::Space()->build($maindiv); 

Как мои идеи? Думаю, те, кто устал, как и я, бороться с клиент-серверным гемороем, меня поймут!

Сейчас я занимаюсь портированием LOTIS на Node.js. Та же архитектура отлично работает в JavaScript — и открывает возможности для PWA, локальных баз данных и приложений, ориентированных на работу в автономном режиме.