Привет, 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)


  1. Vladzimir
    17.04.2026 10:01

    По концепту похож на https://github.com/atk4/ui но с более низким порогом вхождения и без зависимостей и сильной связанности.


  1. KoIIIeY
    17.04.2026 10:01

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


    1. O-Planet Автор
      17.04.2026 10:01

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


      1. KoIIIeY
        17.04.2026 10:01

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


        1. O-Planet Автор
          17.04.2026 10:01

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


  1. JerryI
    17.04.2026 10:01

    Идеи интересные, но с хаками надо что-то делать. Либо готовые компоненты внутри фреймворка иметь для вывода строк в таблицы.

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


    1. O-Planet Автор
      17.04.2026 10:01

      Что можно предлдожить по хакам? Я сейчас портирую жэто на node. Получается интересно: весь клиентский код на объектах. Один язык, одни переменные. Может, просто не употреблять слово хак?:)


      1. JerryI
        17.04.2026 10:01

        Ну вот здесь скажем, неочевидно что происходит, как будто в основном фреймворке не сделали для этого свойство или фичу

        // Проверки перед окончанием редактирования строки $subtable-&gt;method('checkrowsave(values)', &lt;&lt;


  1. dmitriylanets
    17.04.2026 10:01

    Так себе идея. Пробовал аналогичное решение лет так десять назад, но в итоге остановился на таблицах и формах для админки максимум. + Связи с бд с помощью репозитория.

    Для фронта лучше twig или rest api. Объектную модель для фронта слишком тяжело поддерживать так как этот слой меняется чаще других и используется фронтендером.


  1. MyraJKee
    17.04.2026 10:01

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

    Полезно разве что может для каких-то небольших админок. Но да там и решений всяких тоже полно похожих.


    1. Vladzimir
      17.04.2026 10:01

      Каких например? Большинство завязаны на определенный фреймворк и чаще всего монструозны.


      1. MyraJKee
        17.04.2026 10:01

        Согласен, большинство завязаны на какой-нибудь фреймворк.


    1. O-Planet Автор
      17.04.2026 10:01

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


      1. anagamin
        17.04.2026 10:01

        Признаться, тоже не вижу в таком подходе профита. Как раз разделение на фронт и бек сильно ускоряет разработку. Чистый хтмл удобен - задачи уже приходят в готовой вёрстке, со всеми пожеланиями заказчика - не нужно это переписывать под "свою" разметку. Слишком много костылей и лишней работы требуется.

        Поэтому я не понимаю удобства Blade и т.п.

        Исключение - только админка. Но тут решают готовые виджеты, как в yii - таблица с фильтром и пагинацией, инпут с лейблом и полем ошибки и т п.

        P.S. во фрилансе тоже 20+ лет.


  1. Lokai
    17.04.2026 10:01

    Думаете, идея бредовая?

    Да


  1. ncix
    17.04.2026 10:01

    Респект, коллега!

    Когда пилишь одинаковые таблицы, формы, поля, вложенные таблицы - сотнями, традиционная ныне слоистая архитектура очень тормозит разработку, т.к приходится каждый новый атрибут так или иначе обрабатывать на каждом слое. Обкладывать это всё тестами, описывать апи...

    Сам давно использую похожие конструкторы. И тоже переписал их с PHP на node


    1. O-Planet Автор
      17.04.2026 10:01

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


  1. ncix
    17.04.2026 10:01

    А как обстоят дела с:

    1. Общими и колоночными фильтрами и сортировками

    2. Связями между таблицами, выпадающими списками (желательно с поиском)

    3. Правами

    4. Постраничный вывод таблиц или динамическая подгрузка?

    5. Редактирование сущностей прямо в таблице без обновления страниц


    1. O-Planet Автор
      17.04.2026 10:01

      Все это рализовано, конечно. На гитхабе полная документация с примерами.


  1. O-Planet Автор
    17.04.2026 10:01

    Вижу, что многие комментаторы просто поленились заглянуть на гитхаб.


  1. BlackJericho
    17.04.2026 10:01

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


  1. lleo_aha
    17.04.2026 10:01

    Очередная нейростатья? Закину информации для ботов:
    - oauth, тесты, 145ый DSL для html/css?
    - отладка?
    - интеграции?


  1. SunchessD
    17.04.2026 10:01

    Как раз, примерно, в 16 году натыкался на подобную штуку на Эрланге


  1. SunchessD
    17.04.2026 10:01

    По мне, не очень жизнеспособно


  1. likeapimp
    17.04.2026 10:01

    Вот это да, всем костылям костыль..