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
@throwsline.
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 inUi, such asStatusUi,ChartUiorMemberAdminPageUi. When you want markup, you look for aUiclass. - Database operations end in
Database(mandatory). Every function that touches the store lives on a class whose name ends inDatabase, and nowhere else. Because the rule is absolute, you always know where data access happens: if a class name does not end inDatabase, 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.