Module layout and the index file
Every module has the same directory layout, and at the top of it sits an
index.php that wires the module up. Once you know the shape, you can open
any module and know where each kind of code lives and how it loads.
The shape
A module is a directory named after the module. Inside it, code is split by concern
into a fixed set of subdirectories, with an index.php and a
README.md at the top:
Shop/
index.php the module entry point
README.md what the module is and its invariants
class/
Shop.php the main behaviour class
ShopPage.php page rendering and routing
ShopForm.php form handling
data/
ShopProductData.php a plain data object
database/
ShopProductDatabase.php all SQL for one entity
config/
ShopConfig.php the module's configuration API
ShopTypeConfig.php declaration calls used inside ShopConfig
install/
0001.php numbered migration scripts
0002.php
This is the exact shape of the MemberFolder module in the framework. A site module such as Shop follows it too.
What each part holds
- index.php is the module entry point. It includes every module it uses, loads the module's own files in dependency order, and registers the module's migrations. See the index file sections below.
- class/ holds behaviour: the main
<Module>class plus page and form classes. Keep these small and single-concern, one class per file. - data/ holds the
XxxDataobjects: public fields and a constructor, nothing more. See Objects and types. - database/ holds one
XxxDatabaseclass per entity. All SQL lives here, and every query returns a data object. See The database layer. - config/ holds the
<Module>Configclass, the configuration API other modules call, plus any type-declaration class it uses. See Config classes. - install/ holds numbered migration scripts that create and change the module's tables. See Migrations and seed data.
- README.md states what the module is and the invariants it holds, so a reader gets the contract before reading code.
Why the split
Each subdirectory has one job, so behaviour, data shape, SQL, configuration, and
schema changes never mix in the same file. You find things by kind: a query is in
database/, a field is in data/, a schema change is in
install/. The layout is the same in every module, so the next module
reads the same way as the last.
The index file, in order
A module's index.php is its entry point. It requires every other module
this one uses, registers the module's own source files in dependency order, then
configures the modules it includes and registers its own migrations. It only wires the
module up: it never runs application logic. Every index.php is the same
ordered set of sections, always in this order:
- An optional header comment describing the module, the invariants it holds, and how to use it.
- Require the modules this module depends on. A
require_oncefor every module it uses: framework modules through$MODULE_PATH . '/Name/index.php', site modules in the same tree by relative path. - Register this module's own source files.
Loader::Requirefor each of the module's ownconfig/,data/,database/andclass/files, in dependency order. - Configure the modules it includes, and register its own migrations.
Call the included modules'
<Module>Configfunctions (passing callbacks where they are needed), declare anyConfig::Requiredvalues this module reads, and finish with theDatabase::Installthat registers this module's migrations.
That is the whole job. The index file registers files and configures modules - it does not run application logic directly. Passing a callback to a config function is still configuration: the callback is stored now and invoked later, by the module that owns it, when the app actually runs.
A Shop index file
This follows the shape of MemberFolder's and Member's index.php. The
three parts are the require block, the file registrations, and the configuration and
migration block at the end:
<?php
/*
Shop Module
=
Sells products. Owns products, their prices and stock. A product belongs to
exactly one category; stock never goes below zero.
*/
// Required modules
require_once $MODULE_PATH.'/Loader/index.php';
require_once $MODULE_PATH.'/Database/index.php';
require_once $MODULE_PATH.'/Security/index.php';
require_once $MODULE_PATH.'/To/index.php';
require_once $MODULE_PATH.'/Config/index.php';
require_once $MODULE_PATH.'/Web/index.php';
require_once $MODULE_PATH.'/Ui/index.php';
// A site module in the same tree is required by relative path, not $MODULE_PATH.
require_once __DIR__.'/../Catalog/index.php';
// Module files
Loader::Require(__DIR__.'/config/ShopConfig.php');
Loader::Require(__DIR__.'/data/ShopProductData.php');
Loader::Require(__DIR__.'/database/ShopProductDatabase.php');
Loader::Require(__DIR__.'/class/Shop.php');
Loader::Require(__DIR__.'/class/ShopPage.php');
Loader::Require(__DIR__.'/class/ShopForm.php');
// Configure included modules + register this module's migrations
Config::Required('Shop', 'SHOP_CURRENCY', Config::TYPE_STRING);
// Register Shop as a Catalog listing type. The callback is stored now and
// called later, by Catalog, when it renders a listing - configuration, not
// application logic that runs during include.
CatalogConfig::Listing('Shop', 'ShopProductListing::Render');
Database::Install('Shop', __DIR__.'/install', 1);
Includes come first, and are unconditional
This is a hard rule: a module requires what it uses. It includes every module it
depends on, directly and unconditionally, and never assumes another module is already
loaded because something else happened to pull it in. If Shop calls Ui,
its index file requires Ui/index.php, whatever order the site loaded its
modules in.
Never gate code on a presence check such as
class_exists or function_exists for a module you depend on.
If the guard is false the code is silently skipped and behaviour becomes include-order
dependent, so setup and type registration must run every time, not by luck. Include the
module and call it directly.
Own files load in dependency order
Loader::Require loads the module's own files. Order is config, then
data, then database, then class, so each file's dependencies are defined before it
loads: the database class refers to the data object, the main class refers to both.
Configuration, and passing callbacks
After its own files are registered, a module configures the modules it includes. This
is where a module hooks itself into the modules it depends on: it calls their
<Module>Config functions, often passing a callback so the other
module can call back into this one later. MemberFolder, for example, registers a folder
type by handing MemberFolder two callbacks - one to count a folder's rows, one to render
them - and Member registers its login and code types by handing the owning module the
handlers to invoke when a code is entered.
Passing a callback here is configuration, not execution: the callback is recorded now
and only runs later, from inside the owning module, when a request actually reaches it.
A module also declares any config it needs to read with
Config::Required, so a missing value is caught up front. Config functions
always live on a <Module>Config class - see
Config classes.
Registering migrations
The final line hands the module's install folder and its latest version
to Database::Install. That is what applies the numbered scripts and
records how far the module has reached. See
Migrations and seed data.
The application index file
A site has one top-level entry point, public_html/index.php. It is a
module index file at heart - the same require, register, configure body - but it is the
file the web server actually hits, so it carries two extra parts, one at each end.
At the very top: the boot configuration the app needs. Before any
module that consumes them is included, the application provides its configuration
values with Config::Provide. Those values ultimately come from outside the
code - from a Config.php file kept outside public_html, or from
environment variables the container supplies (read here at the top level only, never
with getenv deep inside a module). Providing them first means each module
can validate and read its own settings as it loads. This is covered in
Configuration.
Then the standard module body: require the framework and site modules,
let each register its files and declare its config, and run the application's own
Database::Install. A final Config::SecurityCheck verifies every
Config::Required value was actually provided before anything runs.
At the end: the application code that runs the app. Where a module
index stops after configuration, the application index goes on to start the app. The web
server is started with NetworkWeb::Server (from the Network module), which
takes the site's routing as a callback: inside it the site matches the request URL and
renders the page that was hit. That routing is the subject of
Routing, and how the pieces sit together on disk is
Site structure.
If the app can also be driven from a terminal, this is where its command-line entry
points are registered too. Command-line handling is provided by the Command and Console
modules - the app declares its commands and their options through the Command module and
hands off with a call such as Command::Server. The same file therefore
serves both the web request and the CLI invocation, choosing which to run by whether PHP
was started from the command line.
Next: Config classes.