Skip to content

AI blog · DotApp PHP Framework 2.0

What's new in DotApp PHP Framework

Dated kernel notes since 2.0. Old modules keep working: trigger() still ignores listener returns, a v1 modulesAutoLoader.php still loads, and a public catch-all that already used a tight prefix does not have to change. This article is the digest. Each item links to a complete how-to. Official intro: What's new.

Common mistakes

Wrong Right
Wake on /{path*} and skip /admin only in initializeCondition Put {not:/admin*} on the wake string itself
Treat return false on trigger() as a stop Events::triggerWithVeto() and a Veto object
Call Extender::extend() directly in register() Subscribe to dotapp.module.shop.loading, then extend() inside that callback
Persist or email only on dotapp.catchall Named event. Catchall is the debug tap

URL {not:} — exclude prefixes before the match (NEW – 2026-08-26)

Router::match_url() understands {not:mask|mask} on every URL selector: Router::get, before, onPath, Module::initializeRoutes(), and Listeners::initializeRoutes(). Exclusions run before the positive pattern (strpos / substr on a trailing-* prefix). A public catch-all stays one string and still stays out of /admin, /api/v1, and /assets.


Router::get('/{path*}{not:/admin*|/api/v1*|/assets*}', 'Shop:Public@page!');
    
  • Order matters: /admin/login against /{path*}{not:/admin*} dies on the exclude, not on {path*}.
  • /admin* vs /admin/*: /admin/* does not match exact /admin. Use {not:/admin*} when the admin index must stay out too.
  • Same syntax on wake lists. A public /{path*} that only skips /admin in initializeCondition still wakes the module — put {not:} on the wake string.
  • Do not mark that catch-all Router::STATIC_ROUTE.

Full match table and CMS wake map: How URL {not:} selectors work. AIRULES: AIRULES/03-MODULES-AND-ROUTING.md (path parameters). Sample: AIRULES/examples/EX-18-url-not.md.

Triggers, Extender, and independent listener routes (NEW – 2026-08-22)

The event bus and module loader grew. Matching listeners always register before matching modules initialize.

  • Events::triggerWithVeto() plus Dotsystems\App\Parts\Veto — an explicit stop before a reversible action. Ordinary trigger() still ignores listener returns, including a Veto. How trigger with veto works.
  • Dotsystems\App\Parts\Extender — judged, request-local replacement of a meaningful output. The owner uses exists() / call(). An ordinary result replaces it. isOriginal() continues owner logic only for the unique original() marker. In Listeners::register() subscribe to dotapp.module.shop.loading, then extend() with a string such as 'Loyalty:Pricing@quote!'. How Extender works.
  • Listeners::initializeRoutes() — a listener can wake on its own URL masks without running the module’s initialize(). Omit the method and it inherits the module map. How independent listener routes work.
  • php dotapper.php --optimize-modules writes optimizer format v2 ($modules, $listeners, $modulesAutoLoaderVersion = 2). A v1 file that only exports $modules remains compatible. After a wake-map change, re-run the optimizer. How module initialization works.

dotapp.catchall debug tap (2026-08-21)

Every trigger() except dotapp.catchall itself first fires that name with ($result, $eventname, ...$data). triggerWithVeto() does the same. Triggering catchall does not re-enter catchall. A throw there skips the named event. Keep persist and email on a named event. If you log with Logger::use(), skip dotapp.log or you recurse. Events and listeners — catchall.

Version 2.0 (2026-08-18)

The 2.0 kernel is the production rewrite: modules, fo-rm + crcCheck() once, encrypted ids, DSM, $dotapp(), AIRULES for agents. That is the baseline these later items sit on. Start at the official documentation and how to create a module.

FAQ

Do I have to rewrite old modules?

No. trigger() behaviour is unchanged. A listener without initializeRoutes() still inherits the module map. {not:} is opt-in — add it when a public catch-all must stay out of admin.

Why not {not:/admin/*}?

That mask is prefix /admin/. Exact /admin still matches the positive route. Use {not:/admin*} when the admin index must stay out too.

Where should an AI agent read first?

This page for the list. Then the linked how-to for the API you are about to use. AIRULES 03 for {not:}, 12 for Events / Extender / veto.

See also