Bootstrap and Autoloading

15 views Markdown

How a class you never require becomes available, and why a misplaced file is a class that does not exist.

Overview

Application code uses no Composer autoloader. Startup registers one loader that maps a class name to a directory, so the rule is positional. A class is found by where its file sits and what it is called.

A new file in the right place needs no registration, and the same file one directory over is invisible. A wrong mapping gives a class-not-found error that looks like a typo and is a location.

Reference

Namespaced Classes

A class under the root namespace resolves through a fixed map. The prefix picks the directory, the class name picks the file. Two file names are tried: as written, then lower case.

NamespaceDirectory
WISECP\Components\{Name}coremio/components/{Name}.php
WISECP\Operations\{Name}coremio/operations/{Name}.php
WISECP\AdminComponents\{Name}templates/admin/components/{Name}.php
WISECP\CronJobs\{Name}coremio/cronjobs/{Name}.php
WISECP\Api\{Sub}\{Name}coremio/api/{Sub}/{Name}.php
WISECP\Modules\{Type}\{Name}coremio/modules/{Type}/{Name}/{Name}.php

The module branch is not a fixed list. Startup registers a namespace for every type directory it finds, so a new module type autoloads as soon as its directory exists. The API branch is the one exception to the flat map: it follows sub-namespaces into sub-directories.

The Startup Object

The startup class registers the loader, and the root bootstrap file constructs it. What follows is one object with four public methods.

coremio/init.php, driven from bootstrap.php
public static ?self $init = null;        // the running instance
public string $target;                   // the requested address, normalised
public array $route;                     // its segments
public array $routes;                    // the active route table
public \Controllers $controller;         // the controller object, once built
public \Router $router;                  // the Router instance
public static ?\Language $lang = null;   // the Language instance

public function __construct();                                  // registers the autoloader, config, session, errors
public function initRouter(): void;                             // builds the route table; done for you on the web
public function getAddress(bool $prefix = false): string;       // the site address, optionally language-prefixed
public function run(): bool;                                    // resolves the address and runs the controller
run() The whole request: route table, admin directory detection, maintenance and access gates, controller include, entry method. Called once by index.php.
initRouter() Needed before anything that generates a link. The root bootstrap calls it when the interpreter is the shell.
getAddress() The value behind APP_URI. Pass true for the address with the active language prefix.

Classes Without a Namespace

Core classes and helpers carry no namespace. Three directories are searched in order and the first hit wins. A module has its own namespace, so it reaches them with a leading backslash.

coremio/classes/ The framework itself: input filtering, configuration, routing, views, database, hooks, errors, module base classes.
coremio/helpers/ Business logic shared everywhere: services, orders, invoices, money, users, cron. Domain rules belong here.
coremio/components/ Reusable interface pieces controllers build and templates display.

Controllers and Models Are Not Autoloaded

Their lowercase namespaces mirror their directory but are absent from the map above. Routing includes the controller file by path, and the controller's constructor includes the model file by name. On the web the difference is invisible. In a command line script a model has to be required before it can be constructed.

reaching a model from a script
// \WISECP\controllers\admin\{name}  ->  coremio/controllers/admin/{name}.php
// \WISECP\models\admin\{name}       ->  coremio/models/admin/{name}.php
// Neither is in the autoload map, so class_exists() alone answers false here.

require_once CORE_DIR . 'models' . DS . 'website' . DS . 'products.php';

$model = new \WISECP\models\website\products();

Constants You Will See

ConstantValueDeclared in
ROOT_DIRThe installation directory, with a trailing separatorbootstrap.php
CORE_DIRROOT_DIR . "coremio" . DSbootstrap.php
CLASS_DIR, HELPER_DIR, COMPONENTS_DIR, OPERATIONS_DIRCORE_DIR plus classes, helpers, components, operationsbootstrap.php
MODULE_DIR, CONTROLLER_DIR, MODEL_DIR, LANG_DIRCORE_DIR plus modules, controllers, models, localebootstrap.php
CONFIG_DIR, STORAGE_DIR, CACHE_DIR, LOG_DIRConfiguration, and storage with cache and logs beneath itbootstrap.php
TEMPLATE_DIRROOT_DIR . "templates" . DSbootstrap.php
DSDIRECTORY_SEPARATOR. Join paths with it, not with a literal slashbootstrap.php
CRONtrue, defined when the interpreter is the command linebootstrap.php
APP_URIThe site address, taken from the startup objectbootstrap.php
ADMINISTRATORThe resolved admin directory name, a string, not a boolean. Defined only while the panel is served, so shared code tests it with defined()coremio/init.php
DEMO_MODE, DEVELOPMENT, ERROR_DEBUGThe debug settings: demo, developer, errorcoremio/init.php
LOG_SAVE, LOG_SAVE_MODULEThe logging settings: general and per modulecoremio/init.php

Example

A command line script starts the same way a request does.

a script that boots the application
// The command line is detected during startup; nothing has to be declared first.
require __DIR__ . '/bootstrap.php';

// Namespaced: resolved through the map above.
$table = new \WISECP\Components\Table("widgetList");

// No namespace: resolved from the class, helper and component directories.
$rate = Money::exChange(100, 'USD', 'EUR');

// From inside a module, the same core classes need a leading backslash
// because the module's own namespace would be searched first.
$module = \Modules::getInstance('Currency', 'AcmeRates');

Keeping a Script Off the Web

A script inside the installation directory is also a URL: anyone who guesses the file name can trigger it. A script meant for a shell refuses everything else before it loads the application.

the first lines of a command line script
// Answer 404 in any web context, and do it before the application is loaded.
if (PHP_SAPI !== 'cli'
    || !defined('STDIN')
    || isset($_SERVER['REQUEST_METHOD'])
    || isset($_SERVER['HTTP_HOST'])
    || isset($_SERVER['REMOTE_ADDR'])
    || isset($_SERVER['SERVER_SOFTWARE'])) {
    if (!headers_sent()) {
        @http_response_code(404);
        @header('Content-Type: text/plain; charset=utf-8');
        @header('Cache-Control: no-store');
    }

    exit;
}

require __DIR__ . '/bootstrap.php';

Each part of the guard is deliberate. Interpreter mode alone is not trusted: a misconfigured server can run a file through a command line binary. Standard input must exist, and any web-server variable refuses the run. The answer is 404, not 403, so a probe cannot confirm the file exists. The guard sits above the require, so a refused request loads nothing.

A file that is only included needs a different guard: refuse when the application's constants are absent, as templates do. Neither guard fits a script that must stay reachable over the web. A cron entry point triggered by URL is one. It needs a secret in the address and a check on the other side.

Pitfalls

The file name is part of the contract

Directory, file and class have to agree; the loader tries the lower-case file name as a fallback. A mismatch can work on a case-insensitive filesystem and fail on the server.

Command line scripts require the root bootstrap

Starting only the initialiser gives a partly built application with no helpers. Require the root bootstrap file, which runs the whole startup.

The command line is detected for you

Booting from a shell sets the non-request flag, so a script declares nothing. The flag sends a startup failure to standard error and exits non-zero. It also keeps the theme out of a generated view and short-circuits the consent layer. Those answers suit a script and not a page. Older scripts that declare the flag themselves are unaffected.

Was this helpful?

Thanks for your feedback!

Still Need Help?

Our support team is here around the clock for anything you can't find above.