What do you think about ditching tags entirely and building web projects purely with classes? And having client-side and server-side code live together, like in a desktop app. And working with the same variables in both PHP and JavaScript.
"Why?" someone might ask. Here's why: so we can build domain objects for business processes instead of DOM elements. And stop wasting time on async, promises, ajax, etc. — let the framework handle that!
I'm talking about something like this:
incoming = new Document(base->incoming);
incoming->head(['date', 'buyer', 'comment']);
goodsincoming = incoming->subtable(base->goodsincoming);
goodsincoming->columns(['art', 'goods', 'quantity', 'price', 'total']);
button = new Button('Incoming Invoice');
button->click(incoming->open());
content = new Grid();
content->add(button);
content->build();
After one IT conference, I got seriously fired up about the idea of creating such a tool. About six months later, I had my first version—pretty rough around the edges—then a second, a third... On the N-th stable version, I started assembling WEB applications (luckily, I had plenty of clients). And a year and a half ago, the Russian publishing house "Nauka" commissioned me to write a book on developing WEB applications for business. That was the reason to redo everything from scratch.
The result was inspiring. I'm happy to share it and listen to the experts' thoughts, comments, and suggestions.
🔗 https://github.com/O-Planet/LOTIS/
Every class is described in detail in the documentation (available in Russian and English), with usage examples provided.
LOTIS (Low Time Script)
At the core of my framework's architecture, I laid down a principle I call CMA (Construct – Metadata – Assembly). It's simple: the application is built exclusively from objects implementing a common base interface, Construct. Objects generate metadata. The application of metadata can be incredibly versatile—from building the DOM, implementing reactivity or an API, to creating production-ready output without the original object source code.
To make this clearer, let's look at a couple of working examples.
1. Minimal working example
$div = LTS::Div()->capt("Hello from LOTIS!");
LTS::Space()->build($div);
Simple enough: we create a container, output some text.
But here's the important part: executing LTS::Div() doesn't yet give you a <div></div> on the page. It's an object that can have children and can itself become someone's child. Only when you pass the object into the "universe" does it transform into a WEB page. This approach gives you a sense of freedom during development: you can change component nesting just by redistributing inheritance. No templates, no markup—it's like you're working in the good old Borland C 3!
2. A snippet from a live WEB application
Let's look at a more meaningful piece of code from a working application called "Tracker". Tracker lets you manage product intake and expenses, create technical specifications, record product releases, calculate employee salaries, and compute profits. Sounds complex—but thanks to LOTIS, the module code stays compact, logical, and clear. Here, for instance, is the employee payout document:
// Include LOTIS
include_once 'newlotis/lotis.php';
// Describe database tables
$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');
// Create the document
$maindiv = LTS::DataView();
// Bind DataView to the 'kassa' table in the database
$maindiv->bindtodb($kassa, [
// Columns of the document table
'head' => ['sel' => '', 'date' => 'Date'],
// Fields of the document header editor
'inputs' => [
['name' => 'id', 'type' => 'hidden'],
['name' => 'date', 'type' => 'date', 'caption' => 'Date'],
['name' => 'save', 'caption' => 'Save', 'type' => 'button'],
['name' => 'close', 'caption' => 'Cancel', 'type' => 'button']
],
// Grouping of editor fields
'cells' => ['save, close', 'date'],
// Fields of the document filter window
'filter' => [['name' => 'date', 'type' => 'date', 'caption' => 'Date']],
// Columns by which the document table can be sorted
'sort' => ['date as date']
]);
// Hack for outputting a row in the document table
$maindiv->table->out(
<<<JS
function (row, obj) {
// If the row was selected
row.find('td.Column_sel').text(obj.sel ? '✅' : '☐');
// Format date output, trim time
row.find('td.Column_date').text(obj.date.substr(0, 10));
}
JS
);
// Define the tabular part of the document
$subtable = $maindiv->subtable('kassasubtable', $kassatable, [
// Columns of the document's tabular part
'head' => [
'sel' => '',
'name' => 'Employee',
'pay' => 'Received',
'del' => ''
],
// Explicitly specify fields to be read from kassatable
'fields' => 'user, user.name as name, message, pay',
// Grid area where the tabular part will be placed
'area' => 'element',
// Fields of the row editor for the tabular part
'inputs' => [
['name' => 'ltsDataId', 'type' => 'hidden'],
['name' => 'user', 'type' => 'table', 'dbtable' => $users, 'caption' => 'Employee'],
['name' => 'message', 'caption' => 'Purpose'],
['name' => 'pay', 'type' => 'numeric', 'caption' => 'Paid'],
['name' => 'save', 'type' => 'button', 'caption' => 'OK'],
['name' => 'close', 'type' => 'button', 'caption' => 'Cancel']
],
// Grouping of row editor fields
'cells' => ['save, close', 'user', 'message', 'pay'],
// Disable filter
'filter' => null
]);
// Link the employee selection field from the database to the tabular row
$userfield = $subtable->element->field('user');
$userfield->head(['name' => 'Employee', 'total' => 'Total Received']);
$userfield->fieldmap(['id' => 'user', 'name' => 'name']);
// Hack for outputting a row in the tabular part
$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
);
// Validations before finishing row editing
$subtable->method('checkrowsave(values)',
<<<JS
if(! LTS(userfield).selected) {
alert('Employee not selected!');
return false;
}
if(values.pay == 0) {
alert('Amount must not be zero!');
return false;
}
values.name = LTS(userfield).selected.name;
return true;
JS
);
// Overwrite data stocks when saving the document
$maindiv->onsave(function ($args, $result) {
global $money, $users;
if(! $result['result'])
return $result;
// Get rows from the tabular part
$paytable = $args['subtables']['kassasubtable'];
// Get document date
$date = $args['date'];
// Add date to each row
$paytable = array_map(function ($item) use ($date) {
$item['date'] = $date;
return $item; },
$paytable);
// Open the 'money' stock
$stock = LTS::Stock($money);
// Update the 'total' field in 'users' with data from the tabular part
$stock->collector($users, 'user', ['total' => 'pay']);
// Update stock records
$stock->update(['doc' => $result['data']['id']], $paytable);
return $result;
});
// Include styles from index.css file
$maindiv->CSS()->add('index.css');
// Build the page
LTS::Space()->build($maindiv);
What about my ideas? I think those who, like me, are tired of struggling with the client-server hassle will understand!
Right now, I'm working on porting LOTIS to Node.js. The same architecture works beautifully in JavaScript—and opens up possibilities for PWA, local databases, and applications designed for offline operation.

Top comments (0)