Skip to content

Framework DotApp Updated: 2026-08-27


About the Author

My name is Štefan Miščík and I am a senior fullstack web developer at Dotsystems s.r.o. (WEB)

Design goals

DotApp is not another ordinary PHP framework of which there are hundreds. It is written from scratch, with no extra Composer dependencies and no Laravel or Symfony stack underneath. The kernel is ultra-light and highly scalable: routing, rendering, security, and the database layer share one runtime and one set of conventions.

Application logic lives in modules. Each module owns its routes, controllers, middleware, views, and assets — so a team can split work without colliding, and AIRULES can tell an AI agent the same contract. Security is built into the runtime (protected input, fo-rm, encrypted identifiers), not bolted on as a package.

Work on DotApp started in 2014 as a complete application architecture, not a wrapper around someone else’s vendor tree.


What's new

Kernel updates since DotApp 2.0. Old modules keep working. Dated digest: What's new in DotApp PHP Framework.

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

The router now has a {not:mask|mask} operator. Exclusions run before the positive pattern (strpos / substr on a trailing-* prefix). A public catch-all can stay one string and still stay 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 Module::initializeRoutes() / Listeners::initializeRoutes() wake lists. A public /{path*} that only skips /admin in initializeCondition still wakes the module — put {not:} on the wake string.

Docs: AIRULES/03-MODULES-AND-ROUTING.md (path parameters). Walkthrough: How URL {not:} selectors work.

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

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

  • Events::triggerWithVeto() plus the new Dotsystems\App\Parts\Veto class — an explicit stop before a reversible action. Ordinary trigger() still ignores listener returns, including a Veto.
  • Dotsystems\App\Parts\Extender — judged, opt-in replacement of a meaningful output for this request. The owner uses exists() / call(); an ordinary result replaces it, while isOriginal() lets the owner continue only for the unique original() marker. In Listeners::register() only subscribe to dotapp.module.shop.loading; call extend() inside that callback with a string such as 'Loyalty:Pricing@quote!'.
  • 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.
  • php dotapper.php --optimize-modules writes optimizer format v2 ($modules, $listeners, $modulesAutoLoaderVersion = 2). A v1 file that only exports $modules remains compatible.
  • dotapp.catchall still fires first on every other trigger() (and on triggerWithVeto()) so you can watch every event in one listener. Debug only — do not persist from catchall.

Walkthroughs: trigger with veto · independent listener routes · Extender · dotapp.catchall.


Conventions

The public API uses facades, module controllers, and a small set of conventions:

Facades

Facades (Router, Route, DB, Request, Renderer::new()) are the public API for core services. Application routes are registered in each module’s module.init.php.

Router and Route Facades

Use the Router and Route facades (they are aliases of each other) in module.init.php. Example: Router::get('/helloworld', 'HelloWorld:Home@index!', Router::STATIC_ROUTE).


// app/modules/HelloWorld/module.init.php → initialize($dotApp)
Router::get('/helloworld', fn() => 'Hello World', Router::STATIC_ROUTE);
Route::get('/helloworld', fn() => 'Hello World', Router::STATIC_ROUTE);
Router::get('/helloworld', 'HelloWorld:Home@index!', Router::STATIC_ROUTE);
Route::get('/helloworld', 'HelloWorld:Home@index!', Router::STATIC_ROUTE);
                    
DB Facade

Use the DB facade. Query with DB::module('RAW').


DB::module('RAW')->q(function ($qb) {
    $qb->select(['id', 'title'])->from('helloworld_notes')->where('id', '=', 1);
})->all();
                    
Request Facade

In controllers, read the current request from the $request argument. The Request facade is available when you are outside a controller callback. Incoming values are auto-protected. data() is the escaped copy (safe to print). data(true) is the original — use it for passwords, decrypt, and persist. Channel fields: $request->data(true)['data'].


Request::getPath(); // Get the current request path
Request::getMethod(); // Get the HTTP method (e.g., GET, POST)
Request::data();        // protected/escaped copy — OK to print
Request::data(true);    // original values — passwords, decrypt, compare
$request->form(['POST'], 'myForm', function ($request) {
    return 'Form submitted!';
}, function () {
    return 'Invalid form';
});
                    


The DotApp instance

Prefer facades: Router::, DB::, Renderer::new(), Config::, Events::, Bridge::listen, DSM::use(). When you need the kernel itself (DI bind/resolve, unprotect, ajaxReply, reactive polling), resolve it with:


use \Dotsystems\App\DotApp;
$dotApp = DotApp::DotApp();
            

Register routes with Router::get (and the other verb methods) inside the module’s initialize($dotApp) method.



Key Features of DotApp

Simplicity Without Compromise

DotApp combines intuitive design with high performance. You don’t need complex setups or excessive configurations – just define routes and modules, and everything else manages itself. Routes are processed only where needed, and no extra steps are required to maintain performance – it’s all automatic and efficient.


// Example of simplicity when working with the DotApp framework
namespace Dotsystems\App\Modules\HelloWorld\Controllers;

class TestController1 extends \Dotsystems\App\Parts\Controller {
    public static function testMiddlewareFn($request) {
        return "Hello " . $request->body(); // Adds text at the beginning
    }
    
    public static function mainFn($request) {
        return $request->body() . "World"; // Adds text at the end
    }
}

// Simple controller call
// app/modules/HelloWorld/module.init.php → initialize($dotApp)
Router::get("/home", "HelloWorld:TestController1@mainFn!", Router::STATIC_ROUTE)
    ->before("HelloWorld:TestController1@testMiddlewareFn!");

// Result for /home: "Hello World"
            
Focus on Low Resource Consumption

DotApp keeps memory demands to a minimum – instead of loading massive route structures and configurations, it processes only what’s currently needed. This means faster startup and great performance even on weaker servers.

Fast Route Processing

DotApp intelligently filters only relevant modules and their routes, eliminating unnecessary searches. The result is swift loading even with thousands of routes.

Example

Demonstration of routing speed: Before displaying this page, 1000 unique static and 1000 unique dynamic, deliberately unorganized routes were automatically added to the router at random. The goal was to showcase fast loading despite 2000 extra unnecessary routes. None of them match the current URL, ensuring that all must go through the router’s matching process.


$p = rtrim((string) Config::module('Docs', 'prefix'), '/') ?: '/documentation';

for ($i = 0; $i < 1000; $i++) {
    $path = $p . '/_routa' . $i;
    Router::any($path, function () use ($path) {
        return "This is route: " . $path;
    }, Router::STATIC_ROUTE);
}

for ($i = 0; $i < 1000; $i++) {
    $path = $p . '/_routa' . $i . '(?:/{language})?';
    Router::any($path, function () use ($path) {
        return "This is route: " . $path;
    });
}

// Try it out: /documentation/_routa7
                            


Displaying the page, including route creation, routing, and code generation using the templating system, took:

PHP version: 8.3.15 fpm-fcgi
Script execution time: 0.073384 s
Memory used by the script: 1006.77 KB
Peak memory usage: 1.6 MB
Loaded modules: 6

Modular Efficiency with Bidirectional Connectivity

DotApp processes only the routes of the active module, saving resources. Modules can load each other: one module’s listener can load another, and a parent module can activate children. Combinations stay explicit in module.listeners.php and initializeRoutes(). A listener may declare its own masks so it wakes without running the module’s initialize().

Cascading Module Loading

If a module depends on another (e.g., BBB needs XXX), DotApp automatically loads XXX before completing BBB. This ensures reliability – no errors due to missing dependencies – and keeps the system lightweight by loading only what’s necessary.

Dynamic Dependency Management via Triggers and Listeners

Each module has triggers like init.start, loading, loaded, and more, which listeners respond to. For example, the dotapp.module.Module1.loading listener can trigger the loading of module 2 if module 1 is active. The load() function ensures a module is loaded only once, whether cascading (top-down) or bidirectional (bottom-up).

Note: Trigger names are case-insensitive, so dotapp.module.Module1.loading and Dotapp.Module.Module1.Loading are equivalent, but we recommend using the format dotapp.module.ClassName.eventName for consistency.

Every Events::trigger() and Events::triggerWithVeto() except dotapp.catchall itself first fires that debug event, so you can watch every trigger in one listener. Use it for debugging only. A throw there skips the named event. Contract and sample: Events and listeners — dotapp.catchall. Pre-action stop: trigger with veto (Veto class). Listeners may declare their own routes: independent listener routes.


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

Events::on("dotapp.module.shop.loading", function () {
    DotApp::DotApp()->module("Cart")->load();
});
            
Automatic Dependency Resolution and DI

Modules and their dependencies load automatically – just define the logic in initializeCondition() or listeners. Dependency Injection (DI) is simple and efficient – services are registered (e.g., singleton), and DotApp delivers them where needed without unnecessary overhead.

Register services in the module’s initialize($dotApp) with singleton / bind. Controllers are public static and take $request.


public function initialize($dotApp) {
    \Dotsystems\App\DotApp::DotApp()->singleton('cache', function () {
        return new CacheService();
    });
}

namespace Dotsystems\App\Modules\HelloWorld\Controllers;

class Home extends \Dotsystems\App\Parts\Controller {
    public static function index($request) {
        $cache = \Dotsystems\App\DotApp::DotApp()->resolve('cache');
        return "Hello World";
    }
}
            
First Callback Wins

For each URL, only the first matching callback is retained – subsequent registration attempts are ignored, boosting performance and preventing conflicts.


Router::get('/documentation/test1', "HelloWorld:Home@index!");
Router::get('/documentation/test1', function () { return "Ignored"; });
// Only the first definition is used
            
Scalability for Small and Large Projects

DotApp is ideal for small sites and complex applications alike – it maintains low demands and high speed regardless of project scope. Large modules can be split into smaller parts that load recursively as needed.

No Unnecessary Overhead

DotApp focuses on the essentials – fast routing, minimal resource usage, and ease of use. It doesn’t burden you with features you don’t need.

Template system

Views and layouts live in the module. Print with {{ var: $title }}, include fragments with {{ layout:partials/header }}, and render from a controller with Renderer::new()->module('HelloWorld')->setView('hello'). Full reference: Template system.

DotApp Bridge

Live connection – a bridge between frontend and backend. Just use simple code:

<button {{ dotbridge:on(click)="newsletter.subscribe(newsletter.email)" }}>Subscribe</button>

and on the PHP side, in the module’s initialize():


use Dotsystems\App\Parts\Bridge;
use Dotsystems\App\Parts\Router;

$urls = ['/newsletter', '/newsletter/'];
Bridge::listen($urls, "newsletter.subscribe", function ($request) {
    $email = $request->data(true)['data']['newsletter.email'] ?? '';
    return ['ok' => true, 'email' => $email];
}, Router::STATIC_ROUTE);
            

The button is automatically linked to the PHP function, with rich possibilities to be introduced in the documentation.

Example of generated code:


<button  dotbridge-key="lYiF-1dlH2DgnZ1SaRg8l5y9HVgJxLr" dotbridge-id="1jjTlLmU4ydQIUr2XbsqFiuyoHVQIfXhP19N17ru3XACkha20260909532753275327080827535354a70f95856d58adb29c7981196bf0" dotbridge-event="click" dotbridge-data="yhR4KtkfSeK11R7kP6azxzNUYXlXby9FdkpTcHFQeHhrOTVUa2JwWXZJdC9kZUVrQ2tJK0JNZ3RpbHM9" dotbridge-data-id="+x5syRzG/3du3nwSPqLvCG1vU3VSZlkzTkRLaDVUTERmbDVMMU1QVE1NaTdKT05uV3Q2MEo4ZFZ3NW1nR1dneHZKcGZxTWtSYWFzWkNjbzczMWtPRW4rTnZZa1Jhdm1IVGgwNnMzZWNLUEZYNCtBcXJ3VGh3dzdncUFjPQ==" dotbridge-function="newsletter.subscribe" dotbridge-inputs="newsletter.email">Subscribe</button>
            
HTML

DotApp is tailor-made for developers who want an efficient tool without fluff. It offers speed, low demands, and simplicity that makes work easier. It’s a framework that proves less can be more – with results that speak for themselves.

Try DotApp and see for yourself!