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 XxxData objects: public fields and a constructor, nothing more. See Objects and types.
  • database/ holds one XxxDatabase class per entity. All SQL lives here, and every query returns a data object. See The database layer.
  • config/ holds the <Module>Config class, 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_once for 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::Require for each of the module's own config/, data/, database/ and class/ files, in dependency order.
  • Configure the modules it includes, and register its own migrations. Call the included modules' <Module>Config functions (passing callbacks where they are needed), declare any Config::Required values this module reads, and finish with the Database::Install that 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.