Module structure and separation

Keep classes small and single concern, and let each module own only its own work.

Follow these directives:

  • Keep each class small and single concern, and each function one job. Split by concern before a class grows into a dumping ground.
  • A module owns only its own sections, pages and routing. Pages for another concern live in that concern's module.
  • Reuse the shared Ui components rather than writing new ones for the same purpose. If a component is used by more than one module, move it down into the Ui library.
  • Keep developer and diagnostic tooling behind admin authentication, out of the public front end. The public front end carries no debug flags, diagnostic output or hidden routes.

Type every function and document it

Types and documentation are not optional decoration; they are how a function states its contract so callers do not have to read its body. Every function follows these rules:

  • Every argument is given its type. No untyped parameters.
  • Every function declares its return type, even when that type is void.
  • Every function has a doc comment above it that describes what the function does, describes each parameter, states what it returns, and declares each exception it can throw with a @throws line.

The doc comment lists exceptions so a caller knows what it must be ready to handle without reading the implementation. If a function can throw more than one type, each gets its own @throws line.

/**
 * Load a product by its id.
 *
 * @param int $id The product id to look up.
 * @return ShopProductData The product row.
 * @throws ShopNotFoundException When no product has that id.
 */
function loadProduct(int $id): ShopProductData
{
    $optionalProduct = ShopProductDatabase::find($id);
    if ($optionalProduct === null) {
        throw new ShopNotFoundException($id);
    }
    return $optionalProduct;
}

How we use objects

Only three kinds of class are ever instantiated with new: data objects, union objects, and exceptions. Everything else is a class of static functions. A service, a page, a Database class or a Ui class is never constructed; you call its functions directly as ShopProduct::Load($id). Objects carry data; static functions carry behaviour.

The framework does not use PHP namespaces. Instead of a namespace, related functions are grouped onto a static-function class, and the class name plus its module prefix does the work a namespace would. This keeps every function reachable by a single, predictable name and keeps the grouping visible in the name itself. See Naming for how those names are formed.

Two of those groupings are fixed:

  • User-interface functions end in Ui (convention). Functions that build or render UI are grouped onto a class whose name ends in Ui, such as StatusUi, ChartUi or MemberAdminPageUi. When you want markup, you look for a Ui class.
  • Database operations end in Database (mandatory). Every function that touches the store lives on a class whose name ends in Database, and nowhere else. Because the rule is absolute, you always know where data access happens: if a class name does not end in Database, it runs no SQL.

Database access lives in a Database class

All database access lives in classes whose name ends in Database. Query building and row mapping have one home per entity, so the rest of the module works with data objects and never touches SQL. See Database layer conventions for the full rules.

Next: Database layer conventions.