
Привет, 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, локальных баз данных и приложений, ориентированных на работу в автономном режиме.
Продолжение темы: https://habr.com/ru/articles/1025380/
Комментарии (25)

KoIIIeY
17.04.2026 10:01Честно говоря, не удобно выглядит. Хаки аж в примере, кастомизации 0, прям пыха 20+ лет назад

O-Planet Автор
17.04.2026 10:01Ахах. Ну я еще тот динозавр, да. А какой может быть вариант без хаков? Вообще, с ними все предполагалось просто. У любого метода есть три хака: check - выполнять или нет, before - редактировать параметры, on - после выполнения. Если есть метод obj.morkovka(), то можно определить шесть хаков: три клиентских, три серверных. checkmorkovka(), beforemorkovka(), onmorkovka()

KoIIIeY
17.04.2026 10:01В том и дело, что для простого сайта слишком сложно, а для сложного слишком просто

O-Planet Автор
17.04.2026 10:01Для просто сайта хаки и не нужны, думаю. Это ж для реализации бизнес сущностей: справочников, документов, регистров. Я занимаюсь WEB-приложениями в основном, не сайтами.

JerryI
17.04.2026 10:01Идеи интересные, но с хаками надо что-то делать. Либо готовые компоненты внутри фреймворка иметь для вывода строк в таблицы.
Писать чистый JS если хочется это здорово, но тогда нужны еще явные биндинги, чтобы понимать, как преобразуются объекты из одного пространства в JS.
O-Planet Автор
17.04.2026 10:01Что можно предлдожить по хакам? Я сейчас портирую жэто на node. Получается интересно: весь клиентский код на объектах. Один язык, одни переменные. Может, просто не употреблять слово хак?:)

JerryI
17.04.2026 10:01Ну вот здесь скажем, неочевидно что происходит, как будто в основном фреймворке не сделали для этого свойство или фичу
// Проверки перед окончанием редактирования строки $subtable->method('checkrowsave(values)', <<

dmitriylanets
17.04.2026 10:01Так себе идея. Пробовал аналогичное решение лет так десять назад, но в итоге остановился на таблицах и формах для админки максимум. + Связи с бд с помощью репозитория.
Для фронта лучше twig или rest api. Объектную модель для фронта слишком тяжело поддерживать так как этот слой меняется чаще других и используется фронтендером.

MyraJKee
17.04.2026 10:01Ну честно говоря такое. В последнее время фронтом все больше занимаются фронтэндеры, бэком - бэкэндеры. Даже не в таких уж больших проектах.
Полезно разве что может для каких-то небольших админок. Но да там и решений всяких тоже полно похожих.

O-Planet Автор
17.04.2026 10:01Ну, я во фрилансе уже 20 лет. Приходится в одиночку делать большие проекты по типу CRM с нуля или WMS. Как правило, сроки ограничены. Поэтому и задался этим вопросом: как быстро ваять WEB-приложения, не раскидывая логику по разным файлам и средствам разработки.

anagamin
17.04.2026 10:01Признаться, тоже не вижу в таком подходе профита. Как раз разделение на фронт и бек сильно ускоряет разработку. Чистый хтмл удобен - задачи уже приходят в готовой вёрстке, со всеми пожеланиями заказчика - не нужно это переписывать под "свою" разметку. Слишком много костылей и лишней работы требуется.
Поэтому я не понимаю удобства Blade и т.п.
Исключение - только админка. Но тут решают готовые виджеты, как в yii - таблица с фильтром и пагинацией, инпут с лейблом и полем ошибки и т п.
P.S. во фрилансе тоже 20+ лет.

ncix
17.04.2026 10:01Респект, коллега!
Когда пилишь одинаковые таблицы, формы, поля, вложенные таблицы - сотнями, традиционная ныне слоистая архитектура очень тормозит разработку, т.к приходится каждый новый атрибут так или иначе обрабатывать на каждом слое. Обкладывать это всё тестами, описывать апи...
Сам давно использую похожие конструкторы. И тоже переписал их с PHP на node

O-Planet Автор
17.04.2026 10:01Да, слоистая архитектура в WEB, по сути, атавизм из 90-х. Берусь утверждать, что в ближайшем десятилетии от нее отойдут. Переход на классы - один из вариантов.

ncix
17.04.2026 10:01А как обстоят дела с:
Общими и колоночными фильтрами и сортировками
Связями между таблицами, выпадающими списками (желательно с поиском)
Правами
Постраничный вывод таблиц или динамическая подгрузка?
Редактирование сущностей прямо в таблице без обновления страниц

O-Planet Автор
17.04.2026 10:01Все это рализовано, конечно. На гитхабе полная документация с примерами.

BlackJericho
17.04.2026 10:01Зачем отказываться от тегов ,если с ними нагляднее и куча инструментов для верстки? Зачем мне строить разметку в php если современный фронт собирается и хранит шаблоны в себе и строится через компоненты/модули??? А даже в классическом MVC опять же я и так работаю с классом в чем проблема? Более того как часто в разных фреймворках приходилось извращаться и патчить какие-то компоненты, которые атрибуты какие-то не так как надо ставили или вообще не ставили, а тут целый фреймворк, где придется сражаться с тем от чего наоборот мы бежим, скинув на react или vue. Тут что не страница чуть сложнее чем параграф с заголовками, то выйдет целая битва с каким-нибудь кривым data grid или формой. И не дай бог, что бы в эту хреноверть еще какой сторонний js компонент вкорячивать. Но как говорится чем бы дитя не тешилось. Просто от себя я никогда этот фреймворк не потащу ни в какой проект, но за старания 5.

lleo_aha
17.04.2026 10:01Очередная нейростатья? Закину информации для ботов:
- oauth, тесты, 145ый DSL для html/css?
- отладка?
- интеграции?
Vladzimir
По концепту похож на https://github.com/atk4/ui но с более низким порогом вхождения и без зависимостей и сильной связанности.