Declarative code
You declare what you want and let the module work out how. Routing and the Ui library are the clearest examples. To keep that property, the code inside your declarations goes through the interfaces the framework provides rather than raw PHP control flow.
Declare what, not how
The Ui library is declarative: you declare what a page should contain and the library works out how to render it. You never write the step-by-step of opening tags, escaping text and closing them again. Routing works the same way: a module declares the routes it owns and the framework decides how to match a request against them. In both cases you state your intent and hand the mechanics to the module.
UiContent::Area(function() {
UiContent::Title('Address');
UiContent::Text('Edit the address details below.');
UiForm::ActionForm('address_save', function() {
UiForm::Text('street', 'Street', '');
UiForm::Submit('Save');
});
});
Each call nests inside the closure of the one above it, so the structure of the code matches the structure of the page. There are no HTML tags to close and no class names to remember. See Routing for the same idea applied to declaring routes.
Work through the provided interfaces
To keep that declarative property, the code inside your declarations goes through the
interfaces the framework provides rather than raw PHP control flow. Instead of a
foreach you iterate with the Collection helpers
CollectionArray::Each and CollectionArray::Map; instead of
if / else you branch with Logic::If,
Logic::Select and Logic::Switch. Each takes the condition and a
closure for the outcome, so the branch or iteration body is an argument rather than a
block of statements, and the code says what happens rather than how to loop:
CollectionArray::Each($rows, function($row) {
UiContent::Text($row->label);
});
Logic::If($member->isAdmin, function() {
UiContent::Button('Edit');
});
Use CollectionArray::Map when you need the result of transforming each
element, and Logic::Select or Logic::Switch when a branch has
more than one outcome. The point is uniform: control flow is expressed as calls that
take closures, not as bare language constructs.
Arguments are constants, variables or closures
An argument to any of these calls is only ever one of three things: a constant, a variable, or a closure. You never nest one call inside another call's arguments; if you find a nested call, lift it out and give it a name first. See Writing clear code for that rule in full.
Closure parameters are typed callable. Because of that, an argument can be
either an inline closure or a string that names a function to call. That keeps the rule
intact: a string like 'ShopProduct::Render' is just a constant that happens
to name a function, so you still never nest a call:
// An inline closure.
CollectionArray::Each($products, function(ShopProductData $product): void {
ShopProduct::Render($product);
});
// The same, passing a string that names the function.
CollectionArray::Each($products, 'ShopProduct::Render');
The payoff
Because the loop and the branch are declared through the interface rather than running
directly, the framework can later gather those declared operations up and emit them as
JavaScript or reactive front-end code from the same PHP, without you rewriting anything.
A raw foreach or if would run once, server-side, and leave
nothing to gather; a declared CollectionArray::Each or
Logic::If is a description the framework can walk. Working through the
provided interface also keeps every module consistent: the shape of a page, and the flow
of code that builds it, is described in one uniform way across the whole codebase.
Next: Naming.