Prejsť na obsah

AI blog · DotApp PHP Framework 2.0

How module initialization works in DotApp PHP Framework

A request boots in a fixed order: index.php includes app/config.php, that file constructs new DotApp(), then load_modules() walks app/modules/. For each module, module.listeners.php is included first (if it exists). Then module.init.php instantiates Module. Inside Module::__construct the framework runs one-shot installation(), fires init.start, evaluates initializeCondition, then load() when allowed (loading → load_libraries() → initialize($dotApp) → loaded), then fires init.end. Routes and Config::module fallbacks belong in initialize(). Early global hooks belong in listeners. Production overrides belong in app/config.php. initializeRoutes() plus php dotapper.php --optimize-modules is how Shop stays lazy: it only fully initializes when the URL matches. Listeners have a separate map: Listeners::initializeRoutes() can wake callbacks without running initialize(). Old modules that omit that method inherit the module map. An old optimizer file that only exports $modules still boots. Independent listener walkthrough: How independent listener routes work. This article is a complete Shop listeners file, a complete module.init.php, and the install path — not a teaser.

Common mistakes

Wrong Right
Register Shop routes in index.php or in listeners. Register routes in initialize($dotApp).
Put Router::get in module.listeners.php. Listeners: early hooks. Init: routes and fallbacks. Listener masks may differ from module masks.
Return ['*'] from a heavy Shop so another module can subscribe. Give the subscriber listener-only routes. Keep Shop’s initialize() on Shop URLs.
Skip new Module($dotApp); at the bottom of module.init.php. DotApper writes that line. Keep it. That is the constructor entry.
Return ['*'] or a bare /{path*} from initializeRoutes() on a heavy public module. Return this module’s prefixes. A public catch-all is /{path*}{not:/admin*|…} on the wake string, then --optimize-modules
Expect an init.condition listener return value to skip init. trigger() ignores listener returns. Override initializeCondition() on the class.
Rely on renaming install.php as the only idempotency. Rename prevents a second run. Real DDL safety is your shop_installations table.
Hard-code production prefix only in the module folder. Fallbacks in initialize(), overrides in app/config.php.
Forget the namespace Dotsystems\App\Modules\Shop. Match the folder name. DotApper scaffolds it.

When this matters

You care about init when Shop must register routes, when a listener must run before routes exist, when you want lazy loading, or when install.php should create shop_* tables once. You do not re-implement boot in a controller. You do not edit app/parts/Module.php. You do not add a second bootstrap next to module.init.php. Two teams initialize in parallel: Shop’s initialize() never writes Users routes, and Users never writes Shop fallbacks. That is the team advantage of one module per product surface — see How to create a module in DotApp PHP Framework.

Boot: index.php → config.php → new DotApp → load_modules()

  1. index.php defines __ROOTDIR__ and includes app/config.php.
  2. app/config.php registers drivers, databases, secrets, then new \Dotsystems\App\DotApp().
  3. Unless maintenance is on, DotApp::DotApp()->load_modules() runs.
  4. Optional event dotapp.load_modules.override can replace the scan. If no listener, the default scan runs.
  5. If app/modules/modulesAutoLoader.php exists (from --optimize-modules), the kernel matches two maps: listener masks, then module masks. A v1 file that only has $modules still works — listeners reuse that map. Otherwise every module directory is considered.
  6. Matching module.listeners.php files load first. Then matching module.init.php files / Module constructs run.
  7. Framework default routes (assets) register. DotApp::DotApp()->davajhet() (run()) resolves the request.

Config file details: How app/config.php works in DotApp PHP Framework. Scaffold: How to create a module in DotApp PHP Framework.

Order: listeners first, then module.init.php

load_module_listeners() includes app/modules/Shop/module.listeners.php when the file exists. The listeners class extends \Dotsystems\App\Parts\Listeners and ends with new Listeners(DotApp::DotApp());. register($dotApp) keeps the framework signature; subscribe with Events::on(...) and optional global Router::before hooks that must exist before Shop’s routes. Then load_module() includes module.init.php. That file’s last line is new Module(DotApp::DotApp());. During the listeners pass, __DOTAPP_MODULES_CAN_LOAD__ is not defined yet, so a premature Module construct returns before initialize(). Full init happens on the second construct after the flag is set. You do not need to handle that flag yourself — keep the two files in their roles.

Module::__construct events

Once modules are allowed to load, the constructor does this:

  1. installation() — if install.php exists, fire dotapp.module.shop.install, include it, rename it.
  2. dotapp.module.shop.init.start — always.
  3. initializeConditionAndListener() — URL patterns from initializeRoutes(), then initializeCondition($routeMatch). If a listener is bound to dotapp.module.shop.init.condition, that event fires, but trigger() returns the original payload unchanged. Listener return values are ignored.
  4. If the condition is truthy (or DotApper is running): load() → dotapp.module.shop.loading → load_libraries() → initialize($dotApp) → dotapp.module.shop.loaded.
  5. dotapp.module.shop.init.end — always, even when init was skipped.

Event names are lowercased on register and trigger. The payload is the module instance. After every selected module has loaded: dotapp.modules.loaded.

Event When
dotapp.load_modules.override Before the default module scan
dotapp.module.{name}.install Immediately before one-shot install.php
dotapp.module.{name}.init.start After installation(), before the condition
dotapp.module.{name}.init.condition Fires if a listener exists; cannot change the boolean via return value
dotapp.module.{name}.loading Start of load(), before load_libraries() and initialize($dotApp) — canonical Extender hook
dotapp.module.{name}.loaded After initialize($dotApp) — too late for Extender
dotapp.module.{name}.init.end End of construct, always
dotapp.modules.loaded After all selected modules

initializeRoutes() and --optimize-modules

initializeRoutes() returns URL patterns. Default on the base class is ['*'] (load on every request). Shop should return its prefix:


public function initializeRoutes()
{
    return ['/shop', '/shop/*'];
}
    

Run from the project root:


php .\dotapper.php --optimize-modules
    

That writes app/modules/modulesAutoLoader.php. Current format (v2) exports $modules, $listeners, and $modulesAutoLoaderVersion = 2. On later requests, load_modules() includes that file, registers matching listeners, then fully initializes matching modules. A leftover v1 file with only $modules remains compatible: listeners share the module map. Listeners::initializeRoutes() may return a different list so Audit can subscribe on Shop’s API without booting Audit’s pages — Independent listener routes. Omit that method and the listener inherits Module::initializeRoutes() exactly. initializeCondition($routeMatch) receives whether the module patterns matched. Return truthy to run initialize(), falsy to skip heavy work (routes, fallbacks already in config, catalog boot). A public catch-all must carry {not:/admin*|…} on the wake string — the same text the optimizer stores. Do not wake on /{path*} and then skip /admin only in initializeCondition. How URL {not:} selectors work. Re-run optimize after you change prefixes, listener masks, or add modules. A stale autoloader skips Shop on URLs it should own.

Method When it runs Return
initialize($dotApp) After the condition allows boot. Register routes and fallbacks here. void
Module::initializeRoutes() Condition + --optimize-modules module map List of URL pattern strings. ['*'] = every request
Listeners::initializeRoutes() Listener wake-up map (optimizer v2 $listeners) List of URL pattern strings, or omit / return null to inherit the module map
initializeCondition($routeMatch) After pattern match, before initialize() Truthy to continue; falsy to skip initialize()
installation() Start of construct, before init.start void — runs install.php once
Module::optimize() DotApper --optimize-modules true, or the caught \Exception

installation() and install.php

If app/modules/Shop/install.php exists, installation() triggers dotapp.module.shop.install, require_onces the file, then renames it to installed_<md5>_install.php. That rename is only a one-shot guard. Versioned DDL still belongs in Installation.php with a shop_installations table. DB::migrate() is not implemented — do not call it. Preferred body of install.php:


<?php
use Dotsystems\App\Modules\Shop\Installation;

Installation::module('Shop')->install();
    

Full versioned installer walkthrough: How to create database migrations with Installation.php in DotApp PHP Framework. After tables exist, Shop controllers use QueryBuilder or named RAW (WHERE id = :iduser plus ['iduser' => $id]): How to use the database in DotApp PHP Framework. $qb->raw() counts every ?, including column comments — never COMMENT 'SMS?' in installer SQL.

settings() versus Config::module during init

In initialize() you set portable fallbacks with Config::module. Owner overrides are already in app/config.php from boot. $this->settings('apiUrl') reads app/modules/Shop/settings.php (value or null). That file is for facts Shop persists itself, not for prefix and secrets.

Complete module.listeners.php

File: app/modules/Shop/module.listeners.php. Loaded before init. No Shop GET/POST routes here.


<?php
namespace Dotsystems\App\Modules\Shop;

use Dotsystems\App\DotApp;
use Dotsystems\App\Parts\Events;
use Dotsystems\App\Parts\Logger;

class Listeners extends \Dotsystems\App\Parts\Listeners
{
    public function initializeRoutes()
    {
        return ['/shop', '/shop/*', '/api/v1/auth/Shop', '/api/v1/auth/Shop/*'];
    }

    public function register($dotApp)
    {
        Events::on('shop.item.saved', function ($result, ...$data) {
            Logger::use('shop')->warning('Item saved', ['payload' => $data]);
        });

        Events::on('dotapp.module.shop.init.end', function ($module) {
            Logger::use('shop')->warning('Shop init ended', [
                'name' => $module->modulename,
            ]);
        });
    }
}

new Listeners(DotApp::DotApp());
    

trigger('shop.item.saved', $result, $itemId) always returns the original $result. Listener exceptions abort remaining listeners — wrap risky bodies yourself. Route-scoped on($route, $event, $cb) returns false and does not register when the current request does not match. Pre-action stop: Trigger with veto. Listener-only masks: Independent listener routes. Every trigger() except dotapp.catchall itself first fires that debug tap — see Events and listeners.

Complete module.init.php

File: app/modules/Shop/module.init.php. Fallbacks, routes, lazy patterns, condition. Last line constructs the module.


<?php
namespace Dotsystems\App\Modules\Shop;

use Dotsystems\App\DotApp;
use Dotsystems\App\Parts\Config;
use Dotsystems\App\Parts\Router;
use Dotsystems\App\Parts\Translator;

class Module extends \Dotsystems\App\Parts\Module
{
    public function initialize($dotApp)
    {
        Config::module('Shop', 'prefix') ?? Config::module('Shop', 'prefix', '/shop');
        Config::module('Shop', 'itemsPerPage') ?? Config::module('Shop', 'itemsPerPage', 20);
        Config::module('Shop', 'enckey') ?? Config::module('Shop', 'enckey', bin2hex(random_bytes(16)));
        Config::module('Shop', 'public') ?? Config::module('Shop', 'public', true);
        Config::module('Shop', 'locale') ?? Config::module('Shop', 'locale', 'en_us');

        Translator::setDefaultLocale('en_us');
        Translator::loadLocaleFile('Shop:sk_sk.json', 'sk_sk');
        Translator::loadLocaleFile('Shop:de_de.json', 'de_de');
        Translator::setLocale((string) Config::module('Shop', 'locale'));

        if (Config::module('Shop', 'public') === false) {
            return;
        }

        $p = Config::module('Shop', 'prefix');

        Router::get($p . '/', 'Shop:Home@index!', Router::STATIC_ROUTE);
        Router::get($p . '/item/{id:i}', 'Shop:Home@item!');
        Router::post($p . '/contact', 'Shop:Contact@save!', Router::STATIC_ROUTE);
    }

    public function initializeRoutes()
    {
        return ['/shop', '/shop/*'];
    }

    public function initializeCondition($routeMatch)
    {
        return $routeMatch;
    }
}

new Module(DotApp::DotApp());
    

Keep enckey as a module secret the owner overrides in app/config.php. Do not document or implement key-exchange internals in Shop. Generate local fallbacks with bin2hex(random_bytes(16)) and tell the owner to replace them. The channel working key is separate: app.c_enc_key, a per-session key, extra keys, and key update. The framework builds it. How DotApp PHP Framework protects the browser-to-PHP channel. Routing verbs: How routing works in DotApp PHP Framework. Load locale JSON in this same initialize(): How translations and i18n work in DotApp PHP Framework.

Gotchas

  • First matching route wins. Order registrations inside initialize() on purpose.
  • Router::hasRoute() is inverted relative to the English name. Prefer php dotapper.php --list-routes.
  • If initializeRoutes() during DotApper optimize does not return a one-dimensional list of strings, it throws \InvalidArgumentException.
  • A missing view later in the request returns "" and a log warning — that is render time, not init time. Still: do not assume init failure throws.
  • Do not leak encryption internals in listeners or init. Set keys; do not explain how to break them.
  • Do not put crcCheck() on a global Router::before if handlers also call it. The first success burns the one-time token; the second call is HTTP 400. Request lifecycle — crcCheck once.

FAQ

Why both listeners and module.init.php?

Listeners run first so global hooks exist before Shop constructs. Routes and Config::module fallbacks stay in initialize() so lazy loading can skip them when the URL is not Shop.

How do I skip initialize() on unrelated URLs?

Return tight patterns from Module::initializeRoutes() and return $routeMatch from initializeCondition(). If another module must still subscribe on those URLs, give that module’s listener its own masks — do not wake Shop everywhere. Run --optimize-modules so unmatched modules are not even constructed fully.

Can I cancel init from an init.condition listener?

No. trigger() does not apply listener return values. Override initializeCondition() on Module.

Will install.php run on every request?

No. After the first successful include, the file is renamed. Put real idempotency in Installation.php anyway.

When is return ['*'] correct?

When Shop must boot on every URL (rare: a global widget). Prefer explicit /shop patterns.

What if I delete new Module(DotApp::DotApp())?

The class is never constructed. No routes, no fallbacks, no install. Leave the construct line. DotApper still writes new Module($dotApp) because the include has $dotApp in scope — new Module(DotApp::DotApp()) is the same instance and the preferred form.

Are Config::module overrides from app/config.php visible in initialize()?

Yes. app/config.php ran before load_modules(). The ?? fallback setter only fills keys that are still null.

Does event name case matter?

Names are lowercased. Register and trigger the same logical name; do not rely on mixed case.

Where do I register Extender::extend()?

In Listeners::register() only subscribe Events::on('dotapp.module.{owner}.loading', …) and call extend() inside that callback with a controller string. Cover the owner’s known URLs on Listeners::initializeRoutes(). Matching listeners run before any module constructs, so the subscription exists before the owner boots. .loading fires only if the owner actually loads, still before load_libraries() and initialize(). Direct extend() in register() is not canonical — the listener route can match while the owner’s initializeCondition later skips load. Do not wait for dotapp.module.{owner}.loaded — that fires after the owner’s initialize(). Do not use init.start — that fires before the condition. Do not call extend() from this module’s initialize() — map order can run the owner first. How Extender works.

See also