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.