# DotApp PHP Framework 2.0 documentation (full text)Cite as: DotApp PHP Framework 2.0 by Dotsystems s.r.o., Slovakia. https://dotapp.dev/License: MIT. Author: Štefan Miščík. Contact: dotapp@dotapp.dev.This document is the official English documentation and may be used for search, retrieval, and model training while retaining attribution. --- # Documentation URL: https://dotapp.dev/documentation/intro dotApp Framework About the Author Design goals Key Features of dotApp Framework DotApp Updated: {{ var: $variables['last_update'] }} 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 a PHP framework for applications of every size. Application logic lives in modules. Each module owns its routes, controllers, middleware, views, and assets. Work on DotApp started in 2014 as a complete application architecture: routing, rendering, security, and the database layer share one runtime and one set of conventions. 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. Request::getPath(); // Get the current request path Request::getMethod(); // Get the HTTP method (e.g., GET, POST) Request::data(); // Access request data (e.g., POST or JSON payload) $request->form(['POST'], 'myForm', function ($request) { return 'Form submitted!'; }, function () { return 'Invalid form'; }); The DotApp instance Routing, database, rendering, configuration, and session storage use facades: Router::, DB::, Renderer::new(), Config::, DSM::use(). When you need the kernel itself (events, unprotect, ajaxReply, call), 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: 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(). 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. 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) { $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: 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: HTMLCopy 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! {{_ "Continue" }} {{_ "Each topic is a separate page with its own URL, title, and search entry." }} {{_ "Installation" }} {{_ "Router" }} {{_ "Dependency injection" }} DotBridge {{_ "Database" }} {{_ "Templates" }} {{_ "For AI agents" }} --- # Installation URL: https://dotapp.dev/documentation/installation Installation Post-Installation Setup Running the Framework Adding the First Route Installation The DotApp application is not installed in the standard way via composer install because it is not a library but a complete application with its own architecture. Instead, follow these steps: 1. Install the application using Git: git clone https://github.com/dotsystems-sk/dotapp.git ./ This command clones the repository into the current directory. Alternatively, you can download the ZIP file: Download dotApp Unzip it into the directory where you need to place the application. 2. Setup and Usage: After cloning or unzipping the application, configure it according to your needs (e.g., database configuration, environment settings, etc.). 3. Using Composer: Although the application itself is not installable via Composer, once installed, you can use Composer within the application directory to add additional dependencies required for your project. Simply run: composer require Directory structure. Application logic lives in app/modules/{ModuleName}/. app/parts/ is the framework core. project-root/ ├── index.php # front controller ├── dotapper.php ├── app/ │ ├── config.php # secrets, databases, drivers │ ├── modules/ # your modules │ │ └── HelloWorld/ │ │ ├── module.init.php │ │ ├── module.listeners.php │ │ ├── Controllers/ │ │ ├── Middleware/ │ │ ├── Models/ │ │ ├── views/ │ │ └── assets/ │ ├── parts/ # framework core — do not edit │ ├── runtime/ │ └── vendor/ └── assets/ └── modules/ # public files served from each module's assets/ Post-Installation Setup This section describes the minimal setup required after installing the DotApp framework. If you don’t need to work with databases or prefer your own solution over the built-in library, simply follow these steps: 1. (Optional) Override __ROOTDIR__ in index.php By default, index.php sets __ROOTDIR__ to its own directory. Define it only when the application root is somewhere else (no trailing slash): define('__ROOTDIR__', "/var/www/html"); Ensure that __ROOTDIR__ matches the actual server location, otherwise the framework may not function correctly. 2. Set a unique encryption key in ./app/config.php Config::set("app", "name", "MyApp"); Config::set("app", "c_enc_key", "ReplaceThisWithALongRandomSecret"); $dotApp = new \Dotsystems\App\DotApp(); Use a unique, long secret (at least 32 characters). Do not pass md5(...) into the DotApp constructor. 3. (Optional) Database — uncomment and fill the sample in ./app/config.php use Dotsystems\App\Parts\Config; if (!__MAINTENANCE__) { Config::addDatabase( "main", "127.0.0.1", "username", "password", "dbname", "UTF8", "MYSQL", "pdo" ); } $dotApp = new \Dotsystems\App\DotApp(); $dotApp->load_modules(); Running the Framework After app/config.php loads modules, index.php starts the application. Routes are declared in each module’s module.init.php. The start call is: // Everything ready? Start the framework! - As seen in the index.php file $dotApp->davajhet(); // or in English: $dotApp->run(); /* Note: $dotApp->davajhet() is an alias for $dotApp->run(). I come from eastern Slovakia, so I added a bit of our "davaj het!" (let's go!) */ Adding the First Route Blank page and ERROR 404? If you followed the instructions and see a blank page with a 404 status code after starting, don’t worry. It’s logical. The router is empty, so it couldn’t find a route for the / address in your browser and correctly displayed a 404. Your framework has no modules or routes yet, so it’s doing what it’s supposed to. Add the first route in a module initialize() method. --- # Router URL: https://dotapp.dev/documentation/router dotApp Router 1. Introduction 1.1. What is a Router? 1.2. Key Features 1.3. Basic Routing Principles 2. Getting Started 2.1. Router Initialization 2.2. Accessing the Router in dotApp 2.3. Defining the First Route 3. Defining Routes 3.1. Basic HTTP Methods (GET, POST, etc.) 3.2. The match() Method for Multiple Methods and URLs 3.3. Static vs. Dynamic Routes 3.4. Using Variables in Routes 3.5. Working with Controllers and Middleware 4. Working with the Request Object 4.1. What is a Request? 4.2. Accessing Data from a Request 4.3. Using Request in Callbacks 5. Middleware (Before and After Hooks) 5.1. What are Hooks? 5.2. Defining before() 5.3. Defining after() 5.4. Using with Multiple Routes 6. Error and Exception Handling 6.1. Handling 404 Errors 6.2. Custom Error Handling DotApp Router The Router is a key component of the DotApp Framework, managing the routing of HTTP requests within the application. It allows you to define how requests (e.g., GET, POST) are mapped to specific callback functions, controllers, or middleware. The Router is designed to handle both static and dynamic routes, support hooks (before and after), and provide flexibility in building web applications. 1.1 What is the Router? The Router in the DotApp Framework is a class Dotsystems\App\Parts\Router that processes incoming HTTP requests and directs them to the appropriate handlers. It works in conjunction with the Request object, which contains information about the request (path, method, variables). Its primary role is to simplify route definition and ensure the correct code is executed for a given URL and HTTP method. The Router is integrated directly into the framework's core, so you don’t need to install or configure it separately – simply use the Router facade: Router::get(...). 1.2 Key Features The Router offers a wide range of features that make application development easier: HTTP Method Support: Define routes for GET, POST, PUT, DELETE, PATCH, OPTIONS, HEAD, TRACE, and the universal ANY method. Dynamic Routes: Use variables (e.g., {id}) and regular expressions to capture parts of the URL. Middleware: Support for before and after hooks to execute logic before and after the main handler. Request Object: Passes request information to callbacks and controllers. Flexibility: Ability to use anonymous functions, controllers, or middleware via strings (e.g., "Module:Controller@method!"). Chaining: Method chaining for cleaner code. 1.3 Basic Routing Principles The Router compares the current URL (obtained from $request->getPath()) and HTTP method (from $request->getMethod()) with defined routes. If a match is found: Any before hooks are executed. The main logic (callback, controller, or middleware) is performed. Any after hooks are executed. Routes can be: Static: Exact URL match (e.g., /home). Dynamic: Contain variables or wildcards (e.g., /user/{id}). First Match Wins: The first matching route is used; subsequent matches are ignored. The Router resolves requests using the resolve() method, which is typically called automatically within the DotApp lifecycle. Example of a Basic Route: Register routes in a module’s initialize($dotApp) method in app/modules/{Module}/module.init.php. Controllers live in app/modules/{Module}/Controllers/ and are called as 'Module:Controller@method!'. // app/modules/HelloWorld/module.init.php → initialize($dotApp) Router::get('/home', function ($request) { return "Welcome to the homepage!"; }, Router::STATIC_ROUTE); When accessing the URL http://example.com/home, the text "Welcome to the homepage!" is displayed. 2. Getting Started This chapter guides you through the basics of working with the Router in the DotApp Framework – from initialization to defining your first route. 2.1 Router Initialization The Router is a core service. Register application routes with the Router:: facade from each module’s initialize($dotApp). Controllers live in app/modules/{Module}/Controllers/. Technical Details: The Router is an instance of the Dotsystems\App\Parts\Router class. During construction, it receives $dotAppObj (an instance of the main DotApp class), giving it access to the Request object and other framework services. 2.2 Accessing the Router in DotApp Register routes in module.init.php with the Router facade. The $request object is passed into callbacks and controller methods and carries the current path, method, and variables. Example of Access: // Check the current path echo $request->getPath(); // E.g., "/home" // Check the HTTP method echo $request->getMethod(); // E.g., "get" 2.3 Defining Your First Route The simplest way to start with the Router is to define a basic route using one of the HTTP methods (e.g., get()). A route can be linked to an anonymous function (callback), a controller, or middleware. Example of a First Route with a Callback: // app/modules/HelloWorld/module.init.php → initialize($dotApp) Router::get('/home', function ($request) { return "Welcome to the homepage!"; }, Router::STATIC_ROUTE); Explanation: /home: Static URL path. function($request): Callback that accepts the Request object and returns a response. When calling http://example.com/home, the text "Welcome to the homepage!" is displayed. Example with a Controller: Suppose you have a controller Home in app/modules/HelloWorld/Controllers/Home.php with a method index: // app/modules/HelloWorld/Controllers/Home.php namespace Dotsystems\App\Modules\HelloWorld\Controllers; class Home extends \Dotsystems\App\Parts\Controller { public static function index($request) { return "This is the homepage from the controller!"; } } // app/modules/HelloWorld/module.init.php Router::get('/home', 'HelloWorld:Home@index!', Router::STATIC_ROUTE); Explanation: 'HelloWorld:Home@index!': Module HelloWorld, controller Home, static method index. The trailing ! disables DI on that method. The Router automatically loads and calls this method with the Request object. Running the Routing: The framework resolves routes after modules load. Application routes are declared in module.init.php. // app/modules/HelloWorld/module.init.php public function initialize($dotApp) { Router::get('/home', function ($request) { return "Welcome!"; }, Router::STATIC_ROUTE); } 3. Defining Routes This chapter explains how to define routes in the Router of the DotApp Framework. The Router supports various definition methods – from basic HTTP methods to dynamic routes with variables and working with controllers. 3.1 Basic HTTP Methods (GET, POST, etc.) The Router provides methods for all standard HTTP methods: get(), post(), put(), delete(), patch(), options(), head(), and trace(). Each method defines a route for a specific HTTP request. Example of a GET Route: Router::get('/about', function ($request) { return "This is the About Us page!"; }); Example of a POST Route: Router::post('/submit', function ($request) { return "The form has been submitted!"; }); Note: Each method takes the URL path as the first parameter and a callback (or controller reference) as the second parameter. The callback always receives the $request object. 3.2 The match() Method for Multiple Methods and URLs The match() method allows defining a route for multiple HTTP methods at once or for an array of URLs. It’s a more flexible approach compared to standalone methods like get() or post(). Example with Multiple Methods: Router::match(['get', 'post'], '/contact', function ($request) { return "This is the contact page!"; }); This route works for both GET and POST requests to /contact. Example with Multiple URLs: Router::match(['get'], ['/home', '/index'], function ($request) { return "Welcome to the homepage!"; }); The route captures requests to both /home and /index. 3.3 Static vs. Dynamic Routes The Router distinguishes between static and dynamic routes: Static Routes: Exact URL match (e.g., /home). Dynamic Routes: Contain variables or wildcards (e.g., /user/{id}). Example of a Static Route: Router::get('/profile', function ($request) { return "This is a static profile!"; }); Example of a Dynamic Route: Router::get('/user/{id}', function ($request) { return "User profile with ID: " . $request->matchData()['id']; }); For the URL /user/123, it displays "User profile with ID: 123". 3.4 Using Variables in Routes Dynamic routes can include variables marked with curly braces (e.g., {id}). These variables are automatically extracted and available via $request->matchData(). Basic Usage: Router::get('/article/{slug}', function ($request) { return "Article: " . $request->matchData()['slug']; }); For /article/how-to-cook, it displays "Article: how-to-cook". Typed Variables: The Router also supports variable typing: {param:s}: String (no slashes). {param:i}: Integer. {param:l}: Letters only. {param:s?}: Optional typed parameter (the ? belongs inside the braces). {param*}: Wildcard (captures everything). Router::get('/user/{id:i}', function ($request) { return "User ID: " . $request->matchData()['id']; }); Works only for numbers, e.g., /user/123, but not /user/abc. 3.5 Working with Controllers and Middleware In addition to anonymous functions, you can map routes to controllers or middleware using a string in the format "Module:Controller@method!" or "#Module:Middleware@method!". Example with a Controller: // app/modules/HelloWorld/Controllers/User.php namespace Dotsystems\App\Modules\HelloWorld\Controllers; class User extends \Dotsystems\App\Parts\Controller { public static function show($request) { return "Displaying the user!"; } } // Route definition Router::get('/user', 'HelloWorld:User@show!'); Example with Middleware: // app/modules/HelloWorld/Middleware/AuthMiddleware.php namespace Dotsystems\App\Modules\HelloWorld\Middleware; class AuthMiddleware extends \Dotsystems\App\Parts\ModuleMiddleware { public static function check($request) { // Return a Response to short-circuit. Returning nothing continues the route. if (!\Dotsystems\App\Parts\Auth::isLogged()) { return new \Dotsystems\App\Parts\Response(403, 'Forbidden'); } } } // app/modules/HelloWorld/module.init.php → initialize($dotApp) Router::get('/secure', 'HelloWorld:User@show!') ->before('#HelloWorld:AuthMiddleware@check!'); Note: Functions must be defined as public static and accept $request as a parameter. Middleware classes are almost always attached with ->before('#Module:Class@method!'), not used as the main route handler. 4. Working with the Request Object The Request object is an integral part of the Router in the DotApp Framework. It carries information about the current HTTP request and is automatically passed to callbacks, controllers, and middleware. This chapter explains how it works and how to use it effectively. 4.1 What is Request? The Request is an instance of the Dotsystems\App\Parts\Request class, serving as an interface for working with request data. It contains information about the path, HTTP method, variables from dynamic routes, and other request attributes. It is automatically created during the Router initialization and is accessible via $request. Key Features: Retrieving the current URL path and method. Accessing variables from dynamic routes via matchData(). Passing data to callbacks and hooks. 4.2 Accessing Data from Request The Request object provides methods to retrieve basic request information: getPath(): Returns the current URL path (e.g., /home). getMethod(): Returns the HTTP method (e.g., get, post). matchData(): Returns an array of variables extracted from a dynamic route. hookData(): Returns data assigned to hooks (used with standalone before/after). Example of Access: Router::get('/user/{id}', function ($request) { $path = $request->getPath(); // "/user/123" $method = $request->getMethod(); // "get" $id = $request->matchData()['id']; // "123" return "Path: $path, Method: $method, ID: $id"; }); For a request to /user/123, it displays: "Path: /user/123, Method: get, ID: 123". 4.3 Using Request in Callbacks The Request object is automatically passed as a parameter to all callbacks, controllers, and middleware defined in routes. It allows you to work with request data directly within the route’s logic. Example with an Anonymous Function: Router::get('/profile/{name}', function ($request) { $name = $request->matchData()['name']; return "Hello, $name!"; }); For /profile/Jano, it displays: "Hello, Jano!". Example with a Controller: // app/modules/HelloWorld/Controllers/Profile.php namespace Dotsystems\App\Modules\HelloWorld\Controllers; class Profile extends \Dotsystems\App\Parts\Controller { public static function show($request) { $name = $request->matchData()['name'] ?? ''; return "Profile for: $name"; } } // app/modules/HelloWorld/module.init.php → initialize($dotApp) Router::get('/profile/{name}', 'HelloWorld:Profile@show!'); The result is the same as with the anonymous function. Example with Middleware: // app/modules/HelloWorld/Middleware/CheckMiddleware.php namespace Dotsystems\App\Modules\HelloWorld\Middleware; class CheckMiddleware extends \Dotsystems\App\Parts\ModuleMiddleware { public static function verify($request) { // Optional logging. Do not return a Response unless you want to stop the request. $path = $request->getPath(); } } // app/modules/HelloWorld/module.init.php → initialize($dotApp) Router::get('/check', 'HelloWorld:User@show!') ->before('#HelloWorld:CheckMiddleware@verify!'); For /check, the middleware runs first. If it does not return a Response, the User@show! handler runs. Note: matchData() returns an empty array if the route contains no dynamic variables. Verify the existence of a key before use, e.g., isset($request->matchData()['id']), to avoid errors. 5. Middleware (Before and After Hooks) Middleware in the Router of the DotApp Framework allows you to execute additional logic before or after the main route handler. These "hooks" are defined using the before() and after() methods and are ideal for tasks such as authentication, logging, or response modification. 5.1 What Are Hooks? Hooks are functions that run automatically at specific stages of route processing: before: Executes before the main route logic (e.g., callback or controller). after: Executes after the main logic, with access to the route’s result. Hooks accept the $request object as a parameter and can be defined globally, for a specific route, or for a method with a route. 5.2 Defining before() The before() method is used to add logic that executes before the main handler. It can be applied in three ways: Globally: For all routes. For a Specific Route: Only for a given path. For a Method and Route: Specifically for an HTTP method and path. Global Before: Router::before(function ($request) { return "Before every route!"; }); Router::get('/test', function ($request) { return "Test page"; }); The hook runs for all routes, e.g., for /test, "Before every route!" executes first. Before for a Specific Route: Router::get('/secure', function ($request) { return "Secure page"; })->before(function ($request) { return "Verifying access..."; }); The hook runs only for /secure. Before with a Method and Route: Router::before('get', '/login', function ($request) { return "Checking login for GET"; }); Router::get('/login', function ($request) { return "Login page"; }); 5.3 Defining after() The after() method runs after the main handler and has the same definition options as before(). It’s useful for modifying results or logging. Global After: Router::after(function ($request) { return "After every route!"; }); Router::get('/test', function ($request) { return "Test page"; }); The hook runs after every route, e.g., for /test, "Test page" executes first, followed by "After every route!". After for a Specific Route: Router::get('/profile', function ($request) { return "Profile page"; })->after(function ($request) { return "Profile has been displayed"; }); After with a Method and Route: Router::after('post', '/submit', function ($request) { return "Form has been processed"; }); Router::post('/submit', function ($request) { return "Submission successful"; }); 5.4 Using with Multiple Routes You can assign hooks to multiple routes at once using an array of paths with the match() method or by calling before()/after() separately. Example with Match: Router::match(['get'], ['/home', '/index'], function ($request) { return "Homepage"; })->before(function ($request) { return "Before the homepage"; })->after(function ($request) { return "After the homepage"; }); The hooks apply to both paths: /home and /index. Example with an Array of Paths: Router::before('get', ['/page1', '/page2'], function ($request) { return "Before the pages"; }); Router::get('/page1', function ($request) { return "Page 1"; }); Router::get('/page2', function ($request) { return "Page 2"; }); Note: The output from hooks is appended to the route’s response. To modify the response, work directly with $request->response->body in the hook (more in advanced features). 6. Error and Exception Handling The Router in the DotApp Framework allows developers to manage errors and exceptions that occur during request processing. This chapter explains how to handle standard errors like 404 and implement custom error-handling logic using callbacks and hooks. 6.1 Handling 404 Errors If the Router finds no match, it fires dotapp.router.resolve.404. If no listener handles it, Router::errorHandle(404, $view) can render error_{$view}. Otherwise the framework sends an empty 404 and stops. Preferred: event listener in module.listeners.php use Dotsystems\App\Parts\Events; use Dotsystems\App\Parts\Response; Events::on('dotapp.router.resolve.404', function () { return new Response(404, 'Page not found'); }); Alternative: named error view Router::errorHandle(404, 'notfound'); That looks for a view named error_notfound. Register the listener or error view from a module. 6.2 Custom Error Handling Developers can implement custom error-handling logic directly in callbacks or middleware using conditions and HTTP codes. Example with a Condition in a Callback: Router::get('/user/{id:i}', function ($request) { $id = $request->matchData()['id']; if ($id > 100) { http_response_code(403); return "Access forbidden for IDs greater than 100!"; } return "User profile: $id"; }); For /user/150, it displays "Access forbidden for IDs greater than 100!" with code 403. Example with Middleware: Router::get('/user/{id:i}', function ($request) { $id = $request->matchData()['id']; return "User profile: $id"; })->before(function ($request) { $id = $request->matchData()['id']; if (!isset($id)) { http_response_code(400); return "ID is missing!"; } }); For /user/, it displays "ID is missing!" with code 400. Note: Using http_response_code() in callbacks or hooks allows setting custom error states. It’s up to the developer whether to terminate the script with exit or return an error message. 7. Advanced Features The Router in the DotApp Framework offers advanced features that extend its capabilities. This chapter covers method chaining, dynamic URL matching, a detailed explanation of creating dynamic addresses, and defining API endpoints. 7.1 Method Chaining The Router supports method chaining, allowing you to define routes, hooks, and other settings in a single command. This improves code readability and organization. Example of Chaining: Router::get('/profile/{id}', function ($request) { $id = $request->matchData()['id']; return "Profile ID: $id"; })->before(function ($request) { return "Checking before displaying the profile"; })->after(function ($request) { return "Profile displayed"; }); For /profile/123, before, the main logic, and after execute sequentially. 7.2 Dynamic Route Matching (matchUrl()) The matchUrl() method is used for manually matching a URL against a routing pattern. It returns an array of extracted variables if the pattern matches, or false if not. It’s useful for custom validations or route testing. Example of Use: Router::get('/test', function ($request) { $pattern = '/user/{id:i}'; $url = '/user/123'; $match = Router::matchUrl($pattern, $url); if ($match !== false) { return "Match! ID: " . $match['id']; } return "No match"; }); For /test, it displays "Match! ID: 123". 7.3 Dynamic Addresses and Patterns Dynamic addresses in the Router allow defining routes with variables and optional parts using special syntax. These patterns are recognized consistently across methods (e.g., get(), post(), match()), and variables are available via $request->matchData(). Below is a detailed explanation with an example and a list of the most common patterns. Example of a Dynamic Address: Router::get('/documentation/intro(?:/{language})?', function ($request) { $language = $request->matchData()['language'] ?? 'default'; return "Introductory documentation, language: $language"; }); Explanation: /documentation/intro(?:/{language})?: Defines a route where {language} is an optional part (marked with ?: and ?). /documentation/intro: Valid (language is "default"). /documentation/intro/eng: Valid (language is "eng"). /documentation/intro/: Invalid (the Router expects a value after the slash if present). The language variable is extracted into $request->matchData() if provided, otherwise it’s null. Most Commonly Used Patterns: Here’s a list of 10 common dynamic address patterns used in web applications, with examples and explanations: /{resource}/{id:i} - Basic CRUD Route Router::get('/users/{id:i}', function ($request) { return "User ID: " . $request->matchData()['id']; }); Valid: /users/123, Invalid: /users/abc /{category}/{slug:s} - Category and Article Slug Router::get('/blog/{category}/{slug:s}', function ($request) { return "Category: " . $request->matchData()['category'] . ", Slug: " . $request->matchData()['slug']; }); Valid: /blog/tech/how-to-code /api/v{version}/{endpoint} - Versioned API Router::get('/api/v{version}/{endpoint}', function ($request) { return "API v" . $request->matchData()['version'] . ": " . $request->matchData()['endpoint']; }); Valid: /api/v1/users /{page}(?:/{subpage})? - Optional Subpage Router::get('/docs/{page}(?:/{subpage})?', function ($request) { $subpage = $request->matchData()['subpage'] ?? 'main'; return "Page: " . $request->matchData()['page'] . ", Subpage: $subpage"; }); Valid: /docs/intro, /docs/intro/setup /{type}/{id:i}/{action} - Action on a Resource Router::get('/posts/{id:i}/{action}', function ($request) { return "ID: " . $request->matchData()['id'] . ", Action: " . $request->matchData()['action']; }); Valid: /posts/5/edit /{resource}/{filter:s?} - Optional Filter Router::get('/products/{filter:s?}', function ($request) { $filter = $request->matchData()['filter'] ?? 'all'; return "Products, filter: $filter"; }); Valid: /products, /products/new /{path*} - Wildcard for Entire Path Router::get('/files/{path*}', function ($request) { return "File path: " . $request->matchData()['path']; }); Valid: /files/images/photo.jpg /{lang:l}/{section} - Language and Section Router::get('/{lang:l}/{section}', function ($request) { return "Language: " . $request->matchData()['lang'] . ", Section: " . $request->matchData()['section']; }); Valid: /en/news, Invalid: /123/news /search(?:/{query})? - Optional Search Query Router::get('/search(?:/{query})?', function ($request) { $query = $request->matchData()['query'] ?? 'empty'; return "Search: $query"; }); Valid: /search, /search/php /{resource}/{id:i}(?:/{extra})? - Resource with an Optional Parameter Router::get('/users/{id:i}(?:/{extra})?', function ($request) { $extra = $request->matchData()['extra'] ?? 'none'; return "ID: " . $request->matchData()['id'] . ", Extra: $extra"; }); Valid: /users/10, /users/10/details Note: These patterns are flexible and combinable. Use {?:} for optional parts and types (:i, :s, :l) for precise constraints. 7.4 Defining API Endpoints with apiPoint The apiPoint method in the Router provides a convenient way to define API endpoints with support for versioning, modules, and dynamic parameters. It offers flexibility in defining custom paths and methods, and when combined with the built-in abstract Controller class and its apiDispatch (main logic) and api (shorter alias) methods, it enables automatic dispatching of requests to specific controller methods with dependency injection (DI) support. Definition Router::apiPoint($version, $module, $controller, $custom = null); Parameters: $version: API version (e.g., "1" for v1). $module: Module name (e.g., "shop"). $controller: Callback or string in the format "Module:Controller@method!" (e.g., "HelloWorld:Posts@apiDispatch!", "HelloWorld:Posts@api!", or a custom method). $custom (optional): Specific path (string) or array of paths. Supports regular expressions (e.g., (?:/{id})?). If $custom is not provided, the default dynamic path /api/v{version}/{module}/{resource}(?:/{id})? is used. If specified, only the paths from $custom are applied. First route wins! Static paths must be listed before dynamic ones to avoid being overridden by dynamic logic. Built-in Controller and apiDispatch/api Methods: The framework provides an abstract class Dotsystems\App\Parts\Controller with the apiDispatch method, which automatically dispatches requests to specific methods in the format (e.g., postUsers, getPosts) based on the HTTP method and the value of the dynamic resource parameter. Point apiPoint to Controller@apiDispatch (or Controller@api as a shorter alias). Automatic dispatching works when the path includes {resource}. You do not register a separate route for each resource method. apiDispatch maps HTTP method + resource to a controller method named {method}{Resource}, for example GET …/posts → getPosts($request). Implement those methods as public static. If no method matches, error404($request) runs when it exists; otherwise the framework returns HTTP 404. Register the dispatcher with a trailing !: Router::apiPoint("1", "shop", "HelloWorld:Posts@apiDispatch!"); Customizing Errors: If the target method (e.g., postUsers) doesn’t exist, apiDispatch first checks if the controller defines an error404 method. If so, it calls it, allowing the user to define custom logic for 404 errors (e.g., JSON response, logging). If error404 isn’t present, it returns a default error message with HTTP code 404. Using with Automatic Dispatching: Automatic dispatching via apiDispatch (or api) works only if the path includes the dynamic {resource} parameter in the correct position (e.g., /api/v1/shop/{resource}). If $custom doesn’t maintain this format, the automation won’t work, and a custom method must be used. Example without $custom (Automatic Dispatching): Router::apiPoint("1", "shop", "HelloWorld:Posts@apiDispatch!"); Resulting Paths: POST /api/v1/shop/users - Triggers postUsers if it exists. GET /api/v1/shop/posts - Triggers getPosts. GET /api/v1/shop/status - Triggers error404 if it exists, otherwise 404 with a default message. /api/v1/shop/posts/ - Not captured. Example with $custom and Automatic Dispatching: Router::apiPoint("1", "shop", "HelloWorld:Posts@apiDispatch!", ["{resource}(?:/{id})?/details"]); Resulting Paths: POST /api/v1/shop/users/details - Triggers postUsers. GET /api/v1/shop/posts/details - Triggers getPosts. GET /api/v1/shop/posts/abc123/details - Triggers getPosts. PUT /api/v1/shop/status/details - Triggers error404 if it exists, otherwise 404 with a default message. /api/v1/shop/users/ - Not captured. Example with Custom Routes and a Custom Method: Router::apiPoint("1", "shop", "HelloWorld:Posts@customMethod!", ["users/details", "posts/summary"]); Resulting Paths: Automatic dispatching doesn’t work here because {resource} is missing. The logic depends on the implementation of customMethod. GET /api/v1/shop/users/details - Triggers customMethod. POST /api/v1/shop/posts/summary - Triggers customMethod. Example Controller with DI and Custom Error: namespace Dotsystems\App\Modules\Dotcmsfe\Controllers; class Posts extends \Dotsystems\App\Parts\Controller { public static function postUsers($request, \SomeService $service) { return "Creating users: " . $service->process($request->getPath()); } public static function getPosts($request) { $id = $request->matchData()['id'] ?? null; return "List of posts" . ($id ? " with ID: $id" : ""); } public static function error404($request) { http_response_code(404); return json_encode([ 'error' => 'Not Found', 'message' => "Resource '{$request->matchData()['resource']}' not found or method '{$request->getMethod()}' not supported", 'path' => $request->getPath() ]); } public static function customMethod($request) { return "Custom method for path: " . $request->getPath(); } } Note: The built-in Controller simplifies API handling with apiDispatch (or api) when the path includes {resource}. For custom routes without {resource}, you can use custom methods, but automatic dispatching won’t work. The order of paths in $custom is critical – static paths must precede dynamic ones. 8. Practical Examples This chapter provides practical examples of using the Router in the DotApp Framework. It demonstrates how to combine basic and advanced features to address common scenarios in web applications. 8.1 Simple GET Route The most basic example of defining a static route with a simple response. Example: Router::get('/welcome', function ($request) { return "Welcome to the application!"; }); For a request to /welcome, it displays: "Welcome to the application!". Use Case: Ideal for static pages like homepages or "About Us". 8.2 Dynamic Route with Variables An example of a dynamic route with variable extraction to display user data. Example: Router::get('/user/{id:i}/{name}', function ($request) { $id = $request->matchData()['id']; $name = $request->matchData()['name']; return "User ID: $id, Name: $name"; }); For /user/123/Jano, it displays: "User ID: 123, Name: Jano". Use Case: Suitable for profiles, product details, or other resources with identifiers. 8.3 Using Middleware An example combining a route with before and after hooks for verification and logging. Example: Router::get('/dashboard', function ($request) { return "Welcome to the dashboard!"; })->before(function ($request) { $user = "guest"; // Simulated verification if ($user === "guest") { http_response_code(403); return "Access denied!"; } })->after(function ($request) { return "Dashboard displayed at " . date('H:i:s'); }); For /dashboard, it displays "Access denied!" with code 403 (since the simulated verification fails). If verification succeeded, it would show "Welcome to the dashboard!" followed by the display time. Use Case: Authentication, access logging, or response modification. 8.4 Combining with Controllers An example of integrating a route with a controller to separate logic from routing. Example: // app/modules/HelloWorld/Controllers/Article.php namespace Dotsystems\App\Modules\HelloWorld\Controllers; class Article extends \Dotsystems\App\Parts\Controller { public static function detail($request) { $slug = $request->matchData()['slug']; return "Article detail: $slug"; } } // Route definition Router::get('/article/{slug:s}', 'HelloWorld:Article@detail!'); For /article/how-to-code, it displays: "Article detail: how-to-code". Use Case: Larger applications where code organization into controllers is needed. 9. Tips and Tricks This chapter offers practical tips and tricks for effectively using the Router in the DotApp Framework. These will help you optimize your code, debug issues, and follow best practices. 9.1 Optimizing Routing The Router in the DotApp Framework operates on a "first match wins" principle – the first matching route in the order of definition is used, and others are ignored, regardless of whether they are static or dynamic. The order of definition is therefore critical for optimization. Define the most important routes first: Since the first match wins, place critical or frequently used routes at the top. Use specific patterns: E.g., {id:i} instead of {id} to prevent unintended matches on incorrect routes. Group similar routes: Use match() with an array of paths to reduce code duplication, but be mindful of order. Example of Optimization: Router::get('/user/{id:i}', function ($request) { // First dynamic route return "Dynamic user ID: " . $request->matchData()['id']; }); Router::get('/user/123', function ($request) { // Second static route return "Static user 123"; }); For /user/123, the first route always wins ("Dynamic user ID: 123") because it was defined first, even though the second is static and more precise. To prioritize the static route, define it earlier. Example with Reordered Priority: Router::get('/user/123', function ($request) { // First static route return "Static user 123"; }); Router::get('/user/{id:i}', function ($request) { // Second dynamic route return "Dynamic user ID: " . $request->matchData()['id']; }); Now, for /user/123, it displays "Static user 123" because it’s defined first. 9.2 Debugging Routes When troubleshooting routing issues, use the tools available in the Router and PHP to identify which route is actually being triggered, especially with the "first match" rule. Check the path: Use $request->getPath() to verify the URL the Router is processing. Dump variables: Print $request->matchData() to see which values were extracted. Test order: Add temporary outputs (e.g., echo) in callbacks to determine which route executed. Example of Debugging: Router::get('/page/{id}', function ($request) { echo "Dynamic route triggered for ID: " . $request->matchData()['id']; return "Dynamic page " . $request->matchData()['id']; }); Router::get('/page/1', function ($request) { echo "Static route triggered for /page/1"; return "Static page 1"; }); For /page/1, it displays "Dynamic route triggered for ID: 1" and "Dynamic page 1" because the dynamic route is defined first. Changing the order would prioritize the static route. 9.3 Best Practices for Route Structure Following best practices helps maintain clarity and predictability in routing. Logical order: Define routes from most specific to most general to leverage the "first match wins" rule. Comments: Add comments above routes to clarify why they are in a specific order. Separate logic: Use controllers for complex routes instead of inline callbacks. Example of Best Practices: // Most specific static route Router::get('/api/users/guest', function ($request) { return "Guest user"; }); // Specific dynamic route Router::get('/api/users/{id:i}', function ($request) { return "User ID: " . $request->matchData()['id']; }); // General route last Router::get('/api/{resource}', function ($request) { return "Resource: " . $request->matchData()['resource']; }); For /api/users/guest, the first route triggers; for /api/users/5, the second; and for /api/products, the third, thanks to logical ordering. 10. Conclusion This chapter concludes the documentation for the Router in the DotApp Framework. It summarizes its benefits and offers a look at its future development and community. 10.1 Why Use the Router in DotApp? The Router in the DotApp Framework is a simple yet powerful tool for managing routing in web applications. Its key advantages include: Flexibility: Support for both static and dynamic routes with variables and optional parts. Simplicity: Intuitive interface for defining routes via HTTP methods like get() and post(). Middleware: Ability to add before and after hooks for extended logic. First Match Wins: Predictable behavior based on the order of route definition, giving developers full control. Integration: Seamless collaboration with controllers and the Request object for request handling. Whether you’re building a small application or a complex system, the Router provides the tools to map requests to logic quickly and efficiently. --- # Dependency injection URL: https://dotapp.dev/documentation/dependency-injection DI and Middleware 1.1. What is Dependency Injection and Middleware? 1.2. DI Container in dotApp 1.3. Options for Calling Controllers and Middleware 1.4. Working with Middleware 1.5. Practical Examples 1.6. Notes Dependency Injection and Middleware This chapter describes dependency injection (DI) and middleware in the DotApp PHP Framework 2.0. Controllers, middleware, and routes belong in a module. Call a controller with "Module:Controller@method!" (the trailing ! turns DI off for that method). Attach module middleware with ->before("#Module:AuthGate@check!"). 1. Dependency Injection and Middleware in DotApp The DI container lives on the DotApp instance (bind, singleton, resolve). Middleware is either a route before/after hook, a named Middleware::register pipeline, or a module class under app/modules/{Module}/Middleware/. 1.1. What is Dependency Injection and Middleware? Dependency Injection (DI) inserts registered services into callbacks instead of constructing them by hand. In DotApp the container is the DotApp class. Middleware runs before or after the route handler. Attach a module middleware class with ->before("#Module:Class@method!"). Named pipelines use Middleware::register plus Middleware::use()->group(). 1.2. The DI container Three methods on the kernel you receive as initialize($dotApp): 1.2.1. bind(string $key, callable $resolver) Registers a factory. Each resolve call creates a new instance. $dotApp->bind('logger', function () { return new Logger(); }); $logger = $dotApp->resolve('logger'); $logger2 = $dotApp->resolve('logger'); // different instance 1.2.2. singleton(string $key, callable $resolver) Created once, then reused. The framework already registers DotApp::class as a singleton. $dotApp->singleton('cache', function () { return new CacheService(); }); $cache1 = $dotApp->resolve('cache'); $cache2 = $dotApp->resolve('cache'); // same instance 1.2.3. resolve(string $key) Returns the bound value. Throws if the key is unknown. $cache = $dotApp->resolve('cache'); 1.2.4. How DI is used in practice Register services in a module’s initialize($dotApp). Controller methods used from routes are public static and take $request. The trailing ! on the callable string turns DI off for that method so the handler receives $request only. public function initialize($dotApp) { $dotApp->singleton('cache', function () { return new CacheService(); }); } 1.3. Calling controllers All of the following belong in app/modules/{Module}/module.init.php inside initialize($dotApp). 1.3.1. Module controller string Syntax: "Module:Controller@method!". Live Users demo: Users:Login@page!. Router::get('/documentation/examples/run/users/login', "Users:Login@page!", Router::STATIC_ROUTE); 1.3.2. Trailing ! The ! disables DI for that method. Use it on almost every route handler. Router::post('/documentation/examples/run/users/login', "Users:Login@save!", Router::STATIC_ROUTE); 1.3.3. Closures The first argument is $request. Router::get('/status', function ($request) { return "Current method: " . $request->getMethod(); }, Router::STATIC_ROUTE); 1.3.4. DotApp::call Call a module controller or middleware string from PHP: DotApp::call("Users:Login@page!", $request); 1.4. Middleware 1.4.1. Module middleware on a route Put the class in app/modules/Users/Middleware/AuthGate.php and attach it with before. The route handler remains the controller method. Router::get('/documentation/examples/run/users/app', "Users:Login@app!", Router::STATIC_ROUTE) ->before("#Users:AuthGate@check!"); namespace Dotsystems\App\Modules\Users\Middleware; use Dotsystems\App\Parts\Auth; use Dotsystems\App\Parts\Config; use Dotsystems\App\Parts\Response; class AuthGate extends \Dotsystems\App\Parts\ModuleMiddleware { public static function check($request, array $rights = []) { if (!Auth::isLogged()) { $p = rtrim((string) Config::module('Users', 'prefix'), '/'); return Response::redirect($p . '/login', 302); } if (!empty($rights) && !Auth::can($rights)) { return new Response(403, 'Forbidden'); } } } 1.4.2. Named middleware pipeline Register with Middleware::register, then wrap routes with Middleware::use()->group(). The callback receives ($request, $next) and must call $next($request). use Dotsystems\App\Parts\Middleware; use Dotsystems\App\Parts\Response; Middleware::register('is_admin', function ($request, $next) { if (!\Dotsystems\App\Parts\Auth::can('Users.admin')) { return new Response(403, 'Forbidden'); } return $next($request); }); Middleware::use('is_admin')->group(function () { Router::get('/admin/users', 'Users:Login@app!'); }); 1.4.3. Global before hook A closure used as Router::before must accept $request. Returning a Response short-circuits the route. Router::before(['POST'], ['/documentation/examples/run/users/*'], function ($request) { // e.g. extra CRC or logging }); 1.4.4. Route-chain middleware() On a route chain, Router::middleware() is an alias of before(). Named pipelines are registered with Middleware::register and applied with Middleware::use()->group(). The second argument of Router::get (and the other verb methods) is the route handler: a controller string or a callable. 1.5. Practical examples 1. Singleton registration in initialize($dotApp): $dotApp->singleton('cache', function () { return new CacheService(); }); $cache1 = $dotApp->resolve('cache'); $cache2 = $dotApp->resolve('cache'); echo ($cache1 === $cache2) ? "Same instance" : "Different instances"; 2. Protected page: Router::get('/documentation/examples/run/users/app', "Users:Login@app!", Router::STATIC_ROUTE) ->before("#Users:AuthGate@check!"); 3. Bind plus a closure that reads the request: $dotApp->bind('logger', function () { return new Logger(); }); Router::get('/log', function ($request) use ($dotApp) { $logger = $dotApp->resolve('logger'); return "Method: " . $request->getMethod(); }, Router::STATIC_ROUTE); 1.6. Notes Singleton vs bind: singleton for shared services, bind for a new instance each time. Controllers: public static, first argument $request, route string 'Module:Ctrl@method!'. Middleware: module class + ->before('#Module:Class@method!'), or Middleware::register + use()->group(). Named pipelines: register with Middleware::register, then wrap routes in Middleware::use('name')->group(). On a route chain, ->middleware() is an alias of ->before(), not a registrar. --- # DotBridge URL: https://dotapp.dev/documentation/dotbridge DotBridge Key features How it works Registration Calling from HTML Filters Security DotBridge DotBridge is a key component of the DotApp Framework, ensuring secure and efficient communication between server-side PHP and client-side JavaScript via AJAX requests. It allows you to define PHP functions callable from the front-end, manage inputs, and protect communication from unauthorized access or abuse. DotBridge is designed to provide flexibility, security, and easy integration of dynamic features into web applications. 1.1 What is DotBridge? DotBridge in the DotApp Framework is a class Dotsystems\App\Parts\Bridge that serves as a bridge between back-end PHP logic and front-end JavaScript actions. It enables calling PHP functions from HTML elements (e.g., buttons, form inputs) through encrypted AJAX requests. Bridge calls POST to the current page URL. Its primary role is to simplify client-server communication while ensuring it is verified, encrypted, and protected against attacks such as CSRF or request replay. Bridge functions are registered in a module with Bridge::listen() or Router::bridge() (controller string). 1.2 Key Features DotBridge offers a wide range of features that simplify the development of secure and dynamic applications: Secure Communication: Uses data encryption, key verification, and CRC checks to protect requests. Calling PHP Functions: Define PHP functions callable from JavaScript with support for before and after callbacks. Validation Filters: Real-time input validation (e.g., email, URL, password) with visual feedback on the client side. Rate Limiting: Ability to set request limits per time (rateLimit(seconds,clicks)). One-Time Keys: Support for oneTimeUse and regenerateId for enhanced security. Flexibility: Supports various events (e.g., click, keyup) and dynamic inputs from HTML. Chaining: Method chaining for cleaner code on both PHP and JavaScript sides. 1.3 Basic Operating Principles DotBridge handles communication between the client and server in the following steps: Generates a unique session key and registers PHP functions on the server side via Bridge::listen() or Router::bridge(). In HTML, events (e.g., {{ dotbridge:on(click)="fnName" }}) and inputs (e.g., {{ dotbridge:input="name" }} or short dotbridge="name") are defined and linked to PHP functions. When an event is triggered (e.g., a click), an AJAX POST is sent to the bound page URL (default: the current page) with encrypted data. The server verifies the key, decrypts the data, checks request limits, and executes the requested PHP function. The result is returned as a JSON response, which JavaScript can further process. Communication is safeguarded with data encryption, key verification, and limits to prevent abuse or unauthorized access. Example of Basic Usage: use Dotsystems\App\Parts\Bridge; use Dotsystems\App\Parts\Router; $urls = ['/hello', '/hello/']; Bridge::listen($urls, "sayHello", function ($request) { return ["status" => 1, "message" => "Hello from the server!"]; }, Router::STATIC_ROUTE); Upon clicking the button, the PHP function sayHello is called and returns a JSON response with the message "Hello from the server!". 2. Getting Started This chapter guides you through the basics of working with DotBridge in the DotApp Framework – from initialization to defining your first function, adding a front-end event, and handling the response in JavaScript. Register handlers with Router::bridge(['/path','/path/'], 'fnName', 'Examples:Forms@submit3!', Router::STATIC_ROUTE) or Bridge::listen($urls, 'fnName', $callback, Router::STATIC_ROUTE) in the module’s initialize(). 2.1 Initializing DotBridge DotBridge starts with the application. A unique session key is stored in _bridge.key. Register page handlers from a module with Bridge::listen() or Router::bridge(). Bridge AJAX requests POST to the current page URL; the template modifier url(/path) binds a call to another URL (invalid bound URL: HTTP 403, error_code 6). Include /assets/dotapp/dotapp.js on pages that use bridge events. 2.2 Defining a PHP Function On the server side, register a function with Bridge::listen(). The callback receives a $request object; payload fields are in $request->data(true)['data']. Return a string or an array (e.g. ['ok' => true, 'message' => '...']) — the framework wraps it as JSON { status: 1, body: }. Place handlers in a module’s initialize() or a module controller method. Example: use Dotsystems\App\Parts\Bridge; use Dotsystems\App\Parts\Router; $urls = ['/contact', '/contact/']; Bridge::listen($urls, "sendMessage", function ($request) { $data = $request->data(true)['data']; $message = $data["user.message"] ?? "No message"; return ["ok" => true, "message" => "Message received: " . $message]; }, Router::STATIC_ROUTE); The sendMessage function is now ready to be called from the front-end and will return a JSON response with the received message. 2.3 Adding a Front-End Event In HTML, use the attribute {{ dotbridge:on(event)="functionName(params)" }} to link an event (e.g., click, keyup) to a PHP function. Define inputs with {{ dotbridge:input="name" }} or the short form dotbridge="name" on plain HTML elements. Add modifiers such as rateLimit(60,5) or oneTimeUse to control behavior. Example: When the button is clicked, the value from the user.message input is sent to the sendMessage function with a limit of 5 requests per 60 seconds. 2.4 Handling the Response in JavaScript On the client side, include /assets/dotapp/dotapp.js and use $dotapp().bridge() to define before and after callbacks. The after callback receives body (your PHP return value). Use onResponseCode with $dotapp().parseReply() to read framework error envelopes (status_txt on HTTP 400/403/429). Example: $dotapp().bridge("sendMessage", "click") .before(function () { $dotapp("button").html("Sending..."); }) .after(function (body) { $dotapp("button").html("Done!"); alert(body.message || body); }) .onResponseCode(function (status, text) { var reply = $dotapp().parseReply(text); alert((reply && reply.status_txt) ? reply.status_txt : "Failed"); }, 429); Before sending, the button text changes to "Sending...", and after receiving the response, it displays the message from the PHP function. Rate-limit and other framework errors are handled via onResponseCode and parseReply. 3. Advanced Usage This chapter covers advanced features of DotBridge, such as validation filters, rate limiting, method chaining, and working with dynamic data. These tools enable the creation of more robust and secure applications with greater control over behavior. 3.1 Validation Filters DotBridge provides built-in validation filters for real-time input validation on the client side. Filters like email, url, phone, or password apply regular expressions and visual feedback (CSS classes) based on input validity. They are used in HTML via the {{ dotbridge:input="name(filter, args)" }} attribute. Available Arguments: filter: Name of the filter (e.g., email). start_checking_length: Minimum number of characters to start validation. class_ok: CSS class for valid input. class_bad: CSS class for invalid input. Example: The user.email input starts validation after 5 characters. If the email is valid, the valid-email class is added; if invalid, invalid-email. 3.2 Rate Limiting and Security Mechanisms DotBridge allows you to limit the number of requests using the rateLimit(seconds,clicks) parameters, protecting the application from abuse. Additional security features include oneTimeUse (single-use key) and regenerateId (key regeneration after each use). Usage: rateLimit(seconds,clicks): Maximum of clicks requests per seconds. Repeat the modifier for multiple windows (e.g. rateLimit(60,2) rateLimit(3600,5)). oneTimeUse: Key is valid for only one use. regenerateId: Generates a new key after each call. url(path): POST target URL (default: current page). internalID(id): Assign a stable internal ID to the bridge object. expireAt(timestamp): Expire the bridge key at a Unix timestamp. Example: 1. Example - The button allows a maximum of 10 clicks per minute, 100 per hour. 2. Example - The button allows only one click, and the listener is removed. 3. Example - The button regenerates its ID on each click. 3.3 Method Chaining In PHP, register with Bridge::listen(); optional before() / after() chain on that call. In JavaScript, use $dotapp().bridge() with before() and after(). Example: use Dotsystems\App\Parts\Bridge; use Dotsystems\App\Parts\Router; $urls = ['/process', '/process/']; Bridge::listen($urls, "processData", function ($request) { $data = $request->data(true)['data']; return ["result" => trim($data["value"] ?? "")]; }, Router::STATIC_ROUTE) ->before(function ($request) { return null; }) ->after(function ($request) { return null; }); // JavaScript $dotapp().bridge("processData", "click") .before(function () { $dotapp(".trigger").addClass("loading"); }) .after(function (body) { $dotapp(".trigger").removeClass("loading"); console.log(body.result); }); The handler trims input on the server; before and after hooks receive $request. On the client, a loading state is toggled around the call. 3.4 Working with Dynamic Data DotBridge allows sending and processing dynamic data from multiple inputs defined in HTML. Inputs use {{ dotbridge:input="name" }} or the short form dotbridge="name" and arrive in $request->data(true)['data'] on the server. Example: use Dotsystems\App\Parts\Bridge; use Dotsystems\App\Parts\Router; $urls = ['/account', '/account/']; Bridge::listen($urls, "saveUser", function ($request) { $data = $request->data(true)['data']; $name = $data["user.name"] ?? ""; $email = $data["user.email"] ?? ""; return ["ok" => true, "message" => "User $name ($email) saved"]; }, Router::STATIC_ROUTE); Upon clicking, the values from the user.name and user.email inputs are sent to the saveUser function and returned in the response. 4. Best Practices and Tips This chapter provides recommendations and tips for effectively and securely using DotBridge. It covers security optimization, debugging issues, and integration with other parts of the DotApp Framework. 4.1 Optimizing Security Security is a critical aspect when using DotBridge. The following recommendations will help minimize risks and ensure robust communication: Use rate limiting: Set rateLimit(seconds,count) for actions sensitive to repeated calls (e.g. rateLimit(60,2) rateLimit(3600,5) for multiple windows). Enable one-time keys: For critical operations (e.g., form submission), use oneTimeUse or regenerateId to prevent reuse of the same key. Validate inputs: Combine validation filters with additional server-side checks (e.g., filter_var()) to ensure consistent validation. Monitor sessions: Regularly check and clean old keys in _bridge.objects to avoid memory overflow. Use encryption: Leverage the built-in DotApp encryption (encrypt(), decrypt()) for sensitive data in communication. Example: use Dotsystems\App\Parts\Bridge; use Dotsystems\App\Parts\Router; $urls = ['/secure', '/secure/']; Bridge::listen($urls, "secureAction", function ($request) { return ["ok" => true, "message" => "Action executed securely"]; }, Router::STATIC_ROUTE); This example limits the action to 2 calls per 60 seconds and allows only one use of the key. 4.2 Debugging and Troubleshooting While working with DotBridge, you may encounter errors. Here are common issues and their solutions: CRC check failed (error_code 1): Verify that the data sent from the front-end hasn’t been modified. Check the integrity of the JavaScript code and network requests. Bridge key does not match (error_code 2): Ensure the session key (_bridge.key) matches what the client sends. This could be due to an expired session. Function not found (error_code 3): Confirm that the function is registered with Bridge::listen() or Router::bridge() and that the name matches the HTML call. Rate limit exceeded (error_code 4): You’ve exceeded the set limit. Adjust rateLimit(seconds,count) or inform the user to wait. Tip: Enable debugging in DotApp and inspect bridge POST responses in the browser’s developer tools (Network tab). The default POST target is the current page URL. 4.3 Integration with Other Parts of DotApp DotBridge is designed to work seamlessly with other DotApp components, such as Router, Request, and the database. Integration allows you to build complex applications with minimal effort. Integration Examples: With Router: Use Router::bridge($urls, $fnName, 'Module:Controller@method!', Router::STATIC_ROUTE) to bind handlers to page URLs. Signature: ($url, $function_name, $callback, $static = false). With Request: Handlers receive $request; payload fields are in $request->data(true)['data']. With Database: You can insert front-end data with DB::module('RAW'). Example with Database: use Dotsystems\App\Parts\Bridge; use Dotsystems\App\Parts\DB; use Dotsystems\App\Parts\Router; $urls = ['/notes', '/notes/']; Bridge::listen($urls, "saveEmail", function ($request) { $data = $request->data(true)['data']; $email = $data["user.email"] ?? ""; if (filter_var($email, FILTER_VALIDATE_EMAIL)) { DB::module('RAW')->q(function ($qb) use ($email) { $qb->insert('helloworld_notes', ['email' => $email]); })->execute( function ($result, $db, $exec) {}, function ($error) {} ); return ["ok" => true, "message" => "Email saved"]; } return ["ok" => false, "message" => "Invalid email"]; }, Router::STATIC_ROUTE); Upon clicking, the email is validated and saved to the database, returning a success or error response. --- # Database URL: https://dotapp.dev/documentation/database Databaser 1. Introduction 1.1. What is Databaser? 1.2. Key Features 1.3. RAW vs. ORM: When to Use Which Approach? 1.4. Support for Database Drivers (MySQLi, PDO) 1.5. Integrated Query Builder 1.6. Callbacks for SUCCESS and ERROR 2. Getting Started 2.1. Installing and Configuring Databaser 2.2. Adding a Database Connection 2.3. Choosing a Driver (MySQLi or PDO) 2.4. First Database Connection 3. Query Builder: Detailed Overview 3.1. Basic Principles of Query Builder 3.2. List of Query Builder Methods 3.3. Examples from Simple to Complex Queries 4. Working with Databaser in dotApp 4.1. Setting the Return Type (RAW vs. ORM) 4.2. Methods for Executing Queries 4.3. Working with ORM 4.4. Transactions 4.5. Debugging and Working with Output 5. Practical Examples 5.1. Basic CRUD Operations in RAW Mode 5.2. Basic CRUD Operations in ORM Mode 5.3. Advanced Examples with JOIN and Subquery 5.4. Working with Transactions 5.5. Debugging and Error Handling 6. Working with Schema Builder 6.1. Basic Principles of Schema Builder 6.2. Available Schema Builder Methods 6.3. Using Schema Builder 6.4. Advanced Examples 6.5. Notes and Limitations 7. Cache Driver Interface 7.1. What is Cache Driver Interface? 7.2. Components of Cache Driver Interface 7.3. Implementing a Custom Cache Driver 7.4. Using Cache Driver with Databaser 7.5. Example with Advanced Caching 7.6. Notes and Tips 8. Working with Entity 8.1. What is Entity? 8.2. Basic Entity usage 8.3. Relations 8.4. Creating an Entity 9. Working with Collections 9.1. What is Collection? 9.2. Basic use of Collection 9.3. Filter, map, and saving 10. Database schema 10.1. Database schema 10.2. What is schema management? 10.3. Basic principles 10.4. Using the schema 10.5. Schema methods 10.6. Practical examples 10.7. Notes 11. Case Study: E-commerce with ORM 11.1. Designing the Database Structure 11.2. Implementation in Databaser with ORM 11.3. Using Validation 11.4. Notes on the Case Study 12. Tips and Tricks 12.1. Query Optimization 12.2. Security (SQL Injection Prevention) 12.3. Extending Databaser with Custom Drivers 12.4. ORM relations 12.5. Collection map and save Databaser Run queries from a module controller. Entry points: DB::module('RAW') (arrays) or DB::module('ORM') (Entity/Collection). Read rows with all(), then $rows[0] ?? null when you need a single row. Always pass both callbacks to execute($ok, $err) — without an error callback, a failure throws. Schema changes belong in the module’s Installation.php. Module tables use the {modulename}_* naming pattern. 1. Introduction 1.1. What is Databaser? Databaser is a robust, flexible library for database work, built into DotApp PHP Framework 2.0. It provides a simple, safe, and efficient way to run both basic operations and advanced queries. Databaser removes the need to write raw SQL (while still allowing it) and offers a modern approach through an intuitive QueryBuilder and an optional ORM (Object-Relational Mapping) layer. The goal is to make database work easier for developers without sacrificing flexibility or performance. Databaser is part of the DotApp PHP Framework 2.0 core, so you do not install or configure it separately. After you define database connections in the framework, it is ready to use. 1.2. Key Features Databaser offers a wide set of features for working with databases: Simple SQL construction and execution: Prepared statements keep data work safe and straightforward. Multiple database connections: Define and switch between databases with stored credentials. Custom driver support: Besides the default drivers (MySQLi and PDO), you can implement your own database drivers. Optional ORM: Available for both MySQLi and PDO, with Entity (a single row) and Collection (a set of rows) classes that treat data as objects. Lazy loading and relations: Load related rows with relation methods on an Entity ($user->hasMany('shop_posts', 'user_id')). Advanced relations: Adjust the QueryBuilder inside a relation (for example add limit, orderBy, where) through an optional callback parameter. Validation: The ORM validates attributes during save(). Iterate a Collection and save each Entity individually. Integrated QueryBuilder: An intuitive query builder covering simple SELECTs through complex JOINs and subqueries. SUCCESS and ERROR callbacks: Every operation returns results and debug data through callbacks, which simplifies success and error handling. Transaction support: Straightforward transaction handling with automatic commit or rollback. 1.3. RAW vs. ORM: When to Use Which Approach? Databaser offers two main ways to work with data: RAW and ORM. The choice depends on your project’s needs: RAW MODE: Query results are returned directly (for example arrays or database resources). Ideal for simple applications, quick prototypes, or situations where you need full control over SQL. Example: A simple SELECT to list users without object mapping. Benefits: Fast execution, minimal overhead, full flexibility when writing queries. ORM MODE: Data is mapped to objects (Entity for one row, Collection for many rows), so you work with data as objects. Suitable for complex applications that need table relations, data validation, or object-oriented row handling. Example: Managing users and their posts (a HasMany relation) and saving changes automatically. Benefits: Object-oriented approach, relation support, straightforward data handling. When to use which approach? Choose RAW when you need fast performance and simple queries. Choose ORM when you work with complex data structures and want a cleaner object-oriented solution. 1.4. Support for Database Drivers (MySQLi, PDO) Databaser supports two main database drivers that cover most common needs: MySQLi QueryBuilder and ORM. A good fit for projects that already use MySQLi, or for simpler applications with MySQL databases. Supports all QueryBuilder and ORM features. PDO Support for multiple databases (MySQL, PostgreSQL, SQLite, and others). More flexible thanks to a dynamic DSN (Data Source Name), which lets you connect to different database types. Also supports QueryBuilder and ORM. Both drivers are designed to be interchangeable — code written for one driver works with the other without major changes, as long as you respect the specifics of the target database system. 1.5. Integrated Query Builder QueryBuilder is the heart of Databaser. It lets you build SQL with chainable methods, which makes safe, readable queries easier to write. It supports: Basic operations: select, insert, update, delete. Conditions: where, orWhere, nested conditions via a Closure. Table joins: join, leftJoin. Aggregations: groupBy, having. Sorting and limits: orderBy, limit, offset. Raw queries: raw with both question-mark placeholders (?) and named variables (:name). Table source: from when the table is not passed to select() or delete(). QueryBuilder manages prepared statements and bindings automatically, which protects against SQL injection. Every value used in a query (for example in where conditions or in data passed to insert) is escaped and replaced with placeholders (? or named variables :name). That reduces the risk of security issues and keeps the code easier to read. 1.6. Callbacks for SUCCESS and ERROR Databaser uses callbacks to handle results and errors. Every operation (for example execute(), save()) can take two optional callbacks: SUCCESS callback Runs when the operation succeeds. It receives three parameters: $result: The operation result (for example an array of data in RAW mode, or an object in ORM mode). $db: The Databaser instance, which you can use for further queries. $debug: Debug data (for example the generated SQL query and bindings). ERROR callback Runs when an error occurs. It also receives three parameters: $error: An array with error details (error — error text, errno — error code). $db: The Databaser instance for any follow-up operations. $debug: Debug data for analyzing the problem. This approach simplifies handling and lets you chain operations directly in callbacks. For example, if one query should immediately start another, call $db->q() from the SUCCESS callback. Success and error logic stay separate and easy to follow. Always pass both callbacks to execute($ok, $err). If you omit the ERROR callback, execute() throws on failure, so you can catch the exception with a try/catch block. If the ERROR callback is set, try/catch will not run for that failure — error handling is fully delegated to the callback. 2. Getting Started 2.1. Installing and Configuring Databaser Databaser is an integral part of DotApp PHP Framework 2.0, so you do not install it separately. Once the framework is set up in your project, Databaser is available through the DB:: facade. For module code, use DB::module('RAW') or DB::module('ORM'). This chapter assumes the framework is configured and ready to use. 2.2. Adding a Database Connection Databaser can add and manage multiple database connections. Register them in app/config.php with Config::addDatabase(). Example: Config::addDatabase( 'main', // Connection name 'localhost', // Host 'root', // Username 'password123', // Password 'my_database', // Database name 'utf8mb4', // Charset 'MYSQL', // Database type 'pdo' // Driver ); 2.3. Choosing a Driver (MySQLi or PDO) Databaser supports both MySQLi and PDO. The driver and the main database (maindb) are normally chosen in configuration, not in every query. In the samples, use DB::module('RAW') or DB::module('ORM'). Leave manual driver selection for advanced custom drivers. 2.4. First Database Connection After you define a connection in configuration, the framework uses the driver and the main connection automatically. You can check the connection like this: if (DB::isConnected()) { echo 'Database is connected.'; } Example of a first simple query: DB::module('RAW')->q(function ($qb) { $qb->select('*', 'shop_items'); }) ->execute( function ($result, $db, $debug) { echo "Generated query: " . $debug['query'] . "\n"; var_dump($result); }, function ($error, $db, $debug) { echo "Error: {$error['error']} (code: {$error['errno']})\n"; } ); Explanation DB::module('RAW'): Canonical entry. Driver and default database come from app/config.php. execute($ok, $err): Always pass both callbacks. Without $err a database error throws. q() (alias qb()): Starts QueryBuilder and defines the query (in this case SELECT * FROM shop_items). execute(): Runs the query with callbacks for success and error. $result: An array of results (in RAW mode). $debug: Contains the generated SQL query and other information. Output (example): Generated query: SELECT * FROM shop_items array(2) { [0] => array(3) { ["id"] => string(1) "1" ["name"] => string(4) "Jane" ["age"] => string(2) "25" } [1] => array(3) { ["id"] => string(1) "2" ["name"] => string(5) "Maria" ["age"] => string(2) "30" } } 3. Query Builder: Detailed Overview QueryBuilder is a core tool in Databaser. It lets you build SQL with chainable methods. The main advantages are simplicity, readability, and safety — it manages prepared statements and bindings automatically, which protects against SQL injection. This chapter covers how it works, the available methods, and examples from simple to complex queries. 3.1. Basic Principles of Query Builder QueryBuilder is an object of the Dotsystems\App\Parts\QueryBuilder class. You use it inside q() or qb() on the DB:: facade (typically DB::module('RAW')->q(...)). You build the query by calling methods in sequence; each method adds a part of the SQL statement (for example select, where, join). Finish the query with execute(). Read rows with all(), then take $rows[0] ?? null when you need a single row. Core characteristics: Chainability: Methods return the QueryBuilder instance, so you can chain them. Prepared statements: All values are escaped automatically and replaced with placeholders (?). Flexibility: Raw SQL is available through raw() for special cases. Debuggability: After execution, $debug contains the generated SQL and bindings. Basic usage example: DB::module('RAW')->q(function ($qb) { $qb->select('*', 'shop_items')->where('age', '>', 18); })->execute( function ($result, $db, $debug) { echo $debug['query']; // "SELECT * FROM shop_items WHERE age > ?" var_dump($debug['bindings']); // [18] var_dump($result); }, function ($error, $db, $debug) { echo "Error: {$error['error']} (code: {$error['errno']})\n"; } ); 3.2. List of Query Builder Methods Here is a detailed overview of the main QueryBuilder methods, with explanations and examples. 3.2.1. select The select() method defines which columns to read and from which table. Syntax: select($columns = '*', $table = null) Parameters: $columns: A string or an array of columns (for example 'id, name' or ['id', 'name']). $table: Table name (optional if you use from()). SQL equivalent: SELECT columns FROM table Example: $qb->select('id, name', 'shop_items'); // SQL: SELECT id, name FROM shop_items 3.2.2. insert The insert() method inserts a new row into a table. Syntax: insert($table, array $data) Parameters: $table: Table name. $data: Associative array of data (column => value). SQL equivalent: INSERT INTO table (columns) VALUES (values) Example: $qb->insert('shop_items', ['name' => 'Jane', 'age' => 25]); // SQL: INSERT INTO shop_items (name, age) VALUES (?, ?) // Bindings: ['Jane', 25] 3.2.3. update The update() and set() methods update existing rows. Syntax: update($table) + set(array $data) Parameters: $table: Table name. $data: Associative array of updated values. SQL equivalent: UPDATE table SET column = value Example: $qb->update('shop_items')->set(['age' => 26])->where('id', '=', 1); // SQL: UPDATE shop_items SET age = ? WHERE id = ? // Bindings: [26, 1] 3.2.4. delete The delete() method removes rows from a table. Syntax: delete($table = null) Parameters: $table: Table name (optional if it is defined elsewhere). SQL equivalent: DELETE FROM table Example: $qb->delete('shop_items')->where('id', '=', 1); // SQL: DELETE FROM shop_items WHERE id = ? // Bindings: [1] 3.2.5. where and orWhere The where() and orWhere() methods add conditions. Syntax: where($column, $operator = null, $value = null, $boolean = 'AND') Parameters: $column: A column or a Closure for nested conditions. $operator: Operator (for example =, >, <). $value: A value or a Closure for a subquery. $boolean: Logical join (default AND). SQL equivalent: WHERE column operator value Example: $qb->select('*', 'shop_items') ->where('age', '>', 18) ->orWhere('name', '=', 'Jane'); // SQL: SELECT * FROM shop_items WHERE age > ? OR name = ? // Bindings: [18, 'Jane'] 3.2.6. join (INNER, LEFT) The join() and leftJoin() methods join tables. Syntax: join($table, $first, $operator, $second, $type = 'INNER') Parameters: $table: A table or a subquery (QueryBuilder). $first: First column of the join condition. $operator: Join operator. $second: Second column of the join condition. $type: Join type (INNER, LEFT). SQL equivalent: INNER JOIN table ON condition Example: $qb->select('shop_items.name, shop_posts.title', 'shop_items') ->join('shop_posts', 'shop_items.id', '=', 'shop_posts.user_id'); // SQL: SELECT shop_items.name, shop_posts.title FROM shop_items INNER JOIN shop_posts ON shop_items.id = shop_posts.user_id 3.2.7. groupBy The groupBy() method groups results. Syntax: groupBy($columns) Parameters: $columns: A column or an array of columns. SQL equivalent: GROUP BY columns Example: $qb->select('age', 'shop_items')->groupBy('age'); // SQL: SELECT age FROM shop_items GROUP BY age 3.2.8. having The having() method filters grouped results. Syntax: having($column, $operator, $value) Parameters: $column: Column. $operator: Operator. $value: Value. SQL equivalent: HAVING column operator value Example: $qb->select('age', 'shop_items')->groupBy('age')->having('age', '>', 20); // SQL: SELECT age FROM shop_items GROUP BY age HAVING age > ? // Bindings: [20] 3.2.9. orderBy The orderBy() method sorts results. Syntax: orderBy($column, $direction = 'ASC') Parameters: $column: Column. $direction: Direction (ASC or DESC). SQL equivalent: ORDER BY column direction Example: $qb->select('*', 'shop_items')->orderBy('age', 'DESC'); // SQL: SELECT * FROM shop_items ORDER BY age DESC 3.2.10. limit and offset The limit() and offset() methods limit how many rows are returned. Syntax: limit($limit) + offset($offset) Parameters: $limit: Number of rows. $offset: Starting offset. SQL equivalent: LIMIT count OFFSET offset Example: $qb->select('*', 'shop_items')->limit(5)->offset(10); // SQL: SELECT * FROM shop_items LIMIT ? OFFSET ? // Bindings: [5, 10] 3.2.11. raw The raw() method lets you run a raw SQL query. Syntax: raw($sql, array $bindings = []) Parameters: $sql: Raw SQL string. $bindings: Array of values for placeholders. SQL equivalent: The query you pass in. Example: $qb->raw('SELECT * FROM shop_items WHERE age > ?', [18]); // SQL: SELECT * FROM shop_items WHERE age > ? // Bindings: [18] 3.2.12. from The from() method sets the table when you did not pass it to select(), delete(), or a similar method. Syntax: from($table) Parameters: $table: Table name. SQL equivalent: FROM table Example: $qb->select('id, name')->from('shop_items'); // SQL: SELECT id, name FROM shop_items 3.3. Examples from Simple to Complex Queries Simple select $qb->select('*', 'shop_items'); // SQL: SELECT * FROM shop_items select with a where condition $qb->select('name', 'shop_items')->where('age', '>', 18); // SQL: SELECT name FROM shop_items WHERE age > ? // Bindings: [18] Nested conditions (Closure) $qb->select('*', 'shop_items')->where(function ($qb) { $qb->where('age', '>', 18)->orWhere('name', '=', 'Jane'); }); // SQL: SELECT * FROM shop_items WHERE (age > ? OR name = ?) // Bindings: [18, 'Jane'] join with multiple tables $qb->select('shop_items.name, shop_posts.title', 'shop_items') ->join('shop_posts', 'shop_items.id', '=', 'shop_posts.user_id') ->leftJoin('shop_comments', 'shop_posts.id', '=', 'shop_comments.post_id'); // SQL: SELECT shop_items.name, shop_posts.title FROM shop_items // INNER JOIN shop_posts ON shop_items.id = shop_posts.user_id // LEFT JOIN shop_comments ON shop_posts.id = shop_comments.post_id Subquery as a value $qb->select('name', 'shop_items')->where('id', '=', function ($qb) { $qb->select('user_id', 'shop_posts')->where('title', '=', 'News'); }); // SQL: SELECT name FROM shop_items WHERE id = (SELECT user_id FROM shop_posts WHERE title = ?) // Bindings: ['News'] raw query with named variables $qb->raw('SELECT * FROM shop_items WHERE age > :age AND name = :name', [ 'age' => 18, 'name' => 'Jane' ]); // SQL: SELECT * FROM shop_items WHERE age > ? AND name = ? // Bindings: [18, 'Jane'] 4. Working with Databaser in DotApp This chapter covers practical use of Databaser in the DotApp Framework: setting the return type, running queries, working with ORM, managing transactions, and inspecting results. Databaser is designed for flexibility and simplicity, whether you prefer the RAW approach or the object-oriented ORM. 4.1. Setting the Return Type (RAW vs. ORM) Set the return type with DB::module('RAW') or DB::module('ORM') — not with a return() method. RAW: Returns raw data (for example an array of rows or a database result resource). This is the default. ORM: Returns data as objects (Entity for a single row, Collection for multiple rows). Syntax: DB::module($type) $type: The string 'RAW' or 'ORM' (case does not matter). Example — RAW: DB::module('RAW')->q(function ($qb) { $qb->select('*', 'shop_items'); })->execute( function ($result, $db, $debug) { var_dump($result); // Array of rows }, function ($error) { // execute() without this callback throws on error } ); Example — ORM: DB::module('ORM')->q(function ($qb) { $qb->select('*', 'shop_items'); })->execute( function ($result, $db, $debug) { var_dump($result); // Collection instance }, function ($error) { // execute() without this callback throws on error } ); You can change the return type before each query, so different parts of the application can use RAW or ORM as needed. 4.2. Methods for Executing Queries Databaser provides several methods for running queries built with QueryBuilder. Each method has a specific use. 4.2.1. execute() The execute() method is the most versatile — it runs the query and delivers results through callbacks. Syntax: execute($success = null, $error = null) Parameters: $success: Success callback (function ($result, $db, $debug)). $error: Error callback (function ($error, $db, $debug)). Always pass this callback; without it, execute() throws on error. Output: Depends on the return type (RAW: array/resource, ORM: Collection/Entity). Example: DB::module('RAW')->q(function ($qb) { $qb->select('*', 'shop_items')->where('age', '>', 18); })->execute( function ($result, $db, $debug) { echo "Query: " . $debug['query'] . "\n"; var_dump($result); }, function ($error, $db, $debug) { echo "Error: {$error['error']}\n"; } ); 4.2.2. first() — unsafe on empty results Do not call first() unguarded. Empty RAW triggers an undefined-index warning; empty ORM is fatal. Prefer all() and take index 0: $rows = DB::module('RAW')->q(function ($qb) { $qb->select('*')->from('shop_items')->where('id', '=', 1)->limit(1); })->all(); $row = $rows[0] ?? null; 4.2.3. all() The all() method returns every result row. This is also the safe way to read a single row: take $rows[0] ?? null. Syntax: all() Output: RAW — array of rows; ORM — Collection. Example: $users = DB::module('RAW')->q(function ($qb) { $qb->select('*', 'shop_items'); })->all(); foreach ($users as $user) { echo $user['name'] . "\n"; } 4.2.4. raw() The raw() method is a terminal that returns the driver result (for example a mysqli_result or PDO statement). Fetch rows from that result with DB::fetchArray(). Syntax: raw() Output: Depends on the driver (for example mysqli_result or a PDO statement). Example: $result = DB::module('RAW')->q(function ($qb) { $qb->select('*', 'shop_items'); })->raw(); while ($row = DB::fetchArray($result)) { echo $row['name'] . "\n"; } 4.3. Working with ORM ORM mode lets you work with rows as objects, which simplifies updates and relations between tables. 4.3.1. Entity and Collection Entity: Represents one table row. It exposes attributes that match columns, plus methods for updates and relations. Collection: A group of Entity objects with iteration and helpers such as filter(), map(), and pluck(). Example: $users = DB::module('ORM')->q(function ($qb) { $qb->select('*', 'shop_items'); })->all(); foreach ($users as $user) { echo $user->name . "\n"; // Collection yields Entity objects } 4.3.2. Saving data (save()) Entity::save($ok, $err) writes changes to the database. It returns void — always use the callbacks; do not write if ($entity->save()). Read a row with all() and $rows[0] ?? null before you save. Example: $rows = DB::module('ORM')->q(function ($qb) { $qb->select('*', 'shop_items')->where('id', '=', 1); })->all(); $user = $rows[0] ?? null; if ($user) { $user->age = 26; $user->save( function ($result, $db, $debug) { echo "User saved!\n"; }, function ($error, $db, $debug) { echo "Error: {$error['error']}\n"; } ); } 4.3.3. Relations (hasOne, hasMany) ORM supports relations between tables. Load related rows with a method call on the entity — not a magic property such as $user->posts: hasOne: One-to-one. hasMany: One-to-many. Example: $rows = DB::module('ORM')->q(function ($qb) { $qb->select('*', 'shop_items')->where('id', '=', 1); })->all(); $user = $rows[0] ?? null; $posts = $user ? $user->hasMany('shop_posts', 'user_id') : []; foreach ($posts as $post) { echo $post->title . "\n"; } 4.3.4. Lazy loading and Collection methods Related rows load when you call the relation method (lazy loading). Collection provides helpers such as filter(), map(), and pluck(). pluck('name') returns a Collection of that field’s values; call all() on it if you need a plain PHP array. Example: $users = DB::module('ORM')->q(function ($qb) { $qb->select('*', 'shop_items'); })->all(); $names = $users->pluck('name'); var_dump($names); 4.4. Transactions Databaser supports transactions so a group of writes either all succeed or all roll back. 4.4.1. transaction(), commit(), rollback() Manual control with DB::module('RAW')->transaction(), then commit() or rollback(): $db = DB::module('RAW'); $db->transaction(); $db->q(function ($qb) { $qb->insert('shop_items', ['name' => 'Jane']); })->execute( function ($result, $db, $debug) { $db->commit(); }, function ($error, $db, $debug) { echo "Error: {$error['error']}\n"; $db->rollback(); } ); Automatic transaction with transact(). The operations callback receives the Databaser instance plus success and error callbacks — pass both through to every execute() so the transaction can commit or roll back: DB::module('RAW')->transact(function ($db, $ok, $err) { $db->q(function ($qb) { $qb->insert('shop_items', ['name' => 'Jane']); })->execute($ok, $err); }, function ($result, $db, $debug) { echo "Transaction succeeded!\n"; }, function ($error, $db, $debug) { echo "Error: {$error['error']}\n"; }); 4.5. Debugging and Working with Output Each operation exposes three main values: result: The query result (RAW: array, ORM: objects). db: The Databaser instance for follow-up queries. debug: An array of information (for example query and bindings). The same payload also includes insert_id and affected_rows. Debug example: DB::module('RAW')->q(function ($qb) { $qb->select('*', 'shop_items')->where('age', '>', 18); })->execute( function ($result, $db, $debug) { echo "SQL: " . $debug['query'] . "\n"; echo "Bindings: " . implode(', ', $debug['bindings']) . "\n"; var_dump($result); }, function ($error, $db, $debug) { echo "Error: {$error['error']}\n"; } ); 5. Practical Examples This chapter shows practical Databaser usage in the DotApp Framework. We cover common CRUD operations (Create, Read, Update, Delete), advanced queries with join and subqueries, transactions, and error handling. After an insert or update, read the new ID and the affected-row count with $db->inserted_id() and $db->affected_rows() (underscores). The same values are also available as $execution_data['insert_id'] and $execution_data['affected_rows'] in the execute callbacks. 5.1. Basic CRUD Operations in RAW Mode Create: DB::module('RAW')->q(function ($qb) { $qb->insert('shop_items', ['name' => 'Jane', 'age' => 25]); })->execute( function ($result, $db, $debug) { $id = $db->inserted_id(); // ID of the new row echo "New user with ID: $id has been created.\n"; }, function ($error, $db, $debug) { echo "Error: {$error['error']}\n"; } ); Read: DB::module('RAW')->q(function ($qb) { $qb->select('*', 'shop_items')->where('age', '>', 20); })->execute( function ($result, $db, $debug) { foreach ($result as $user) { echo "Name: {$user['name']}, Age: {$user['age']}\n"; } }, function ($error, $db, $debug) { echo "Error: {$error['error']}\n"; } ); Update: DB::module('RAW')->q(function ($qb) { $qb->update('shop_items')->set(['age' => 26])->where('name', '=', 'Jane'); })->execute( function ($result, $db, $debug) { $rows = $db->affected_rows(); // Number of affected rows echo "$rows row(s) updated.\n"; }, function ($error, $db, $debug) { echo "Error: {$error['error']}\n"; } ); Delete: DB::module('RAW')->q(function ($qb) { $qb->delete('shop_items')->where('name', '=', 'Jane'); })->execute( function ($result, $db, $debug) { $rows = $db->affected_rows(); echo "$rows row(s) deleted.\n"; }, function ($error, $db, $debug) { echo "Error: {$error['error']}\n"; } ); 5.2. Basic CRUD Operations in ORM Mode Create: DB::module('ORM')->q(function ($qb) { $qb->insert('shop_items', ['name' => 'Maria', 'age' => 30]); })->execute( function ($result, $db, $debug) { $id = $db->inserted_id(); $items = $db->q(function ($qb) use ($id) { $qb->select('*', 'shop_items')->where('id', '=', $id); })->all(); $user = $items[0] ?? null; if ($user) { echo "Created user: {$user->name}\n"; } }, function ($error, $db, $debug) { echo "Error: {$error['error']}\n"; } ); Read: $users = DB::module('ORM')->q(function ($qb) { $qb->select('*', 'shop_items'); })->all(); foreach ($users as $user) { echo "Name: {$user->name}, Age: {$user->age}\n"; } Update: $rows = DB::module('ORM')->q(function ($qb) { $qb->select('*', 'shop_items')->where('name', '=', 'Maria'); })->all(); $user = $rows[0] ?? null; if ($user) { $user->age = 31; $user->save( function ($result, $db, $debug) use ($user) { echo "User {$user->name} updated.\n"; }, function ($error, $db, $debug) { echo "Error: {$error['error']}\n"; } ); } Delete: $rows = DB::module('ORM')->q(function ($qb) { $qb->select('*', 'shop_items')->where('name', '=', 'Maria'); })->all(); $user = $rows[0] ?? null; if ($user) { DB::module('RAW')->q(function ($qb) use ($user) { $qb->delete('shop_items')->where('id', '=', $user->id); })->execute( function ($result, $db, $debug) { $rows = $db->affected_rows(); echo "$rows row(s) deleted.\n"; }, function ($error, $db, $debug) { echo "Error: {$error['error']}\n"; } ); } 5.3. Advanced Examples with JOIN and Subquery JOIN across tables: DB::module('RAW')->q(function ($qb) { $qb->select('shop_items.name, shop_posts.title', 'shop_items') ->join('shop_posts', 'shop_items.id', '=', 'shop_posts.user_id') ->where('shop_items.age', '>', 25); })->execute( function ($result, $db, $debug) { foreach ($result as $row) { echo "User: {$row['name']}, Post: {$row['title']}\n"; } }, function ($error, $db, $debug) { echo "Error: {$error['error']}\n"; } ); Subquery in ORM: DB::module('ORM')->q(function ($qb) { $qb->select('*', 'shop_items') ->where('id', '=', function ($subQb) { $subQb->select('user_id', 'shop_posts') ->where('title', '=', 'News'); }); })->execute( function ($users, $db, $debug) { foreach ($users as $user) { echo "User with News post: {$user->name}\n"; } }, function ($error, $db, $debug) { echo "Error: {$error['error']}\n"; } ); 5.4. Working with Transactions Automatic transaction: DB::module('RAW')->transact(function ($db, $ok, $err) { $db->q(function ($qb) { $qb->insert('shop_items', ['name' => 'Peter', 'age' => 28]); })->execute(function ($result, $db, $debug) use ($ok, $err) { $id = $db->inserted_id(); $db->q(function ($qb) use ($id) { $qb->insert('shop_posts', ['user_id' => $id, 'title' => 'First post']); })->execute($ok, $err); }, $err); }, function ($result, $db, $debug) { echo "Transaction succeeded. Last insert ID: " . $db->inserted_id() . "\n"; }, function ($error, $db, $debug) { echo "Transaction error: {$error['error']}\n"; }); Manual transaction: $db = DB::module('RAW'); $db->transaction(); $db->q(function ($qb) { $qb->insert('shop_items', ['name' => 'Anna', 'age' => 22]); })->execute( function ($result, $db, $debug) { $id = $db->inserted_id(); $db->q(function ($qb) use ($id) { $qb->insert('shop_posts', ['user_id' => $id, 'title' => 'Test']); })->execute( function ($result, $db, $debug) { $db->commit(); echo "Transaction completed.\n"; }, function ($error, $db, $debug) { $db->rollback(); echo "Rollback: {$error['error']}\n"; } ); }, function ($error, $db, $debug) { $db->rollback(); echo "Error: {$error['error']}\n"; } ); 5.5. Debugging and Error Handling Debugging a query: DB::module('RAW')->q(function ($qb) { $qb->select('*', 'shop_items')->where('age', '>', 18); })->execute( function ($result, $db, $debug) { echo "SQL: " . $debug['query'] . "\n"; echo "Bindings: " . implode(', ', $debug['bindings']) . "\n"; echo "Affected rows: " . $db->affected_rows() . "\n"; }, function ($error, $db, $debug) { echo "Error: {$error['error']} (code: {$error['errno']})\n"; echo "SQL: " . $debug['query'] . "\n"; } ); Handling an error: DB::module('RAW')->q(function ($qb) { $qb->select('*', 'missing_table'); // Invalid query })->execute( function ($result, $db, $debug) { echo "Success\n"; }, function ($error, $db, $debug) { echo "Error: {$error['error']}\n"; // Follow-up query on a table that exists $db->q(function ($qb) { $qb->select('*', 'shop_items'); })->execute( function ($result, $db, $debug) { echo "Recovery query succeeded.\n"; }, function ($error, $db, $debug) { echo "Error: {$error['error']}\n"; } ); } ); 6. Working with SchemaBuilder SchemaBuilder is a Databaser tool for defining and managing database structure. It lets you create, alter, and drop tables from PHP without writing raw DDL by hand. It is integrated with QueryBuilder through createTable(), alterTable(), and dropTable(). This chapter covers its methods, arguments, and practical examples. 6.1. SchemaBuilder fundamentals SchemaBuilder is the class Dotsystems\App\Parts\SchemaBuilder. You receive it in the callback of createTable(), alterTable(), and related helpers. Those helpers are called from QueryBuilder inside q() or from schema(). The goal is a programmatic way to define tables, columns, indexes, and foreign keys. The resulting statements are converted to SQL and executed through the active driver (MySQLi or PDO). Key characteristics: Chainable methods: Like QueryBuilder, SchemaBuilder is designed for chaining. Abstraction: It works independently of the database driver, although some features depend on the engine. Simplicity: You can define a schema without writing full SQL syntax. 6.2. SchemaBuilder methods Overview of the main methods, their arguments, and examples. Column helpers return a column definition. Chain modifiers on that object: nullable(), default(), unsigned() (MySQL only), and comment(). Do not pass a nullable flag as a trailing argument to string() or integer(). There is no timestamps() helper — declare created_at and updated_at yourself with datetime() (or timestamp() when you explicitly want a TIMESTAMP column). Other column helpers include text(), decimal($name, $precision = 10, $scale = 2), timestamp(), date(), boolean(), bigInteger(), and tinyInteger(). 6.2.1. id() Adds a BIGINT AUTO_INCREMENT primary key. Syntax: id($name = 'id') Parameters: $name: Column name (default 'id'). SQL equivalent: id BIGINT NOT NULL AUTO_INCREMENT plus a primary-key constraint. On MySQL you may chain ->unsigned(). Example: $schema->id(); // Creates the `id` column 6.2.2. string() Adds a VARCHAR column. Syntax: string($name, $length = 255) Parameters: $name: Column name. $length: Length (default 255). Allow NULL by chaining nullable(): $schema->string('name', 100)->nullable(). SQL equivalent: VARCHAR(length) [NOT NULL | NULL] Example: $schema->string('name', 100)->nullable(); // `name` VARCHAR(100) NULL 6.2.3. integer() Adds an INT column. Syntax: integer($name) Parameters: $name: Column name. Allow NULL by chaining nullable(): $schema->integer('age')->nullable(). SQL equivalent: INT [NOT NULL | NULL] Example: $schema->integer('age'); // `age` INT NOT NULL $schema->integer('age')->nullable(); // `age` INT NULL 6.2.4. created_at / updated_at Declare datetime columns with datetime(). Do not call timestamps() — that method does not exist. Use timestamp() only when you want a TIMESTAMP column. Syntax: datetime('created_at') / datetime('updated_at') SQL equivalent: created_at DATETIME NOT NULL updated_at DATETIME NOT NULL Example: $schema->datetime('created_at'); $schema->datetime('updated_at'); 6.2.5. foreign() Adds a foreign key. Syntax: foreign($column, $name = null) then chain ->references($col)->on($table)->onDelete($action). Parameters: $column: Local column that holds the foreign key. $name: Optional constraint name. Chain references(), on(), and onDelete() on the object returned by foreign(). SQL equivalent: FOREIGN KEY (column) REFERENCES table (references) ON DELETE CASCADE Example: $schema->foreign('user_id')->references('id')->on('shop_items')->onDelete('CASCADE'); 6.2.6. index() Adds an index on one or more columns. Syntax: index($columns, $name = null) Parameters: $columns: Column name or an array of column names. $name: Optional index name. SQL equivalent: INDEX (column) Example: $schema->index('name'); 6.2.7. addColumn() (for ALTER TABLE) Adds a new column to an existing table. Syntax: addColumn($name, $type, $length = null, $nullable = false, $default = null, $comment = null) Parameters: $name: Column name. $type: Type (for example VARCHAR, INT). $length: Length (optional). $nullable: Allow NULL (default false). $default: Default value (optional). $comment: Column comment (optional). SQL equivalent: ADD column type [length] [NOT NULL | NULL] Example: $schema->addColumn('email', 'VARCHAR', 150, true); 6.2.8. dropColumn() (for ALTER TABLE) Removes a column from a table. Syntax: dropColumn($name) Parameters: $name: Column name. SQL equivalent: DROP COLUMN column Example: $schema->dropColumn('email'); 6.3. Using SchemaBuilder Use SchemaBuilder with QueryBuilder methods createTable(), alterTable(), and dropTable(). Run them inside DB::module('RAW')->q(function ($qb) { ... })->execute($ok, $err). Alternatively, wrap the same QueryBuilder work in DB::module('RAW')->schema($callback, $success, $error). Always pass both callbacks to execute(), and always pass an error callback to schema(). 6.3.1. Creating a table createTable() creates a new table. Example: DB::module('RAW')->q(function ($qb) { $qb->createTable('shop_items', function ($schema) { $schema->id(); $schema->string('name', 50); $schema->integer('age')->nullable(); $schema->datetime('created_at'); $schema->datetime('updated_at'); $schema->index('name'); }); })->execute( function ($result, $db, $debug) { echo "Table 'shop_items' was created.\n"; echo "SQL: {$debug['query']}\n"; }, function ($error, $db, $debug) { echo "Error: {$error['error']}\n"; } ); The same DDL through schema(): DB::module('RAW')->schema( function ($qb) { $qb->createTable('shop_items', function ($schema) { $schema->id(); $schema->string('name', 50); $schema->integer('age')->nullable(); $schema->datetime('created_at'); $schema->datetime('updated_at'); $schema->index('name'); }); }, function ($result, $db, $debug) { echo "Table 'shop_items' was created.\n"; }, function ($error, $db, $debug) { echo "Error: {$error['error']}\n"; } ); Generated SQL: CREATE TABLE shop_items ( `id` BIGINT NOT NULL AUTO_INCREMENT, `name` VARCHAR(50) NOT NULL, `age` INT NULL, `created_at` DATETIME NOT NULL, `updated_at` DATETIME NOT NULL, INDEX `idx_name` (`name`), CONSTRAINT `pk_id` PRIMARY KEY (`id`) ) 6.3.2. Altering a table alterTable() changes an existing table. Example: DB::module('RAW')->q(function ($qb) { $qb->alterTable('shop_items', function ($schema) { $schema->addColumn('email', 'VARCHAR', 100, true); $schema->foreign('user_id')->references('id')->on('shop_items')->onDelete('CASCADE'); $schema->dropColumn('age'); }); })->execute( function ($result, $db, $debug) { echo "Table 'shop_items' was altered.\n"; echo "SQL: {$debug['query']}\n"; }, function ($error, $db, $debug) { echo "Error: {$error['error']}\n"; } ); Generated SQL: ALTER TABLE shop_items ADD `email` VARCHAR(100) NULL, ADD FOREIGN KEY (`user_id`) REFERENCES `shop_items` (`id`) ON DELETE CASCADE, DROP COLUMN `age` 6.3.3. Dropping a table dropTable() removes a table. Example: DB::module('RAW')->q(function ($qb) { $qb->dropTable('shop_items'); })->execute( function ($result, $db, $debug) { echo "Table 'shop_items' was dropped.\n"; }, function ($error, $db, $debug) { echo "Error: {$error['error']}\n"; } ); Generated SQL: DROP TABLE shop_items 6.4. Advanced examples Creating a table with foreign keys: DB::module('RAW')->q(function ($qb) { $qb->createTable('shop_posts', function ($schema) { $schema->id(); $schema->string('title', 200); $schema->integer('user_id'); $schema->foreign('user_id')->references('id')->on('shop_items')->onDelete('CASCADE'); $schema->datetime('created_at'); $schema->datetime('updated_at'); }); })->execute( function ($result, $db, $debug) { echo "Table 'shop_posts' created.\n"; }, function ($error, $db, $debug) { echo "Error: {$error['error']}\n"; } ); Bulk schema change in a transaction: DB::module('RAW')->transact( function ($db, $commitOnSuccess, $rollbackOnError) { $db->q(function ($qb) { $qb->createTable('shop_items', function ($schema) { $schema->id(); $schema->string('name'); }); })->execute($commitOnSuccess, $rollbackOnError); $db->q(function ($qb) { $qb->createTable('shop_posts', function ($schema) { $schema->id(); $schema->integer('user_id'); $schema->foreign('user_id')->references('id')->on('shop_items')->onDelete('CASCADE'); }); })->execute($commitOnSuccess, $rollbackOnError); }, function ($result, $db, $debug) { echo "Schema change succeeded.\n"; }, function ($error, $db, $debug) { echo "Error: {$error['error']}\n"; } ); 6.5. Notes and limitations Compatibility: Some features (for example ON DELETE CASCADE) do not behave the same on every engine. SQLite in particular has limited support for dropping columns, indexes, and foreign keys. Transactions: For larger schema changes use DB::module('RAW')->transact(...) so the work stays consistent. Debugging: Always inspect $debug['query'] (the third execute argument) to verify the generated SQL. Exceptions: SchemaBuilder throws \InvalidArgumentException for invalid identifiers, unsupported types, and missing foreign-key targets. Wrap DDL in try/catch. 7. CacheDriverInterface Databaser in the DotApp framework can cache query results so repeated requests for the same data skip the database. Query caching is enabled when Config::db('cache') === true. A custom store must expose get and set; attach it with DB::module()->cache($driverObject) (prefer that over DB::cache()). For ORM writes you also need deleteKeys(). This chapter describes the expected driver contract and how to use it with Databaser. 7.1. What is CacheDriverInterface? CacheDriverInterface is the contract for storing and loading cached query results. Databaser can talk to any backing store (Memcached, Redis, the filesystem) as long as your object implements the methods below. After you assign a driver with cache(), Databaser tries get() before running a query and set() after a successful execution. Important: Entity::save() with cache enabled requires deleteKeys() on the driver. No shipped driver implements deleteKeys(). Keep db.cache off unless you supply a custom driver that implements all four methods, including deleteKeys(). Benefits: Lower database load. Faster access to frequently requested data. Flexibility — you can use any cache backend. 7.2. CacheDriverInterface methods The interface defines four methods. Query caching uses get and set. Invalidation on Entity::save() also requires deleteKeys(): interface CacheDriverInterface { public function get($key); public function set($key, $value, $ttl = null); public function delete($key); public function deleteKeys($pattern); } 7.2.1. get($key) Loads a value from cache by key. Parameter: $key: String — unique key for the stored data. Return value: The stored value, or null if the key does not exist. Purpose: Databaser calls this method to check whether the query result is already cached. 7.2.2. set($key, $value, $ttl = null) Stores a value in cache under the given key. Parameters: $key: String — storage key. $value: Data to store (array, object, and so on). $ttl: Lifetime in seconds (optional; null means no expiry at the driver level). Databaser always passes 3600. Return value: None (or true/false depending on the implementation). Purpose: After a successful query, Databaser stores the result in cache. 7.2.3. delete($key) Removes a single key from cache. Parameter: $key: String — key to remove. Return value: None (or true/false). Purpose: Explicit deletion of one cache entry. 7.2.4. deleteKeys($pattern) Removes multiple keys that match a pattern. Parameter: $pattern: String — key pattern (for example "shop_items:*"). Return value: None (or the number of deleted keys). Purpose: Databaser calls this method when data changes (for example Entity::save() in ORM) so related cache entries are invalidated. If a cache driver is set and this method is missing, save() throws. 7.3. Implementing a custom cache driver Example of a simple file-based cache driver. Treat this as a sample custom driver, not a shipped framework class: class FileCacheDriver implements CacheDriverInterface { private $cacheDir; public function __construct($cacheDir = '/tmp/cache') { $this->cacheDir = $cacheDir; if (!is_dir($cacheDir)) { mkdir($cacheDir, 0777, true); } } public function get($key) { $file = $this->cacheDir . '/' . md5($key); if (file_exists($file)) { $data = unserialize(file_get_contents($file)); if ($data['expires'] === null || $data['expires'] > time()) { return $data['value']; } unlink($file); // Expired — remove it } return null; } public function set($key, $value, $ttl = null) { $file = $this->cacheDir . '/' . md5($key); $expires = $ttl ? time() + $ttl : null; $data = ['value' => $value, 'expires' => $expires]; file_put_contents($file, serialize($data)); return true; } public function delete($key) { $file = $this->cacheDir . '/' . md5($key); if (file_exists($file)) { unlink($file); return true; } return false; } public function deleteKeys($pattern) { $count = 0; foreach (glob($this->cacheDir . '/*') as $file) { $key = basename($file); if (fnmatch($pattern, $key)) { unlink($file); $count++; } } return $count; } } Explanation: get(): Reads data from a file if it has not expired. set(): Writes data to a file with an optional TTL. delete(): Removes a specific file. deleteKeys(): Removes files matching a pattern (uses fnmatch). 7.4. Using a cache driver with Databaser Enable query caching with Config::db('cache') === true. Then assign your store with DB::module()->cache($driverObject). The object must expose get($key) and set($key, $value, $lifetime). Prefer that over DB::cache(). $cacheDriver = new FileCacheDriver('/tmp/myapp_cache'); DB::module()->cache($cacheDriver); // Example query with caching DB::module('RAW')->q(function ($qb) { $qb->select('*', 'shop_items')->where('age', '>', 18); })->execute( function ($result, $db, $execution_data) { echo "Results (from cache or DB):\n"; var_dump($result); // On a cache hit, $execution_data is an empty array. }, function ($error, $db, $execution_data) { echo "Error: {$error['error']}\n"; } ); How it works: Databaser builds a key in the form "{table}:{returnType}:" . md5($query . serialize($bindings)) (for example shop_items:RAW: followed by the hash). It checks cache with get(). On a hit it returns the stored value without querying the database and delivers an empty $execution_data to the success callback. On a miss it runs the query and stores the result with set(). TTL is hardcoded to 3600 seconds. On an ORM update such as Entity::save() it invalidates related keys with deleteKeys(). 7.5. Advanced caching example Caching with ORM and invalidation: $cacheDriver = new FileCacheDriver(); DB::module()->cache($cacheDriver); $rows = DB::module('ORM')->q(function ($qb) { $qb->select('*', 'shop_items'); })->all(); $user = $rows[0] ?? null; if ($user !== null) { $user->age = 40; $user->save( function ($result, $db, $execution_data) { echo "Item saved, cache invalidated.\n"; }, function ($error, $db, $execution_data) { echo "Error: {$error['error']}\n"; } ); } What happens: The first query stores the Collection in cache. On save(), deleteKeys("shop_items:ORM:*") runs and invalidates all ORM cache entries for shop_items. If the assigned driver has no deleteKeys(), save() throws. Keep db.cache off unless your custom driver implements all four methods. 7.6. Notes and tips TTL: Databaser stores query results for 3600 seconds. Key format: Keys use "{table}:{returnType}:" . md5(...), so patterns such as "shop_items:*" match a table's entries. Cache hits: The success callback still runs, but $execution_data is empty. deleteKeys(): No shipped driver implements it. Keep Config::db('cache') off unless you supply a custom driver that implements get, set, delete, and deleteKeys(). Performance: For production, prefer a fast store such as Redis over files — still only after that store implements the four methods above. Testing: Verify that deleteKeys() actually invalidates cache so you do not serve stale rows after save(). 8. Working with Entity An Entity represents one table row in the ORM module. Obtain entities through DB::module('ORM'), but read the first row safely with all() and $rows[0] ?? null. Note on ORM relations: with(), whereHas(), and withCount() only store state in DotApp PHP Framework 2.0 and do not affect SQL. Do not present them as eager loading. Load relations by calling Entity methods, for example $user->hasMany('shop_posts', 'user_id'). 8.2. Basic Entity usage $rows = DB::module('ORM')->q(function ($qb) { $qb->select('*', 'shop_items')->where('id', '=', 1)->limit(1); })->all(); $user = $rows[0] ?? null; if ($user) { echo $user->name; $user->age = 26; $user->save( function ($result, $db, $debug) { echo "User saved.\n"; }, function ($error, $db, $debug) { echo "Error: {$error['error']}\n"; } ); } 8.3. Relations Relations are methods called on an entity. The optional callback can adjust the related query, for example with where(), orderBy(), or limit(). $rows = DB::module('ORM')->q(function ($qb) { $qb->select('*', 'shop_items')->where('id', '=', 1)->limit(1); })->all(); $user = $rows[0] ?? null; $posts = $user ? $user->hasMany('shop_posts', 'user_id', null, function ($qb) { $qb->orderBy('created_at', 'DESC')->limit(2); }) : []; foreach ($posts as $post) { echo $post->title . "\n"; } Relation methods available on Entity: hasOne($relatedTable, $foreignKey, $localKey = null, $callback = null) → Entity|null belongsTo($relatedTable, $foreignKey, $ownerKey = null, $callback = null) → Entity|null hasMany($relatedTable, $foreignKey, $localKey = null, $callback = null) → Collection morphOne($relatedTable, $typeField, $idField, $typeValue, $localKey = null, $callback = null) → Entity|null morphMany($relatedTable, $typeField, $idField, $typeValue, $localKey = null, $callback = null) → Collection morphTo($name = null, $type = null, $id = null, $ownerKey = null) is also available and returns Entity|null Polymorphic relationship with a filter A polymorphic relation stores the parent type and id on the related row. Pass a callback to filter, order, or limit the related query. Read the parent with all() and $rows[0] ?? null: $rows = DB::module('ORM')->q(function ($qb) { $qb->select('*', 'shop_items')->where('id', '=', 1)->limit(1); })->all(); $item = $rows[0] ?? null; if ($item) { $recentImages = $item->morphMany('shop_images', 'imageable_type', 'imageable_id', 'shop_items', null, function ($qb) { $qb->orderBy('created_at', 'DESC')->limit(3); }); foreach ($recentImages as $image) { echo "Latest image: {$image->url}\n"; } } 8.4. Inserting a new Entity $item = DB::newEntity(); $item->table('shop_items'); $item->name = 'Jane Novak'; $item->age = 30; $item->save( function ($result, $db, $debug) { echo "New record created with ID: {$db->inserted_id()}.\n"; }, function ($error, $db, $debug) { echo "Error: {$error['error']}\n"; } ); 9. Working with Collection A Collection is a set of entities returned by all(). Use iteration, filter(), map(), pluck(), toArray(), and count(). Do not use saveAll(); Entity::save() returns void, so save each entity individually and always pass an error callback. 9.2. Basic Collection usage $items = DB::module('ORM')->q(function ($qb) { $qb->select('*', 'shop_items'); })->all(); foreach ($items as $item) { echo "Name: {$item->name}\n"; } $allItems = $items->all(); $first = $allItems[0] ?? null; 9.3. Filter, map, and individual saves $items = DB::module('ORM')->q(function ($qb) { $qb->select('*', 'shop_items'); })->all(); $active = $items->filter(function ($item) { return (int) $item->active === 1; }); foreach ($active as $item) { $item->checked_at = date('Y-m-d H:i:s'); $item->save( null, function ($error, $db, $debug) { echo "Error: {$error['error']}\n"; } ); } 10. Database schema You define table structure in code with SchemaBuilder and, in a module, through a versioned Installation.php installer. You can create, alter, and drop tables and columns. Wrap batch operations in a transaction with transact(). Never call DB::migrate(). There is no timestamps() helper — add datetime() columns explicitly when you need them. 10.2. What is schema management? You define tables and relations programmatically. In Databaser this is done with SchemaBuilder; wrap batch changes in a transaction with transact(). Main advantages: Automation: Database changes live in code and can be versioned. Transactions: Batch operations are safe and reversible on error. Multi-platform: Support for different drivers (MySQLi, PDO) with syntax adapted to the database. 10.3. Basic principles Schema management in Databaser rests on these principles: SchemaBuilder: Definition of tables and columns (for example id(), string(), foreign(), datetime()). Installation.php: Versioned install and uninstall of module tables, guarded with self::alreadyDone and self::markDone. Transactions: Batch changes via transact(), where several operations run as one unit. Driver support: MySQLi and PDO adapt syntax to the database type (for example MySQL, PostgreSQL, SQLite). 10.4. Using the schema Define table structure and apply it. Always pass an error callback to schema(). Example of creating a table: DB::module('RAW')->schema(function ($schema) { $schema->createTable('shop_items', function ($table) { $table->id(); $table->string('name'); }); }, function ($result, $db, $debug) { echo "Table 'shop_items' was created successfully.\n"; }, function ($error, $db, $debug) { echo "Error: {$error['error']}\n"; }); Output: Table 'shop_items' was created successfully. Batch change with a transaction (several tables): DB::module('RAW')->transact(function ($db) { $db->q(function ($qb) { $qb->createTable('shop_items', function ($schema) { $schema->id(); $schema->string('name'); }); })->execute(null, function ($error) { echo "Error: {$error['error']}\n"; }); $db->q(function ($qb) { $qb->createTable('shop_posts', function ($schema) { $schema->id(); $schema->integer('user_id'); $schema->foreign('user_id')->references('id')->on('shop_items')->onDelete('CASCADE'); }); })->execute(null, function ($error) { echo "Error: {$error['error']}\n"; }); }, function ($result, $db, $debug) { echo "Schema change succeeded.\n"; }, function ($error, $db, $debug) { echo "Error: {$error['error']}\n"; }); Output: Schema change succeeded. 10.5. Available schema methods Databaser provides the following methods for working with schema: 10.5.1. schema($callback, $success, $error) Defines and runs a single schema operation (for example, creating a table). Always pass the error callback. Syntax: schema(callable $callback, callable $success = null, callable $error = null) Parameters: $callback: Closure that defines the operation through SchemaBuilder. $success: Callback on success. $error: Callback on error (always pass this). Example: DB::module('RAW')->schema(function ($schema) { $schema->createTable('shop_items', function ($table) { $table->id(); $table->string('email', 100); }); }, function ($result, $db, $debug) { echo "Table created.\n"; }, function ($error, $db, $debug) { echo "Error: {$error['error']}\n"; }); Output: Table created. 10.5.2. Module schema with Installation.php Create and version module tables from Installation.php. Guard each version with self::alreadyDone and record it with self::markDone. Never call DB::migrate(). DB::module('RAW')->q(function ($qb) { $qb->raw( "CREATE TABLE IF NOT EXISTS `shop_items` ( `id` INT NOT NULL AUTO_INCREMENT, `title` VARCHAR(200) NOT NULL, `created_at` DATETIME NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4", [] ); })->execute( function () { /* self::markDone('1.0.0'); */ }, function ($error) { \Dotsystems\App\Parts\Logger::use()->error('schema failed', $error); } ); 10.5.3. transact($operations, $success, $error) Runs a batch of schema changes inside a transaction. Call it on the module instance: DB::module('RAW')->transact(...). Syntax: transact(callable $operations, callable $success = null, callable $error = null) Parameters: $operations: Closure with several schema operations. The first argument is the module instance ($db). $success: Callback on success (committed). $error: Callback on error (rolled back). Always pass this. Example: DB::module('RAW')->transact(function ($db) { $db->q(function ($qb) { $qb->createTable('shop_comments', function ($schema) { $schema->id(); $schema->integer('post_id'); }); })->execute(null, function ($error) { echo "Error: {$error['error']}\n"; }); }, function ($result, $db, $debug) { echo "Batch schema change succeeded.\n"; }, function ($error, $db, $debug) { echo "Error: {$error['error']}\n"; }); Output: Batch schema change succeeded. 10.5.4. SchemaBuilder::createTable($table, $callback) Creates a new table with a defined structure. Syntax: createTable(string $table, callable $callback) Example: DB::module('RAW')->schema(function ($schema) { $schema->createTable('shop_products', function ($table) { $table->id(); $table->string('name'); $table->decimal('price', 8, 2); }); }, function ($result, $db, $debug) { echo "Table 'shop_products' created.\n"; }, function ($error, $db, $debug) { echo "Error: {$error['error']}\n"; }); 10.5.5. SchemaBuilder::alterTable($table, $callback) Alters an existing table (for example, adds a column). Syntax: alterTable(string $table, callable $callback) Example: DB::module('RAW')->schema(function ($schema) { $schema->alterTable('shop_items', function ($table) { $table->addColumn('age', 'INT', null, true); }); }, function ($result, $db, $debug) { echo "Column added.\n"; }, function ($error, $db, $debug) { echo "Error: {$error['error']}\n"; }); 10.5.6. SchemaBuilder::dropTable($table) Drops a table. Syntax: dropTable(string $table) Example: DB::module('RAW')->schema(function ($schema) { $schema->dropTable('shop_items'); }, function ($result, $db, $debug) { echo "Table dropped.\n"; }, function ($error, $db, $debug) { echo "Error: {$error['error']}\n"; }); 10.6. Practical examples Creating tables with a foreign key: DB::module('RAW')->transact(function ($db) { $db->q(function ($qb) { $qb->createTable('shop_items', function ($schema) { $schema->id(); $schema->string('username'); }); })->execute(null, function ($error) { echo "Error: {$error['error']}\n"; }); $db->q(function ($qb) { $qb->createTable('shop_posts', function ($schema) { $schema->id(); $schema->string('title'); $schema->integer('user_id'); $schema->foreign('user_id')->references('id')->on('shop_items')->onDelete('CASCADE'); }); })->execute(null, function ($error) { echo "Error: {$error['error']}\n"; }); }, function ($result, $db, $debug) { echo "Tables created.\n"; }, function ($error, $db, $debug) { echo "Error: {$error['error']}\n"; }); Output: Tables created. Altering a table (adding a column): DB::module('RAW')->schema(function ($schema) { $schema->alterTable('shop_items', function ($table) { $table->string('email', 100); }); }, function ($result, $db, $debug) { echo "Email column added.\n"; }, function ($error, $db, $debug) { echo "Error: {$error['error']}\n"; }); Output: Email column added. Drop tables from Installation::uninstaller() with DB::module('RAW')->q(...)->execute($ok, $err). 10.7. Notes Transactions: Use transact() on the module instance for batch schema changes so the database stays consistent. You can also call transaction(), commit(), and rollback() on that instance. Driver support: Syntax is adapted to the driver (for example MySQL vs. SQLite), but some features (for example ON UPDATE on Oracle) may not be fully supported. Installing schema: Create module tables in Installation.php with self::alreadyDone / self::markDone. Never call DB::migrate(). Error callbacks: Always pass an error callback to schema(), execute(), and transact(). 11. Case study: e-shop with ORM This chapter is a practical case study that shows how to use Databaser and its ORM to build a simple e-shop. We design the database structure, create tables, seed them with data, and work with that data through Entity and Collection. The examples include error callbacks so you can apply robust error handling. 11.1. Designing the database structure For the e-shop we will use these tables: shop_customers: Customers and administrators. shop_products: Products in the catalog. shop_product_descriptions: Product descriptions (one product can have several, for example in different languages). shop_orders: Orders. shop_order_items: Order lines (products linked to orders). SQL to create the tables You can copy these statements and run them in a MySQL database: -- Customers CREATE TABLE shop_customers ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, name VARCHAR(50) NOT NULL, email VARCHAR(100) NOT NULL UNIQUE, role ENUM('customer', 'admin') DEFAULT 'customer', created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); -- Products CREATE TABLE shop_products ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, name VARCHAR(100) NOT NULL, price DECIMAL(10, 2) NOT NULL, stock INT NOT NULL DEFAULT 0, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); -- Product descriptions CREATE TABLE shop_product_descriptions ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, product_id BIGINT UNSIGNED NOT NULL, language VARCHAR(10) NOT NULL, description TEXT NOT NULL, FOREIGN KEY (product_id) REFERENCES shop_products(id) ON DELETE CASCADE ); -- Orders CREATE TABLE shop_orders ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, customer_id BIGINT UNSIGNED NOT NULL, total_price DECIMAL(10, 2) NOT NULL, status ENUM('pending', 'shipped', 'delivered') DEFAULT 'pending', created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (customer_id) REFERENCES shop_customers(id) ON DELETE CASCADE ); -- Order items CREATE TABLE shop_order_items ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, order_id BIGINT UNSIGNED NOT NULL, product_id BIGINT UNSIGNED NOT NULL, quantity INT NOT NULL DEFAULT 1, price DECIMAL(10, 2) NOT NULL, FOREIGN KEY (order_id) REFERENCES shop_orders(id) ON DELETE CASCADE, FOREIGN KEY (product_id) REFERENCES shop_products(id) ON DELETE CASCADE ); SQL to seed data These statements fill the tables with sample data: -- Customers INSERT INTO shop_customers (name, email, role) VALUES ('Jane Novak', 'jane@example.com', 'customer'), ('Admin Peter', 'admin@example.com', 'admin'); -- Products INSERT INTO shop_products (name, price, stock) VALUES ('White t-shirt', 15.99, 50), ('Black shoes', 49.99, 20), ('Winter jacket', 89.99, 10); -- Product descriptions INSERT INTO shop_product_descriptions (product_id, language, description) VALUES (1, 'en', 'Comfortable white cotton t-shirt.'), (1, 'sk', 'Comfortable white cotton t-shirt.'), (2, 'en', 'Elegant black shoes for any occasion.'), (3, 'en', 'Warm winter jacket with a hood.'); -- Orders INSERT INTO shop_orders (customer_id, total_price, status) VALUES (1, 65.98, 'pending'), (1, 89.99, 'shipped'); -- Order items INSERT INTO shop_order_items (order_id, product_id, quantity, price) VALUES (1, 1, 2, 15.99), (1, 2, 1, 49.99), (2, 3, 1, 89.99); 11.2. Implementation in Databaser with ORM ORM examples use DB::module('ORM'), safe reads through all(), and explicit relations through hasMany(). Do not use with() as if it loaded related rows in SQL — it does not emit SQL. 11.2.1. Fetching a customer and their orders $rows = DB::module('ORM')->q(function ($qb) { $qb->select('*', 'shop_customers')->where('id', '=', 1)->limit(1); })->all(); $customer = $rows[0] ?? null; $orders = $customer ? $customer->hasMany('shop_orders', 'customer_id') : []; foreach ($orders as $order) { echo "Order #{$order->id}: {$order->status}\n"; } 11.2.2. Adding a new product with a description DB::module('RAW')->transact(function ($db) { $product = $db->newEntity(); $product->table('shop_products'); $product->name = 'Green scarf'; $product->price = 19.99; $product->stock = 30; $product->save( function ($result, $db, $debug) { $description = $db->newEntity(); $description->table('shop_product_descriptions'); $description->product_id = $db->inserted_id(); $description->language = 'en'; $description->description = 'Warm green scarf for winter.'; $description->save(null, function ($error, $db, $debug) { echo "Error: {$error['error']}\n"; }); }, function ($error, $db, $debug) { echo "Error: {$error['error']}\n"; } ); }, function ($result, $db, $debug) { echo "Product added.\n"; }, function ($error, $db, $debug) { echo "Transaction error: {$error['error']}\n"; }); 11.2.3. Displaying an order with its items $rows = DB::module('ORM')->q(function ($qb) { $qb->select('*', 'shop_orders')->where('id', '=', 1)->limit(1); })->all(); $order = $rows[0] ?? null; $items = $order ? $order->hasMany('shop_order_items', 'order_id') : []; foreach ($items as $item) { $productRows = DB::module('ORM')->q(function ($qb) use ($item) { $qb->select('name', 'shop_products')->where('id', '=', $item->product_id)->limit(1); })->all(); $product = $productRows[0] ?? null; if ($product) { echo "Item: {$product->name}, quantity: {$item->quantity}\n"; } } 11.3. Using validation Add validation for a product before saving. $product = DB::newEntity(); $product->table('shop_products'); $product->setRules([ 'name' => ['required', 'string', 'max:100'], 'price' => ['required', 'numeric', 'min:0'], 'stock' => ['integer', 'min:0'] ]); $product->name = 'This English product name is deliberately written to be longer than one hundred characters so that Databaser validation rejects it'; $product->price = -5; $product->stock = 10; $product->save( function ($result, $db, $debug) { echo "Product saved successfully.\n"; }, function ($error, $db, $debug) { echo "Validation failed: {$error['error']}\n"; } ); 11.4. Notes on the case study Transactions: Using transact() on the module instance keeps related writes consistent; error callbacks report failures. Relations: Load related rows with explicit Entity methods such as hasMany(). with(), whereHas(), and withCount() are stubs and do not emit SQL — they are not a way to load relations. Validation: Rules protect against invalid data and produce a clear error message. Error handling: error callbacks let you react to problems (for example logging or user-facing notices). Always pass an error callback to Entity::save(). 12. Tips and tricks This chapter offers practical advice on using Databaser effectively in the DotApp Framework. It covers query optimization, security, and extension points. 12.1. Query optimization Efficient queries are key to a fast application. A few tips: Select only the columns you need: Instead of select('*', 'shop_items'), use specific columns, for example select('id, name', 'shop_items'). That reduces the amount of data transferred. DB::module('RAW')->q(function ($qb) { $qb->select('id, name', 'shop_items')->where('age', '>', 18); })->execute( function ($result, $db, $debug) { var_dump($result); }, function ($error, $db, $debug) { echo "Error: {$error['error']}\n"; } ); Use indexes: For frequent filters in where() (for example id, age), add indexes with schema(): DB::module('RAW')->schema(function ($schema) { $schema->alterTable('shop_items', function ($table) { $table->index('age'); }); }, function ($result, $db, $debug) { echo "Index created.\n"; }, function ($error, $db, $debug) { echo "Error: {$error['error']}\n"; }); Paginate lists that can grow: Prefer paginate($perPage, $page) over raw limit() / offset() for accumulating lists (items, orders, logs). Use limit() and offset() only when you need a one-off slice. $page = DB::module('RAW')->q(function ($qb) { $qb->select('id, name', 'shop_items')->orderBy('id', 'DESC'); })->paginate(20, 1); foreach ($page['data'] as $row) { echo "{$row['name']}\n"; } Cache repeated queries: If you have a cache driver implemented, use it to store results: DB::module('RAW')->cache($myCacheDriver)->q(function ($qb) { $qb->select('*', 'shop_items'); })->execute( function ($result, $db, $debug) { echo "Results from cache or DB: "; var_dump($result); }, function ($error, $db, $debug) { echo "Error: {$error['error']}\n"; } ); 12.2. Security (SQL injection prevention) Databaser is designed with security in mind, but it is still worth knowing the proven practices: Always use prepared statements: QueryBuilder escapes values automatically, so never interpolate variables into the query string. Correct: DB::module('RAW')->q(function ($qb) { $qb->select('*', 'shop_items')->where('name', '=', 'Jane'); })->execute(null, function ($error) { echo "Error: {$error['error']}\n"; }); Incorrect: $name = "Jane'; DROP TABLE shop_items; --"; DB::module('RAW')->q(function ($qb) use ($name) { $qb->raw("SELECT * FROM shop_items WHERE name = '$name'"); })->execute(null, function ($error) { echo "Error: {$error['error']}\n"; }); // Dangerous! Raw queries with RAW: If you use raw(), always pass values through bindings: DB::module('RAW')->q(function ($qb) { $qb->raw('SELECT * FROM shop_items WHERE age > ?', [18]); })->execute(null, function ($error) { echo "Error: {$error['error']}\n"; }); Validation rules in ORM: When saving data through Entity, set rules: $rows = DB::module('ORM')->q(function ($qb) { $qb->select('*', 'shop_items')->where('id', '=', 1)->limit(1); })->all(); $user = $rows[0] ?? null; if ($user) { $user->setRules(['name' => 'required|string|max:50']); $user->name = 'Jane'; $user->save( null, function ($error, $db, $debug) { echo "Error: {$error['error']}\n"; } ); } 12.3. Extending Databaser with custom drivers Register a custom database driver with Databaser::customDriver($name, $class). The class must expose public static function create(Databaser $db) and, inside create(), register the driver closures on that instance. DB::addDriver() is not the module API for registering a driver. Driver classes may use it internally from create(); application and module code should call Databaser::customDriver($name, $class). Closures to register: select_db, q, return, execute, first, all, raw, fetchArray, fetchFirst, newEntity, newCollection, inserted_id, affected_rows, schema, transaction, transact, commit, rollback. Databaser::customDriver('custom', CustomDriver::class); // CustomDriver::create(Databaser $db) registers the closures listed above. 12.4. ORM relations Load related rows with explicit Entity methods such as hasMany(). with() in DotApp 2.0 does not generate SQL and is not a way to load relations: $items = DB::module('ORM')->q(function ($qb) { $qb->select('*', 'shop_items'); })->all(); foreach ($items as $item) { foreach ($item->hasMany('shop_posts', 'user_id') as $post) { echo "Item: {$item->name}, Post: {$post->title}\n"; } } 12.5. Batch operations with Collection Do not use saveAll(). After map(), save each entity individually with save() and always pass an error callback: $items = DB::module('ORM')->q(function ($qb) { $qb->select('*', 'shop_items'); })->all(); $items->map(function ($item) { $item->age += 1; $item->save( function ($result, $db, $debug) { echo "Item saved.\n"; }, function ($error, $db, $debug) { echo "Error: {$error['error']}\n"; } ); return $item; }); Methods: filter(), map(), pluck(). --- # Templates URL: https://dotapp.dev/documentation/templates Template System 1. Introduction 1.1. What is the template system? 1.2. Files and folders 1.3. The Renderer facade 1.4. Missing files 2. Rendering a page 2.1. First render 2.2. View vars vs layout vars 2.3. Shell view and content 2.4. Cross-module files 2.5. Other render methods 3. Directives 3.1. Printing values 3.2. Translation 3.3. Conditionals 3.4. Loops 3.5. Includes 3.6. Forms and encryption 3.7. Blocks 3.8. Unsupported syntax 4. Assets 5. Custom renderers 6. Pipeline and debugging 7. Translator API 8. Client-side templates 9. Checklist Template system DotApp renders HTML with the Renderer facade and a small set of {{ … }} directives. Templates are PHP files owned by a module. Controllers pass data with setViewVar(). There is no separate template language runtime: directives compile to PHP, then a sandbox evaluates the result. Live pages on this site follow the same rules. Hello World: /helloworld. Secure forms: /documentation/examples/run/forms2. The step-by-step walkthrough is Step-by-step guide. 1.1 What is the template system? Each module keeps presentation in app/modules/{Module}/views/. A view is a full page or a shell. A layout is a reusable fragment (header, list row, heading). The controller chooses the files, assigns variables, and returns the HTML string from renderView() or renderLayout(). Print a value with {{ var: $title }}. That is the only supported print syntax. {{ $title }} is not a directive and will not output the variable. 1.2 Files and folders Kind Path Selected with View app/modules/{Module}/views/{name}.view.php setView('name') Layout app/modules/{Module}/views/layouts/{path}.layout.php setLayout('path') or {{ layout:path }} Another module Same structure under that module setView('Shop:home'), {{ layout: Shop:partials/header }} Base layout app/parts/views/layouts/ {{ baselayout:name }} only Assets app/modules/{Module}/assets/... /assets/modules/{Module}/... {{ layout:partials/header }} loads views/layouts/partials/header.layout.php. The layouts directory is already the root — do not write layout:layouts/header. Nested includes stop at depth 20. 1.3 The Renderer facade Create a renderer with Renderer::new(), point it at a module, then set the view. Named instances (Renderer::new('docs')) are reused as singletons. For a page, start a fresh chain with Renderer::new(). use Dotsystems\App\Parts\Logger; use Dotsystems\App\Parts\Renderer; use Dotsystems\App\Parts\Response; $html = Renderer::new() ->module('HelloWorld') ->setView('hello') ->setViewVar('title', 'Hello World') ->setViewVar('message', 'DotApp 2.0 is running.') ->renderView(); if ($html === '') { Logger::use()->error('HelloWorld view produced empty output'); return new Response(500, 'Template error'); } return $html; There is no setViewVars() plural and no public getView() / getLayout(). Read a single value with getViewVar('title') (missing key returns "") or the whole bag with getViewVars(). 1.4 Missing files fail silently A missing view or layout does not throw. The renderer logs a warning and returns an empty string. A blank page usually means a wrong file name, the wrong module, or setView() never ran. Always pass a fallback name and test the return value: $html = Renderer::new() ->module('Shop') ->setView('home', 'fallback/empty') ->setLayout('catalog/list', 'catalog/empty') ->setViewVar('title', $title) ->renderView(); The second argument of setView() and setLayout() is a fallback file, not a wrapper layout. loadViewStatic() does not check that the file exists — prefer setView() / loadView(). 2.1 First render The live Hello World module is the minimal pattern: one view, two variables, no nested layout. app/modules/HelloWorld/views/hello.view.php: {{ var: $title }}

{{ var: $title }}

{{ var: $message }}

Call setView('hello') before any setViewVar(). Switching the view later drops the previous variable bag. 2.2 View variables versus layout variables renderView() evaluates the compiled template with the view variable bag. Values set only with setLayoutVar() do not appear in that output. When you render a page with renderView(), pass every value the view and its included layouts need through setViewVar(). Use setLayoutVar() with renderLayout() when you render a layout file on its own (this documentation site does that for article chunks). 2.3 Shell view plus content layout Give the page a shell view that contains {{ content }}, and put the inner HTML in a layout selected with setLayout(). Includes from inside the view still use {{ layout:… }}. return Renderer::new() ->module('Shop') ->setView('home') ->setLayout('content/welcome') ->setViewVar('title', 'Shop') ->setViewVar('items', $items) ->renderView(); View views/home.view.php: {{ var: $title }} {{ layout:partials/header }}
{{ content }}
Layout views/layouts/content/welcome.layout.php:

{{_ "Welcome" }}

{{ foreach $items as $item }}

{{ var: $item['title'] }}

{{ /foreach }} A view can also be a complete HTML document with no setLayout() and no {{ content }}. Hello World, the Examples demos, and the Users demo all do that. 2.4 Cross-module files Prefix the path with the other module’s name and a colon: Renderer::new()->module('Checkout')->setView('Shop:home')->renderView(); {{ layout: Shop:partials/header }} The file still lives under that module’s views/ (or views/layouts/ for layouts). 2.5 Other render methods Method Use renderView() Normal page. Evaluates with view variables. renderLayout() One layout file. Evaluates with layout variables. renderCode($code, $vars) Compile and evaluate an HTML string you already have. loadView($name) Read the view file as text. Missing file: "". 3.1 Printing values {{ var: $title }} {{ var: $user['name'] }} {{ var:$title }} The compiler turns this into echo. There is no automatic escaping in the directive. Request data from $request->data() is already protected against XSS. If you pass raw HTML from PHP, escape it in the controller with htmlspecialchars() before setViewVar(), or leave it protected and call DotApp::DotApp()->unprotect($html) only when you intentionally render markup. {{ var: }} does not accept expressions, ??, ->, or function calls. Prepare the value in the controller. 3.2 Translation {{_ "Login" }} {{_ var: $message }} Use double quotes around the source string. A missing key prints the original text. Load JSON files from the module and set the locale in PHP: use Dotsystems\App\Parts\Translator; Translator::loadLocaleFile('Shop:sk_sk.json', 'sk_sk'); Translator::setLocale('sk_sk'); echo Translator::trans('Hello, {{ arg0 }}', $name); Files live in app/modules/{Module}/translations/{locale}.json. Placeholders are {{ arg0 }}, {{ arg1 }}. There is no pluralization and no locale fallback chain. 3.3 Conditionals {{ if isset($user) }}

Signed in

{{ elseif $guest === true }}

Guest

{{ else }}

Unknown

{{ /if }} Put a space after {{ before if, elseif, else, and /if. The closing tag is {{ /if }}, not {{ endif }}. 3.4 Loops {{ foreach $items as $item }}
  • {{ var: $item['title'] }}
  • {{ /foreach }} {{ while $i < 5 }}

    {{ var: $i }}

    {{ /while }} Closing tags are {{ /foreach }} and {{ /while }}. Increment counters in the controller or with a small PHP block in the template. Keep business logic in the controller. 3.5 Includes and the content slot {{ layout:partials/header }} {{ layout: Shop:partials/header }} {{ baselayout: something }} {{ content }} Layout tags are includes. They have no closing tag. {{ content }} is filled only when you called setLayout() and then renderView(). 3.6 Forms and encryption {{ formName(saveItem) }} {{ formName(saveItem) }} must sit between (or
    ) and the matching close tag. The tag needs a method attribute. Outside that pair the renderer leaves the token unchanged. Prefer . dotapp.js converts it to a real form and posts with CRC. PHP still runs $request->crcCheck() then $request->form(…). When the form posts to the current page, omit action and pass $request->getPath() as the last argument of form(). {{ CSRF }} emits a plain token. Use formName for application forms. Encrypt values in the template with a dedicated extra key per field: {{ enc: $secret }} {{ enc(mykey): "literal" }} {{ enc: "literal" }} encrypts while the template compiles. {{ enc(key): $var }} encrypts when the page runs. Decrypt with the same extra key: Crypto::decrypt($cipher, 'Shop.user.id'). Failure is === false. Full form walkthrough: Secure forms. 3.7 Blocks Register a named block in initialize($dotApp), then wrap markup in the view: Renderer::new()->addBlock('alert', function ($inner, array $args) { $kind = $args[0] ?? 'info'; return '
    ' . $inner . '
    '; }); {{ block:alert(danger) }}Warning{{ /block:alert }} privateblock stores a fragment as a PHP object you can clone inside the same file: {{ privateblock:row }}
  • {{ var: $name }}
  • {{ /privateblock }} set('name', $it['name'])->html(); ?> Native PHP in a template is allowed, but the sandbox strips dangerous functions (eval, exec, system, file_*, curl_*, mail, header, extract, call_user_func*, …). If a call does nothing, that is why. Put I/O and queries in the controller. 3.8 Syntax that is not supported Do not write Write {{ $title }} {{ var: $title }} {{ endif }} / {{ endforeach }} {{ /if }} / {{ /foreach }} {{ include 'x' }} in a PHP view {{ layout:x }} extends / section / yield renderView() + {{ content }} or {{ layout: }} {{ $x ?? 'd' }} Prepare the value in the controller {{ include path }} exists only in the optional JavaScript template engine (section 8), never in PHP views. Input-group tags such as {{ InputKeys('register_form') }} and {{ input:text … }} come from Input.php, not from the core directive table. Prefer formName for ordinary HTML forms. Bridge attributes ({{ dotbridge:on(click)="…" }}) are documented on DotBridge. 4. Assets Store CSS, JS, and images under app/modules/{Module}/assets/. The framework serves them as: /assets/modules/{Module}/{path} Pages that submit , call $dotapp().load(), or use Bridge must load /assets/dotapp/dotapp.js first. That URL is a framework route. It injects per-session keys. Do not link a raw file from app/parts/js/ on a public page. Optional CSS helpers: prepareCss() concatenates and minifies into a cache file and prints a tag. removeUnusedCss(true) drops selectors that do not appear as class="…" in the HTML — it also removes classes added later by JavaScript. Leave it off unless you verified the output. There is no built-in cache-busting helper; append ?v= yourself if you need it. Do not enable HTML page cache with useCache(true). 5. Custom renderers A custom renderer is a callable that receives the compiled HTML (and, when present, the variable bag) and returns HTML. Register it once in initialize($dotApp). It runs for every render after that. use Dotsystems\App\Parts\Renderer; Renderer::new()->addRenderer('shop.money', function (string $code, array $vars = []): string { $amount = number_format((float) ($vars['price'] ?? 0), 2); return str_replace('{{ money }}', $amount, $code); }); Renderer::add($name, $callable) is the same registration on the facade. getRenderer($name) returns the callable or false. renderWith($name, $code) runs one renderer on a string. Built-in renderers registered by the framework include dotapp.block, reactive, and input_form_*. This documentation module registers Docs.code.replace so samples inside
     are escaped. 6. Pipeline, sandbox, debugging A typical renderView() run: Resolve nested {{ layout: }} / {{ baselayout: }} (depth ≤ 20). Extract privateblock and run custom renderers. Insert setLayout() HTML into {{ content }}. Compile var / if / foreach / while / enc / translation. Replace {{ CSRF }} and {{ formName() }}. Process Bridge tags. Evaluate in RenderingIsolator. A compile or eval failure prints ERROR WHILE EVAL: … into the response. For real line numbers: define('__RENDER_TO_FILE__', true); Compiled PHP is written under app/runtime/generator/rendering_*.php, included, then deleted. 7. Translator API Method Result trans($text, ...$args) / t() Translated string, or the original text if the key is missing setLocale($locale) / getLocale() Current locale (default en_us) loadLocaleFile('Module:file.json', $locale) Missing file is skipped with no exception has($key, $locale = null) bool — use this to detect a missing key all($locale = null) All keys for that locale Product copy that a person can see (buttons, empty states, permission names) must read like shipped UI, not like a reply to a prompt. Keys are the source English (or source) string, lowercased on lookup. 8. Client-side templates Optional script /assets/dotapp/dotapp.template.js adds $dotapp('#box').template('path/to/view', { items: […] }) in the browser. It understands {{ var: }}, {{ if }}, {{ foreach }}, {{ block: }}, and {{ include partials/header }}. The default base path is /app/views/. Load it after dotapp.js. Wait for the dotapp-template-ready event if the add-on is still loading. PHP views never gain include. Server HTML stays on Renderer + {{ layout: }}. Use the JS engine when you patch a list from $dotapp().load() without a full page render. Core reactivity (variable, databind, computed) is in the reactivity example. Custom $dotapp().fn widgets are in the JS library example. The live list demo is /documentation/examples/run/lists. 9. Checklist View file: {name}.view.php. Layout file: views/layouts/{path}.layout.php. Renderer::new()->module('Name')->setView('name') before setViewVar(). Treat renderView() === '' as an error. Print with {{ var: $x }}. Close branches with {{ /if }} / {{ /foreach }}. Pass every value through setViewVar() when you call renderView(). Put {{ formName(handler) }} inside  and load /assets/dotapp/dotapp.js. Keep queries, auth, and writes in the controller. The template sandbox will strip unsafe PHP.
    
    
    ---
    
    # Configuration
    
    URL: https://dotapp.dev/documentation/configuration
    
    Configuration Config Class Overview Basic Settings Session Configuration Session Drivers Application Configuration Database Configuration Module Configuration Two-Factor Authentication (2FA) Configuration Examples
    Config Class Overview The Config class, located at /app/Config.php, provides the core configuration settings required to run a DotApp application. It includes default settings that ensure the framework operates smoothly, but users typically need to customize only a few key parameters, such as the database, application name, and encryption key. Important: The encryption key (c_enc_key) is used for securing passwords and sensitive data. Once set, it should not be changed, as doing so would require resetting all encrypted data, such as user passwords. Basic Settings To run a DotApp application, you must define the root directory in index.php if the application is hosted in a subdirectory. Otherwise, the current directory is used by default. define('__ROOTDIR__', "path/to/your/application"); // Set this only if the application runs in a subdirectory Custom configuration settings are defined in /app/config.php. This file allows you to override default settings and tailor the framework to your needs. Session Configuration DotApp provides a robust session management system with customizable settings. Below are the default session configuration options defined in /app/config.php: 'session' => [ 'driver' => 'default', // Default session driver 'lifetime' => 3600, // Session expiration in seconds 'rm_always_use' => false, // Always use "Remember Me" functionality? 'rm_autologin' => false, // Enable automatic autologin? 'rm_lifetime' => 2592000, // Remember Me lifetime (30 days) 'cookie_name' => 'dotapp_session', // Session cookie name 'path' => '/', // Cookie path 'secure' => false, // Restrict to HTTPS 'httponly' => true, // Prevent XSS attacks 'samesite' => 'Strict', // Prevent CSRF attacks 'database_use' => false, // Use database for session storage? 'database_table' => 'users_sessions', // Table for database sessions 'redis_host' => '127.0.0.1', // Redis host 'redis_port' => 6379, // Redis port 'redis_timeout' => 2, // Redis connection timeout 'redis_password' => '', // Redis password 'redis_persistent' => false, // Persistent Redis connection 'redis_database' => 0, // Redis database number 'redis_prefix' => 'session:', // Redis session prefix 'file_driver_dir' => '/app/runtime/SessionDriverFile', // Directory for SessionDriverFile 'file_driver_dir2' => '/app/runtime/SessionDriverFile2', // Directory for SessionDriverFile2 ] To customize session settings, use the Config::session method. For example: Config::session("lifetime", 30 * 24 * 3600); // Set session lifetime to 30 days Config::session("rm_autologin", true); // Enable automatic autologin Session Drivers DotApp supports multiple session drivers, allowing you to choose the storage mechanism that best suits your application. Importantly, session settings must be configured before defining the driver, as the driver uses these settings upon initialization. Available Drivers Default Driver (SessionDriverDefault): Uses PHP’s built-in $_SESSION mechanism. File Driver (SessionDriverFile): Stores sessions in files, using the directory specified in file_driver_dir. Requires a CRON job for garbage collection. File Driver 2 (SessionDriverFile2): Similar to SessionDriverFile, but stores each cookie in separate files, ideal for handling many cookies. Uses file_driver_dir2. Requires a CRON job for garbage collection. Database Driver (SessionDriverDB): Stores sessions in a database, ideal for load-balanced environments. Uses the users_sessions table (default prefix: dotapp_). Redis Driver (SessionDriverRedis): Uses Redis for fast, centralized session storage, suitable for load-balanced setups. To set a driver, use the Config::sessionDriver method after configuring session settings: Config::sessionDriver("default", SessionDriverDefault::driver()); // Default $_SESSION driver Config::sessionDriver("default", SessionDriverFile::driver()); // File-based driver Config::sessionDriver("default", SessionDriverFile2::driver()); // File-based driver with multiple files Config::sessionDriver("default", SessionDriverDB::driver()); // Database driver Config::sessionDriver("default", SessionDriverRedis::driver()); // Redis driver Database Driver Setup For the database driver, you must create the users_sessions table. Below is the SQL to create it: CREATE TABLE IF NOT EXISTS `dotapp_users_sessions` ( `session_id` varchar(64) NOT NULL, `sessname` varchar(255) NOT NULL, `values` longtext NOT NULL, `variables` longtext NOT NULL, `expiry` bigint NOT NULL, `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, `updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`session_id`,`sessname`), KEY `idx_expiry` (`expiry`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; To change the table prefix: Config::db("prefix", "your_prefix_"); Redis Driver Configuration To configure the Redis driver, update the relevant settings using Config::session. For example: Config::session("redis_host", "your IP"); // Redis host Config::session("redis_port", 6379); // Redis port Config::session("redis_timeout", 2); // Redis connection timeout Config::session("redis_password", "your_password"); // Redis password Config::session("redis_persistent", false); // Persistent Redis connection Config::session("redis_database", 0); // Redis database number Config::session("redis_prefix", "session:"); // Redis session prefix Application Configuration DotApp allows you to configure core application settings, such as the application name, encryption key, and version. Below are the default settings: 'app' => [ 'name' => 'dotApp123456', 'name_hash' => '', // Do not modify directly 'c_enc_key' => 'K9xP7mW3qT2rY6vL8cF4hD5aE0zJ1nB2X7bP9qRtY2mW4kZjN6vL8cF3hD5aE0xQ', // Encryption key 'version' => '1.0', ] To customize these settings, use the Config::app method: Config::app("name", "Your Application Name"); // Set application name Config::app("c_enc_key", "YourStrongEncryptionKey"); // Set a strong encryption key Config::app("version", "0.1 alpha"); // Set application version Important: The encryption key (c_enc_key) must be strong and secure, as it is used for encrypting sensitive data. Avoid changing it after initial setup to prevent issues with existing encrypted data. Database Configuration DotApp supports multiple database drivers (PDO and MySQLi) and allows you to configure multiple databases. To add a database, use the Config::addDatabase method: Config::addDatabase("main", "127.0.0.1", "dotsystems", "dotsystems", "dotsystems", "UTF8", "MYSQL", "pdo"); This configures a database named main using the PDO driver with MySQL. Supported PDO database types include mysql, pgsql, sqlite, oci, and sqlsrv. Default database settings: 'db' => [ 'prefix' => 'dotapp_', // Database table prefix 'driver' => 'pdo', // Default driver 'maindb' => 'main' // Default database name ] The main database is used by default across the application unless another database is specified. To switch the default database for all modules: Config::db("maindb", "anotherdb"); Config::addDatabase("anotherdb", "127.0.0.1", "dotsystems2", "dotsystems2", "dotsystems2", "UTF8", "MYSQL", "pdo"); To change the default driver: Config::db("driver", "mysqli"); Important: Each driver maintains its own database credentials. A database configured for PDO cannot be used with MySQLi unless reconfigured. For example: // Incorrect: Mixing PDO database with MySQLi driver Config::db("driver", "mysqli"); Config::db("maindb", "anotherdb"); Config::addDatabase("anotherdb", "127.0.0.1", "dotsystems2", "dotsystems2", "dotsystems2", "UTF8", "MYSQL", "pdo"); // Correct: Matching driver and database Config::db("driver", "mysqli"); Config::db("maindb", "anotherdb"); Config::addDatabase("anotherdb", "127.0.0.1", "dotsystems2", "dotsystems2", "dotsystems2", "UTF8", "MYSQL", "mysqli"); Queries use the configured default database: DB::module('RAW')->q(function ($qb) { $qb->select('*', 'shop_items')->limit(20); })->all(); Module Configuration DotApp allows module-specific configuration using the Config::module method, which provides getter and setter functionality for module settings. Example of setting and getting module configuration: Config::module("crm", "title", "CRM PAGE TITLE"); // Set module configuration $title = Config::module("crm", "title"); // Get module configuration This allows module developers to provide customizable settings for users while keeping configurations organized and module-specific. Two-Factor Authentication (2FA) DotApp supports two-factor authentication (2FA) with TOTP (Time-based One-Time Password). The default settings are: 'totp' => [ 'issuer' => 'DotApp', 'algorithm' => 'SHA256', 'digits' => 6, 'period' => 30, ] To customize the 2FA issuer (e.g., for display in authenticator apps): Config::totp("issuer", "MyApp"); Other settings (algorithm, digits, period) should typically remain unchanged. 2FA configuration is only necessary if your application uses two-factor authentication for login. Configuration Examples To see practical examples of configuring DotApp, including session drivers, database setup, and module configuration, visit the Examples section. These examples demonstrate how to apply the configurations discussed here in real-world scenarios.
    
    
    ---
    
    # Recommended practices
    
    URL: https://dotapp.dev/documentation/recommended-practices
    
    Recommended Practices Philosophy Overview Accessing DotApp Instance Using Facades Dependency Injection Database Practices Session Management with DSM See Examples
    Recommended Practices This section outlines the recommended practices for developing modules and applications with the DotApp PHP Framework. By adhering to these practices, you ensure that your modules are portable, shareable across applications, and adaptable to any server configuration, including different session drivers (e.g., Redis, file-based, database) and database drivers (PDO, MySQLi). Following the framework's philosophy guarantees consistent outputs regardless of the underlying drivers. Philosophy Overview The DotApp framework is designed to create portable and maintainable modules that work seamlessly across different environments. By following these practices, your modules will adapt to the user's session driver (e.g., Redis, database) and database driver (PDO, MySQLi) without requiring code changes. This ensures that your application remains flexible and shareable, aligning with DotApp’s core philosophy of modularity and adaptability. A key aspect of DotApp’s philosophy is input security. By default, all inputs are automatically protected against common vulnerabilities such as Cross-Site Scripting (XSS). This design keeps the application secure even when a single input is forgotten. To access the original, unprotected value — for example when storing HTML — use DotApp::DotApp()->unprotect($variable). The method accepts a string or an array by reference and recursively removes protection. For example: use \Dotsystems\App\DotApp; $variable = $_POST['variable']; DotApp::DotApp()->unprotect($variable); // $variable now contains the original, unprotected value Note that unprotect modifies the variable by reference, so you should call it as DotApp::DotApp()->unprotect($variable) without reassigning the result (i.e., avoid $variable = DotApp::DotApp()->unprotect($variable)). This approach reinforces DotApp’s philosophy: developers don’t need to protect variables manually, as they are secure by default, but they have the flexibility to retrieve unprotected values when explicitly required. Accessing DotApp Instance The DotApp kernel is available as DotApp::DotApp(). Use it for unprotect, ajaxReply, and call. Routing, queries, views, config, and sessions use facades. use Dotsystems\App\DotApp; DotApp::DotApp()->unprotect($htmlFromEditor); DotApp::DotApp()->ajaxReply(['status' => 1], 200); DotApp::call('HelloWorld:Home@index!', $request); In initialize($dotApp) the kernel is the method argument. Register services with $dotApp->bind / singleton / resolve. Controllers use DotApp::DotApp() or facades. Using Facades Facades are the public API for core services. For example: Renderer::new()->module(self::moduleName())->setView("dotapper-cli.eng")->setViewVar("variables", $viewVars)->renderView(); The Renderer facade keeps the code concise. Custom renderers: Renderer::add("Docs.code.replace", function($code) { /* logic */ }); Common Facades Renderer::new(): Returns a resettable renderer object. Renderer::add(): Adds a custom renderer. Router::get(): Defines a GET route, e.g., Router::get(['/helloworld', '/helloworld/'], "HelloWorld:Home@index!", Router::STATIC_ROUTE);. Using facades improves code readability and aligns with DotApp’s philosophy of clean, maintainable code. Dependency Injection Register your own services in initialize($dotApp): public function initialize($dotApp) { $dotApp->singleton('cache', function () { return new CacheService(); }); } Controllers render with Renderer::new(): public static function index($request) { return Renderer::new()->module('HelloWorld')->setView('hello')->renderView(); } Database Practices To ensure your modules are portable and driver-agnostic, DotApp’s philosophy requires using the DB::module() facade for database access. This facade uses configuration settings to automatically select the configured driver and database, ensuring consistency across the application. Using DB::module() Use DB::module("ORM") or DB::module("RAW") for database queries: DB::module("RAW")->q(function ($qb) use ($token) { $qb ->select('user_id', Config::get("db","prefix").'users_rmtokens') ->where('token', '=', $token); })->execute( function ($result) { // $result is an array of rows in RAW mode }, function ($error) { \Dotsystems\App\Parts\Logger::use()->error('query failed', ['msg' => is_object($error) ? $error->getMessage() : (string) $error]); } ); Using Callbacks Always pass success and error callbacks to execute(). The success callback receives an array of rows in RAW mode. Success callback: function($result, $db, $debug) — $result is an array of rows in RAW mode. Error callback: function($error, $db, $debug) — required so failures are handled. DB::module("RAW")->q(function ($qb) use ($token) { $qb ->select('user_id', Config::get("db","prefix").'users_rmtokens') ->where('token', '=', $token); })->execute( function ($result, $db, $debug) use (&$data) { if ($result === null || $result === []) { $data = []; setcookie('dotapp_'.Config::get("app","name_hash"), "", [ 'expires' => time() - 3600, 'path' => Config::session("path"), ]); } else { $db->q(function ($qb) use (&$data, $result) { $qb ->select(['username', 'password'], Config::get("db","prefix").'users') ->where('id', '=', $result['user_id']); })->execute(function ($result, $db, $debug) use (&$data) { $data['username'] = $result[0]['username']; $data['passwordHash'] = $result[0]['password']; $data['stage'] = 0; \Dotsystems\App\Parts\Auth::login($data, true); }, function ($error, $db, $debug) { // Handle error, e.g., log or display error message $data['error'] = $error->getMessage(); }); } }, function ($error, $db, $debug) { // Handle initial query error error_log("Database error: " . $error->getMessage()); } ); In this example: The success callback processes the $result array, which is driver-agnostic (e.g., $result[0]['user_id']). The nested query uses another execute with its own success and error callbacks to handle results or errors. The error callback logs or handles any database errors, preventing uncaught exceptions. If callbacks lead to complex code (callback hell), you can store results in a variable to simplify logic: $dbreturn = null; DB::module("RAW")->q(function ($qb) use ($token) { $qb ->select('user_id', Config::get("db","prefix").'users_rmtokens') ->where('token', '=', $token); })->execute( function ($result, $db, $debug) use (&$dbreturn) { $dbreturn = $result; }, function ($error, $db, $debug) { error_log("Database error: " . $error->getMessage()); } ); // Continue logic with $dbreturn Important: Avoid returning raw driver objects (e.g., $returnDB = DB::module("RAW")->q(...)->execute()), as they are driver-specific (MySQLi or PDO). Using callbacks ensures your module works with any driver, aligning with DotApp’s philosophy. Session Management with DSM The DotApp Session Manager (DSM) is a required component for session handling, replacing raw $_SESSION usage. DSM abstracts the underlying session driver (e.g., default, file, database, Redis), ensuring your application or module remains portable across different environments. Using DSM Import and use DSM as follows: use \Dotsystems\App\Parts\DSM; $dsm = new DSM("MyModuleStorage"); $dsm->load(); $dsm->set('variable1', "hello"); Alternatively, use the DSM facade for cleaner code (recommended): DSM::use("MyModuleStorage")->set('variable1', "hello"); echo DSM::use("MyModuleStorage")->get('variable1'); // Outputs: hello Each module should create its own storage (e.g., MyModuleStorage) to avoid conflicts with other modules. Variables in different storages can share the same name without collisions. Key DSM Methods set($name, $value): Sets a session variable. get($name): Retrieves a session variable. delete($name): Removes a session variable. clear(): Clears all variables in the storage. start(): Automatically called in the constructor. destroy(): Destroys the storage (optional). session_id(): Returns the session ID. load(): Loads the session (not needed with facade). save(): Saves the session (automatic on destruction). The most commonly used methods are: DSM::use("MyModuleStorage")->set('variable1', "hello"); DSM::use("MyModuleStorage")->get('variable1'); DSM::use("MyModuleStorage")->delete('variable1'); DSM::use("MyModuleStorage")->clear(); Why DSM? Using DSM instead of $_SESSION ensures your module is independent of the session driver. The facade approach eliminates the need for manual load() calls, making code cleaner and more maintainable. See Examples To see practical examples of these recommended practices, including database queries with DB::module() and session management with DSM, visit the Examples section. These examples demonstrate how to apply these practices in real-world scenarios.
    
    
    ---
    
    # DotApper CLI
    
    URL: https://dotapp.dev/documentation/dotapper-cli
    
    DotApper CLI Home Installation Commands Advanced FAQ
    Home Welcome to the DotApper CLI, the official command-line interface for the dotApp PHP Framework. DotApper is designed to streamline your development workflow, making it easy to set up, manage, and extend your dotApp projects with a simple and powerful set of commands. What is DotApper CLI? DotApper CLI is a lightweight utility that helps you: Install and update the dotApp PHP Framework. Create and manage modules, controllers, middleware, and models. Generate and optimize project assets like routes and .htaccess files. Boost productivity with an intuitive command-line interface. Built with simplicity and efficiency in mind, DotApper CLI is perfect for developers who want to focus on building modern web applications without getting bogged down by repetitive setup tasks. Why Use DotApper CLI? Fast Setup: Install dotApp in seconds with a single command. Modular Workflow: Easily create and organize modules for scalable projects. Developer-Friendly: Clear commands and helpful outputs save time. Seamless Integration: Works hand-in-hand with the dotApp framework's modular architecture. Get Started Ready to dive in? Head over to the Installation section to set up DotApper CLI and start your first project. Or, explore the Commands section to discover the full range of tools at your disposal. Quick Start Example: # Install dotApp with DotApper CLI php dotapper.php --install For more details, check out the full dotApp Documentation or reach out to our Support team. Proudly made in Slovakia 🇸🇰
    Installation Installing the DotApper CLI is simple and requires only downloading a single file, dotapper.php, to your local machine. This section explains how to obtain and prepare the DotApper CLI for use. Prerequisites Before installing DotApper CLI, ensure your environment meets the following requirement: PHP: Version 7.4 or higher, with command-line access to run PHP scripts. Installing DotApper CLI To install DotApper CLI, follow these steps to download the dotapper.php file: Open your web browser and navigate to the following link: https://github.com/dotsystems-sk/DotApp/raw/refs/heads/main/dotapper.php. The browser will display the source code of dotapper.php. Right-click anywhere on the page and select Save Page As (or similar, depending on your browser). Save the file as dotapper.php to your desired directory (e.g., your project folder or a tools directory). Verify that the file is saved correctly by opening a terminal and running: php dotapper.php --help This should display a list of available DotApper CLI commands, confirming that the tool is ready to use. Notes File Placement: You can place dotapper.php in any directory, but it’s typically stored in your project root for easy access. Execution: Run DotApper CLI commands using php dotapper.php [command] from the directory containing the file. Next Steps: Learn how to use DotApper CLI to manage dotApp projects in the Commands section. Troubleshooting: If the --help command fails, ensure PHP 7.4+ is installed and accessible in your terminal. Check the FAQ for common issues. With dotapper.php downloaded, you’re ready to explore its commands. Visit the Commands section to start using DotApper CLI.
    Commands The DotApper CLI provides a powerful set of commands to streamline development with the dotApp PHP Framework. This section details all available commands, their purposes, and how to use them. Commands are executed in the terminal using the dotapper.php script from the directory where it is located. Command Structure All DotApper CLI commands follow this format: php dotapper.php [command] Replace [command] with the specific command and its parameters as described below. To see a list of all commands, run: php dotapper.php --help Available Commands The following commands are available in DotApper CLI, each designed to simplify tasks such as installing the framework, managing modules, and inspecting routes. --help Displays a list of all available commands and their descriptions. Example: php dotapper.php --help Result: Outputs a help menu with all commands, e.g., --install, --create-module, etc. --install Installs a fresh copy of the dotApp PHP Framework in the current directory, creating its directory structure and configuration files. Example: php dotapper.php --install Result: Sets up the dotApp framework, including directories like app/, public/, and the app/config.php file. --update Updates the dotApp framework to the latest version while preserving existing configuration files and modules. Example: php dotapper.php --update Result: Downloads and applies the latest dotApp core files, keeping app/config.php and module directories intact. --create-module= Creates a new module with the specified name, initializing its directory structure for controllers, middleware, and models. Example: php dotapper.php --create-module=Blog Result: Creates a Blog module in app/modules/Blog/ with subdirectories for components. --modules Lists all modules currently available in the dotApp project. Example: php dotapper.php --modules Result: Outputs a list of module names, e.g., Blog, Auth, Shop. --module= --create-controller= Creates a new controller in the specified module. Example: php dotapper.php --module=Blog --create-controller=ArticleController Result: Generates ArticleController.php in app/modules/Blog/controllers/. --module= --create-middleware= Creates a new middleware in the specified module. Example: php dotapper.php --module=Blog --create-middleware=AuthMiddleware Result: Generates AuthMiddleware.php in app/modules/Blog/middleware/. --module= --create-model= Creates a new model in the specified module. Example: php dotapper.php --module=Blog --create-model=PostModel Result: Generates PostModel.php in app/modules/Blog/models/. --list-routes Lists all defined routes in the dotApp application, including their HTTP methods, paths, associated controllers, and middleware (before and after hooks). Example: php dotapper.php --list-routes Result: Outputs a detailed list of routes. Use Case: Useful for debugging routing issues or documenting the application’s routing structure. --list-route= Displays all routes that match the specified path, including their HTTP methods, associated controllers, and middleware (before and after hooks). The path can be exact (e.g., / for the homepage) or partial (e.g., /documentation/). Example 1: Homepage Routes php dotapper.php --list-route=/ Result: Outputs routes matching /. Example 2: Documentation Routes php dotapper.php --list-route=/documentation/ Result: Outputs routes matching /documentation/. Use Case: Helps identify which controllers and middleware are triggered for a specific URL, aiding in debugging or route optimization. --create-htaccess Creates or recreates a .htaccess file in the public/ directory to configure URL rewriting for Apache servers. Example: php dotapper.php --create-htaccess Result: Generates a .htaccess file optimized for dotApp's routing. --optimize-modules Optimizes module loading for projects with many modules, improving performance by caching module metadata. Example: php dotapper.php --optimize-modules Result: Creates or updates a module cache to reduce loading time. Command Summary The table below provides a quick reference for all DotApper CLI commands: Command Description --help Display a list of available commands --install Install a fresh copy of the dotApp framework --update Update dotApp core to the latest version --create-module= Create a new module --modules List all modules --module= --create-controller= Create a new controller in the specified module --module= --create-middleware= Create a new middleware in the specified module --module= --create-model= Create a new model in the specified module --list-routes List all routes with controllers and middleware --list-route= List routes matching the specified path with controllers and middleware --create-htaccess Create or recreate a .htaccess file --optimize-modules Optimize module loading Notes Ensure you run commands from the directory containing dotapper.php. Commands like --install, --create-module, and --list-routes require a valid dotApp project structure in the current directory. For practical examples of using these commands, see the Usage section. If a command fails, refer to the FAQ or contact Support. To see these commands in action, check out the Usage section for real-world examples.
    Advanced The DotApper CLI offers advanced features for developers who want to automate workflows, script complex tasks, or extend its functionality for custom needs in dotApp PHP Framework projects. This section explores scripting, automation, combining commands, and tips for extending DotApper CLI. Scripting with DotApper CLI You can combine multiple DotApper CLI commands in shell scripts (e.g., Bash or PowerShell) to automate repetitive tasks, such as setting up a new project with predefined modules and configurations. Example: Bash Script for Project Setup #!/bin/bash # setup_project.sh echo "Setting up new dotApp project..." php dotapper.php --install php dotapper.php --create-module=Blog php dotapper.php --module=Blog --create-controller=ArticleController php dotapper.php --module=Blog --create-middleware=AuthMiddleware php dotapper.php --module=Blog --create-model=PostModel php dotapper.php --create-htaccess echo "Project setup complete!" Usage: Save the script as setup_project.sh, make it executable (chmod +x setup_project.sh), and run it: ./setup_project.sh Result: Installs dotApp, creates a Blog module with a controller, middleware, and model, and generates a .htaccess file.
    FAQ This section addresses frequently asked questions about the DotApper CLI tool for the dotApp PHP Framework. It covers common issues, troubleshooting steps, and clarifications to help you use DotApper CLI effectively. 1. Why do I get a "command not found" error when running php dotapper.php? This error typically occurs if PHP is not installed, not accessible in your terminal, or if dotapper.php is not in the current directory. Solution: Verify PHP is installed and meets the minimum version requirement (7.4+): php --version If PHP is not installed, download it from php.net. Ensure you’re in the directory containing dotapper.php. Check with: ls | grep dotapper.php If missing, download it as described in the Installation section. Add PHP to your system’s PATH if it’s installed but not accessible. For example, on Linux: export PATH=$PATH:/path/to/php 2. Why does --install fail with a permissions error? The --install command requires write permissions in the current directory to create the dotApp framework’s files and directories. Solution: Check directory permissions: ls -ld . Ensure your user has write access. Grant write permissions: chmod -R u+w . Alternatively, run the command with elevated privileges (use cautiously): sudo php dotapper.php --install 3. What should I do if --list-routes shows no routes? If --list-routes or --list-route= returns an empty list, your dotApp project may not have defined routes or the project structure may be incomplete. Solution: Ensure the dotApp framework is installed: php dotapper.php --install Verify that routes are defined in the module’s module.init.php. For example: Router::get('/home', 'HelloWorld:Home@index!', Router::STATIC_ROUTE); Check for errors in the project directory by running: php dotapper.php --list-routes If errors persist, ensure the app/config.php file exists and is valid. 4. Can I use DotApper CLI without installing dotApp? Some commands (e.g., --help, --install) work without a dotApp project, but most commands (e.g., --update, --create-module, --list-routes) require a dotApp project structure in the current directory. Solution: Run --install to set up a dotApp project: php dotapper.php --install Alternatively, use --help to explore commands without a project: php dotapper.php --help 5. Why does --update overwrite my custom files? The --update command is designed to preserve app/config.php and module directories, but it may overwrite core framework files. Custom changes to core files will be lost. Solution: Back up your project before running --update: cp -r . /path/to/backup Avoid modifying core framework files. Instead, use modules for custom functionality (see Usage). If custom files were overwritten, restore from your backup and reapply changes in a module. 6. How do I debug issues with --list-route=? If --list-route= doesn’t return the expected routes, controllers, or middleware, the route may not be registered, or there may be an issue with the path format. Solution: Ensure the path is correct, including leading/trailing slashes (e.g., / or /documentation/). Run --list-routes to view all routes and confirm the target path exists: php dotapper.php --list-routes Check your routing definitions in controllers or routing files for errors. If middleware or controllers are missing, verify they are correctly registered in the module’s configuration. 7. Can I run DotApper CLI commands globally? DotApper CLI is designed to run from the project directory containing dotapper.php, but you can make it globally accessible by adding it to your system’s PATH. Solution: Move dotapper.php to a directory in your PATH (e.g., /usr/local/bin/ on Linux): sudo mv dotapper.php /usr/local/bin/dotapper sudo chmod +x /usr/local/bin/dotapper Run commands globally from any directory: php dotapper --help Note: You still need to be in a dotApp project directory for most commands to work. 8. What if a command fails without a clear error message? Some failures may result from PHP configuration, file permissions, or an incomplete dotApp setup. Solution: Enable verbose PHP errors by adding this to dotapper.php (temporarily): ini_set('display_errors', 1); ini_set('display_startup_errors', 1); error_reporting(E_ALL); Check PHP logs for detailed errors (location depends on your server configuration). Ensure all prerequisites are met (see Installation).
    
    
    ---
    
    # Step-by-step guide
    
    URL: https://dotapp.dev/documentation/step-by-step-guide
    
    Step By Step Philosophy Installation First Module & Setup First Controller Hello World Template system Form and layout Try It Live Download HelloWorld module
    Philosophy The DotApp PHP Framework is built with modularity at its core. From the ground up, DotApp is designed to provide a robust and scalable foundation for modern web applications, prioritizing modular architecture to ensure flexibility, maintainability, and efficiency. Why Modular Design? Modularity is the heart of DotApp’s philosophy. By structuring applications as a collection of independent, reusable modules, DotApp enables developers to: Build scalable applications with clear separation of concerns. Reuse components across projects, reducing development time. Maintain and update specific parts of an application without affecting the whole system. Integrate new features or third-party tools seamlessly. This approach ensures that your projects remain organized and adaptable, whether you're building a small prototype or a large-scale enterprise application. Robust Foundation, Recommended Practices DotApp provides a solid foundation with tools and conventions tailored for modular development. In this guide, we focus on the recommended practices that align with DotApp’s design goals: Module-Centric Workflow: Organize your application into self-contained modules for clarity and scalability. Consistent Structure: Follow DotApp’s conventions for controllers, templates, and configurations to streamline collaboration. Best Practices: Leverage built-in tools for routing, templating, and module management to avoid common pitfalls. Future-Proofing: Build with modularity to make future expansions or refactoring effortless. While DotApp is flexible enough to support alternative approaches, this guide emphasizes the methods that best utilize its modular architecture. We aim to teach techniques that maximize the framework’s strengths and help you avoid inefficient or error-prone patterns. What’s Next? Ready to start building with DotApp? Head to the Installation section to set up the framework and begin your modular journey. For a deeper dive into creating your first module, check out the First Module & Setup section. Proudly made in Slovakia 🇸🇰
    Installation Installing the DotApp PHP Framework is quick and flexible. Choose one of the three methods below to set up your project. Each method results in the same modular project structure, ready for development. Option 1: Git Clone If you have Git installed, you can clone the DotApp repository directly. Run the following command in your terminal: git clone https://github.com/dotsystems-sk/dotapp.git ./ This creates a DotApp project in your current directory. Don’t have Git? No problem—try one of the other methods. Option 2: DotApper CLI Download the dotapper.php CLI tool and use it to install DotApp. Follow these steps: Download the file: dotapper.php. Save it to your project directory. Run the installation command: php dotapper.php --install This sets up DotApp with all necessary dependencies. Option 3: ZIP Download Prefer a manual approach? Download the DotApp ZIP file and extract it: Download the ZIP: DotApp main.zip. Extract the contents to your project directory. Once extracted, your project is ready to use. Project Structure After installation, your project directory will have the following modular structure: project-root/ ├── index.php ├── dotapper.php ├── app/ │ ├── config.php │ ├── modules/ # your application logic │ │ └── HelloWorld/ │ │ ├── module.init.php │ │ ├── module.listeners.php │ │ ├── Controllers/ │ │ ├── Middleware/ │ │ ├── Models/ │ │ ├── views/ │ │ └── assets/ │ ├── parts/ # framework core — do not edit │ ├── runtime/ │ └── vendor/ └── assets/ ├── dotapp/ └── modules/ Application controllers, middleware, models, and views belong in app/modules/{ModuleName}/. app/parts/ is the framework core. Routes are declared in each module’s module.init.php. What’s Next? With DotApp installed, you’re ready to create your first module. Head to the First Module & Setup section to start building your modular application.
    First Module & Setup With the DotApp PHP Framework installed, you’re ready to create your first module. Modules are the core of DotApp’s modular architecture, allowing you to organize your application into reusable, self-contained components. In this section, we’ll create a HelloWorld module and configure it to serve /helloworld. Creating the Module Use the DotApper CLI to generate a new module. Run the following command in your project directory: php dotapper.php --create-module=HelloWorld You’ll see the output: Module successfully created in: ./app/modules/HelloWorld This creates a new HelloWorld module in the app/modules directory. Module Structure The HelloWorld module has the following structure: ├───modules │ │ .gitkeep │ │ │ └───HelloWorld │ │ module.init.php │ │ module.listeners.php │ │ │ ├───Api │ │ Api.php │ │ │ ├───assets │ │ howtouse.txt │ │ │ ├───Controllers │ │ Controller.php │ │ │ ├───Libraries │ ├───Middleware │ ├───Models │ ├───translations │ └───views │ │ clean.view.php │ │ │ └───layouts │ example.layout.php Here’s what each file and directory is for: module.init.php: Defines the module’s routes and initialization conditions, controlling when and how the module is loaded. module.listeners.php: Registers event listeners for the module, allowing it to respond to framework events like module loading. Api/Api.php: A sample API controller for building API endpoints (can be deleted or ignored). assets/: Stores module-specific assets like CSS, JavaScript, or images. Contains a howtouse.txt guide for beginners. Controllers/Controller.php: A sample controller (can be deleted or ignored). Libraries/: Holds custom PHP libraries or classes specific to the module. Middleware/: Contains middleware classes for request processing, such as authentication or validation. Models/: Stores model classes for database interactions or business logic. translations/: Manages language files for internationalization. views/: Contains view templates, including clean.view.php (a sample view) and layouts/example.layout.php (a sample layout), both of which can be deleted or ignored. The sample files (Api.php, Controller.php, clean.view.php, example.layout.php) are included as examples for beginners. In this guide, we’ll create our own controller and views, so you can safely delete or ignore these files. Configuring the Module Configure the HelloWorld module to serve /helloworld. Step 1: Event listeners Generated app/modules/HelloWorld/module.listeners.php is the place for module events. Routes go in initialize() (next steps). Leave register() empty unless you subscribe to events. DotApp fires several module-specific events if you need them later: dotapp.module.HelloWorld.init.start: Fired when module initialization begins. dotapp.module.HelloWorld.init.loading: Fired when the module’s main functions (e.g., routes) start loading, if initialization conditions are met. dotapp.module.HelloWorld.init.loaded: Fired after the module’s routes and functions are loaded. dotapp.module.HelloWorld.init.end: Fired when module initialization ends, regardless of whether conditions were met. dotapp.modules.loaded: Fired after all modules are loaded. Step 2: Configure Module Initialization Open app/modules/HelloWorld/module.init.php to define when the module should activate. Modify the initializeRoutes function to specify that the module activates for routes starting with /helloworld: public function initializeRoutes() { return ['/helloworld', '/helloworld/*']; } This ensures the module only activates for URLs starting with /helloworld (e.g., /helloworld, /helloworld/, /helloworld/sekcia). Using ['*'] (activating for all URLs) is less efficient and not recommended for large projects, so we optimize by specifying our route prefix. Next, configure the initializeCondition function to determine if the module should initialize based on the route match. By default, set it to: public function initializeCondition($routeMatch) { return $routeMatch; } This activates the module whenever a route from initializeRoutes matches. You can add custom logic. For example, to activate only in a given year: public function initializeCondition($routeMatch) { if ($routeMatch === true) { if (date("Y") == 2026) return true; } return false; } That example is only an illustration. For this guide, keep return $routeMatch;. Step 3: Define Initial Routes In the same module.init.php file, import Config and Router at the top, then define routes in initialize: public function initialize($dotApp) { Config::module('HelloWorld', 'prefix') ?? Config::module('HelloWorld', 'prefix', '/helloworld'); $p = rtrim((string) Config::module('HelloWorld', 'prefix'), '/'); Router::get($p, 'HelloWorld:Home@index!', Router::STATIC_ROUTE); Router::get($p . '/', 'HelloWorld:Home@index!', Router::STATIC_ROUTE); } This sets up static routes for /helloworld and /helloworld/, pointing to the index method of the Home controller in the HelloWorld module. Router::STATIC_ROUTE matches the exact path. What’s Next? Your HelloWorld module is now created and configured. Next, we’ll create the Home controller to handle the /helloworld route. Head to the First Controller section to continue.
    First Controller With your HelloWorld module configured, create a controller for the /helloworld route. Controllers live in app/modules/{Module}/Controllers/. This guide uses Home, which is also the controller shipped with the live demo. Creating the Controller Use the DotApper CLI to generate the Home controller: php dotapper.php --module=HelloWorld --create-controller=Home You’ll see: Controller 'Home' successfully created! That creates app/modules/HelloWorld/Controllers/Home.php. Setting Up the Controller Open that file and implement index as a public static method. The live demo renders a view with the Renderer facade. setView() must run before setViewVar(). If the view is missing, renderView() returns an empty string. namespace Dotsystems\App\Modules\HelloWorld\Controllers; use Dotsystems\App\Parts\Logger; use Dotsystems\App\Parts\Renderer; use Dotsystems\App\Parts\Response; class Home extends \Dotsystems\App\Parts\Controller { public static function index($request) { $html = Renderer::new() ->module('HelloWorld') ->setView('hello') ->setViewVar('title', 'Hello World') ->setViewVar('message', 'DotApp 2.0 is running.') ->renderView(); if ($html === '') { Logger::use()->error('HelloWorld view produced empty output'); return new Response(500, 'Template error'); } return $html; } } Create app/modules/HelloWorld/views/hello.view.php as a full HTML page (the live demo does not use a nested layout for this screen): {{ var: $title }} {{ var: $title }} {{ var: $message }} Back to the guide The route string is 'HelloWorld:Home@index!'. The trailing ! turns dependency injection off for that method. The first argument is always $request. What’s Next? Head to the Hello World section to open the page in the browser.
    Hello World Your HelloWorld module and Home controller are ready. This section confirms that /helloworld renders. Testing Your Application Use PHP’s built-in server from the project root: php -S 127.0.0.1:8000 Then open http://127.0.0.1:8000/helloworld (or /helloworld/). You should see the heading Hello World and the message from the view. Viewing the Hello World Page The live site serves the same module at /helloworld. Output comes from app/modules/HelloWorld/Controllers/Home.php rendering views/hello.view.php. Understanding the Flow module.init.php activates the module for /helloworld and /helloworld/*. Static routes map both slash variants to HelloWorld:Home@index!. Home::index builds HTML with Renderer::new()->module('HelloWorld')->setView('hello'). The documentation site serves / from the Docs module. HelloWorld is available at /helloworld. Congratulations Next, add a layout include and a named form. Head to Introduction to the template system.
    Introduction to the template system Hello World already rendered a view. This section names the pieces so you can grow that page: files, the Renderer facade, and the {{ … }} directives. The full reference lives on the documentation hub: Template system. Files View — app/modules/{Module}/views/{name}.view.php, selected with setView('name'). Layout — app/modules/{Module}/views/layouts/{path}.layout.php, selected with setLayout('path') or included as {{ layout:path }}. Assets — app/modules/{Module}/assets/..., served as /assets/modules/{Module}/.... {{ layout:h1-test }} loads views/layouts/h1-test.layout.php. Do not prefix the name with layouts/. Renderer $html = Renderer::new() ->module('HelloWorld') ->setView('hello') ->setViewVar('title', 'Hello World') ->renderView(); if ($html === '') { return new Response(500, 'Template error'); } Call setView() before setViewVar(). The second argument of setView() is a fallback view, not a wrapper layout. A missing file returns "" — no exception. Check the string. renderView() sees view variables only. Pass everything through setViewVar(). Directives Write Meaning {{ var: $title }} Print a value. Not {{ $title }}. {{ if … }} … {{ /if }} Conditional. Space after {{. {{ foreach $items as $item }} … {{ /foreach }} Loop. {{ layout:partials/header }} Include a layout file. {{ content }} Slot for setLayout() when you call renderView(). {{ formName(saveItem) }} Between  and . {{ enc(key): $id }} Encrypt a field. Decrypt with the same key. {{_ "Login" }} Translate a string. Load /assets/dotapp/dotapp.js on pages that submit  or call $dotapp().load(). What’s next The next section adds a notes page to Hello World: a layout include, a named form, and form() in the controller. That extra route is a local exercise — it is not on the public demo. For every directive and the render pipeline, open Template system.
    Hello World with a form and a layout Extend the live HelloWorld module on your own copy of the project. You will add /helloworld/notes, include a small layout, and process a named form. The public site keeps only /helloworld. Step 1: Route In app/modules/HelloWorld/module.init.php, next to the existing /helloworld routes: Router::match( ['GET', 'POST'], ['/helloworld/notes', '/helloworld/notes/'], 'HelloWorld:Home@index2!', Router::STATIC_ROUTE ); initializeRoutes() already returns /helloworld/*, so the new path is loaded with the module. The ! on the controller string turns dependency injection off; the method receives $request only. Step 2: Layout Create app/modules/HelloWorld/views/layouts/notes-heading.layout.php: 

    {{ var: $heading }}

    Step 3: View Create app/modules/HelloWorld/views/notes.view.php. {{ formName(saveNote) }} sits inside . The form has no action, so it posts to the current URL. {{ var: $title }} {{ layout:notes-heading }}

    {{ var: $lead }}

    {{ if $saved }}

    You submitted: {{ var: $saved }}

    {{ /if }} {{ formName(saveNote) }}

    Tips from foreach

      {{ foreach $tips as $tip }}
    • {{ var: $tip }}
    • {{ /foreach }}

    Back to Hello World

    This page does not use AJAX, so dotapp.js is optional for a classic POST. Keep the script if you later bind $dotapp().form('#noteForm') like the secure forms demo. Step 4: Controller Add index2 in app/modules/HelloWorld/Controllers/Home.php. Always pass an error callback to form(). Pass $request->getPath() so the encrypted handler matches the posted URL (with or without a trailing slash). public static function index2($request) { $saved = ''; $request->form(['POST'], 'saveNote', function ($request) use (&$saved) { $saved = (string) ($request->data()['note'] ?? ''); }, function () { // Required. Runs when the name does not match or the signature is invalid. }, $request->getPath()); $html = Renderer::new() ->module('HelloWorld') ->setView('notes') ->setViewVar('title', 'Notes') ->setViewVar('heading', 'Notes') ->setViewVar('lead', 'Submit a line of text. The next render shows it below the heading.') ->setViewVar('btnText', 'Save') ->setViewVar('saved', $saved) ->setViewVar('tips', [ 'setView() before setViewVar()', 'formName stays between fo-rm tags', 'Empty renderView() means a missing file', ]) ->renderView(); if ($html === '') { Logger::use()->error('HelloWorld notes view produced empty output'); return new Response(500, 'Template error'); } return $html; } $request->data() is the XSS-protected bag, which is what you want to print. Use $request->data(true) when you decrypt or compare secrets. Do not build HTML strings in the controller — pass data and format it in the view. Try it From the project root: php -S 127.0.0.1:8000 Open http://127.0.0.1:8000/helloworld/notes. You should see the heading from the layout, three tips from foreach, and the form. Submit text; the page reloads and shows the protected value. Next: Try it live for the public Hello World page, or the full template system reference (directives, assets, custom renderers, sandbox). Try it live The HelloWorld module from this guide is running on this site. You do not need a local server to see the first page. Hello World Open /helloworld. That is HelloWorld:Home@index! rendering views/hello.view.php with Renderer::new(). Notes page (local only) The form-and-layout exercise in the previous section (/helloworld/notes) is not deployed here. Add that route on your own copy, then compare it with the public page. What to read next Template system — every directive, assets, custom renderers, sandbox. Secure forms — fo-rm, CRC, encrypted fields. Examples — live demos. --- # For AI agents URL: https://dotapp.dev/documentation/ai-agents For AI agents DotApp PHP Framework 2.0 ships machine-readable documentation so Cursor, Claude, GPT, and other coding agents write real DotApp code instead of Laravel-shaped guesses. This page is the official entry point for crawlers and for people who wire an agent into a DotApp project. Preferred index Start here. These files are public, MIT licensed, and meant to be retrieved, cited, and used for training while the author name stays in library headers. https://dotapp.dev/llms.txt — short catalog of every documentation URL. https://dotapp.dev/.well-known/llms.txt — the same index at the well-known path. https://dotapp.dev/llms-full.txt — concatenated plain text of the official docs. GitHub repository — source, AIRULES, and module examples. Cite as: DotApp PHP Framework 2.0, Dotsystems s.r.o., Slovakia. Contact: dotapp@dotapp.dev. AIRULES The framework repository contains an AIRULES/ folder next to index.php. Agents should read AIRULES/00-AGENT-CONTRACT.md first. The contract is not optional color: it is how DotApp applications are structured. Application routes live in modules, not in index.php. Controllers are static strings with a trailing !: Module:Ctrl@fn!. Views use Renderer::new()->module()->setView() before setViewVar. Interactive posts use fo-rm with formName(handler) between the tags. Do not invent Laravel or jQuery APIs. The browser API is $dotapp. MCP Documentation tools are available at POST {{ var: $host }}/mcp: docs_list_pages — slug, title, URL, description for every official page. docs_search — full-text search over the catalog. docs_get_page — plain text for one slug such as router or examples/forms. Chapter URLs Each core topic has its own URL, title tag, and search document. Prefer these over a single long page: {{_ "Documentation" }} Installation Router Dependency injection DotBridge Database Templates Configuration Examples --- # Examples URL: https://dotapp.dev/documentation/examples --- # Forms URL: https://dotapp.dev/documentation/examples/forms Examples Introduction Creating the Examples Module Creating the Forms Controller Configuring Routes Creating View and Layout Handling Forms Rendering Theory Live Demo Forms Example Open the live demo at /documentation/examples/run/forms. This example demonstrates named server-rendered forms in the DotApp PHP Framework. The live page is plain HTML, posts to the same URL, and does not load dotapp.js. Introduction Three forms can share one POST endpoint when each form contains a {{ formName(Name) }} token. The controller calls $request->form(['POST'], 'Name', $ok, $err) for each expected form name and returns the rendered page for the form that matches. Review the module and routing basics first if you are new to DotApp: DotApper CLI Configuration Recommended Practices Step-by-Step Guide Creating the Examples Module Create the Examples module with the DotApper CLI: php dotapper.php --create-module=Examples The live module activates only for the example runner URLs. In /app/modules/Examples/module.init.php, keep the route scope explicit: public function initializeRoutes() { return ['/documentation/examples/run', '/documentation/examples/run/*']; } Creating the Forms Controller Create a controller named Forms for the Examples module: php dotapper.php --module=Examples --create-controller=Forms Controllers in DotApp 2.0 expose public static action methods and are referenced with module controller strings such as 'Examples:Forms@index!'. Configuring Routes Define a route pair for both the slash and no-slash URL. The live module uses a small helper so GET and POST stay consistent: public function initialize($dotApp) { Config::module('Examples', 'prefix') ?? Config::module('Examples', 'prefix', '/documentation/examples/run'); $p = rtrim((string) Config::module('Examples', 'prefix'), '/'); $pair = function (string $path): array { $path = rtrim($path, '/'); return [$path, $path . '/']; }; Router::get($pair($p . '/forms'), 'Examples:Forms@index!', Router::STATIC_ROUTE); Router::post($pair($p . '/forms'), 'Examples:Forms@submit!', Router::STATIC_ROUTE); } The GET action renders the form page. The POST action checks the submitted form name and returns a fresh HTML response. Creating the View The live demo uses a standalone view at /app/modules/Examples/views/forms.view.php. The file is a complete HTML document. {{ var: $title }} - DotApp PHP Framework 2.0

    Named forms

    Three forms post to the same URL. This demo does not use dotapp.js.

    {{ if $formNumber }}
    Using form {{ var: $formNumber }}, you submitted the text: {{ var: $formText }}
    {{ /if }} {{ formName(Form1) }}
    {{ formName(Form2) }}
    {{ formName(Form3) }}
    The {{ formName(Form1) }}, {{ formName(Form2) }}, and {{ formName(Form3) }} tags must be inside their corresponding
    tags. Handling Forms The live controller returns the HTML string. Each $request->form() call includes both a success callback and an error callback so non-matching form checks can safely continue. use Dotsystems\App\Parts\Logger; use Dotsystems\App\Parts\Renderer; use Dotsystems\App\Parts\Response; class Forms extends \Dotsystems\App\Parts\Controller { public static function index($request) { return self::formPage('', 0); } public static function submit($request) { $attempts = [ 1 => ['Form1', 'textfrom1'], 2 => ['Form2', 'textfromanother'], 3 => ['Form3', 'textfromanother'], ]; foreach ($attempts as $num => $spec) { $html = $request->form(['POST'], $spec[0], function ($request) use ($num, $spec) { $text = (string) ($request->data()[$spec[1]] ?? ''); return self::formPage($text, $num); }, function () { return null; }); if (is_string($html) && $html !== '') { return $html; } } return self::formPage('', 0); } private static function formPage(string $text, int $formNumber) { return self::view('forms', [ 'title' => 'Named forms demo', 'docsUrl' => '/documentation/examples/forms', 'btnName' => 'Send', 'formNumber' => $formNumber, 'formText' => htmlspecialchars($text, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'), ]); } private static function view(string $name, array $vars) { $r = Renderer::new()->module('Examples')->setView($name, 'clean'); foreach ($vars as $key => $value) { $r->setViewVar($key, $value); } $html = $r->renderView(); if ($html === '') { Logger::use()->error('Examples view empty', ['view' => $name]); return new Response(500, 'Template error'); } return $html; } } Renderer::new()->module('Examples')->setView('forms', 'clean') selects the standalone view before any variables are assigned with setViewVar(). Rendering Theory DotApp can render a full view directly or render a view that contains {{ content }} and a layout. This live example uses the direct standalone-view approach because the demo page is self-contained. $r = Renderer::new()->module('Examples')->setView('forms', 'clean'); $r->setViewVar('btnName', 'Send'); $html = $r->renderView(); Layout rendering applies when a shared wrapper is useful. This walkthrough uses the standalone forms.view.php page. Live Demo Try the live demo at /documentation/examples/run/forms. It posts three named forms to one endpoint and returns the rendered page from the controller. --- # Secure forms URL: https://dotapp.dev/documentation/examples/secure-forms Examples Introduction Prerequisites Creating the View Adding Styles Creating the Layout Configuring Routes Updating the Controller Implementing JavaScript Using the Crypto Facade Live Demo Secure Forms with dotapp.js Open the live demo at /documentation/examples/run/forms2. This example is the live Examples:Forms@index2! / @submit2! page. Select values are encrypted in the template. The browser posts with fo-rm, formName, CRC, and dotapp.js. The page does not reload. Prerequisites Start from the named-forms example so the Examples module and Forms controller already exist. Forms Example Step-by-Step Guide Creating the View The live demo uses a standalone view at /app/modules/Examples/views/forms2.view.php. The file is a complete HTML document. {{ var: $title }} — DotApp PHP Framework 2.0 Documentation Secure form Secure form Select values are encrypted in the template. The post uses fo-rm, CRC, and dotapp.js. Name Category Select a category Support Feedback Other Select created by foreach Select a category again {{ foreach $items as $item }} {{ var: $item['text'] }} {{ /foreach }} {{ formName(CSRF) }} Submit Submit the form to see the result Put {{ formName(CSRF) }} between the tags. Set action to the current path (the live demo uses $request->getPath()). Pass the same path as the last argument of form() so the handler matches. Load /assets/dotapp/dotapp.js with a unique query on each page render so the session CSRF token is not reused from a cached docs page. Encrypted option values use {{ enc(additionalKey): "SupportVal" }}. Adding Styles Shared example chrome lives at /app/modules/Examples/assets/css/examples.css and is served as /assets/modules/Examples/css/examples.css. Creating the Layout setView('forms2') renders the full HTML document. Modules can also wrap views with {{ layout:name }}. This demo keeps the view self-contained. Configuring Routes Router::get($pair($p . '/forms2'), 'Examples:Forms@index2!', Router::STATIC_ROUTE); Router::post($pair($p . '/forms2'), 'Examples:Forms@submit2!', Router::STATIC_ROUTE); Register those lines in initialize($dotApp) next to the named-forms routes. $pair returns both slash variants. Updating the Controller Methods are public static and take $request only. Use Renderer::new(). When the callable string ends with !, the method does not receive injected services as extra arguments. public static function index2($request) { $items = [ ['value' => 'ValueItem1', 'text' => 'Text item 1'], ['value' => 'ValueItem2', 'text' => 'Text item 2'], ['value' => 'ValueItem3', 'text' => 'Text item 3'], ]; return self::view('forms2', [ 'title' => 'Secure form demo', 'docsUrl' => '/documentation/examples/secure-forms', 'items' => $items, ]); } public static function submit2($request) { $answer = ['code' => 403, 'body' => ['status' => 0, 'error' => 1, 'error_txt' => 'CRC check failed!', 'message' => 'CRC check failed!']]; if ($request->crcCheck()) { $answer = $request->form(['POST'], 'CSRF', function ($request) { $payload = $request->data(true)['data'] ?? []; $categoryVal = Crypto::decrypt((string) ($payload['category'] ?? ''), 'additionalKey'); $foreachVal = Crypto::decrypt((string) ($payload['foreach'] ?? ''), 'additionalKey2'); if ($categoryVal === false || $foreachVal === false) { return [ 'code' => 403, 'body' => ['status' => 0, 'message' => 'Data manipulation detected!'], ]; } return [ 'code' => 200, 'body' => [ 'status' => 1, 'text' => 'Category: ' . $categoryVal . ', Foreach: ' . $foreachVal, 'message' => 'Form submitted successfully.', ], ]; }, function () { return ['code' => 403, 'body' => ['status' => 0, 'message' => 'Invalid signature']]; }, $request->getPath()); } if (!is_array($answer) || !isset($answer['body'])) { return DotApp::DotApp()->ajaxReply(['status' => 0, 'error' => 1, 'error_txt' => 'Invalid signature', 'message' => 'Invalid signature'], 403); } return DotApp::DotApp()->ajaxReply($answer['body'], $answer['code']); } crcCheck() runs before form(). The form name is CSRF because the view uses formName(CSRF). The error callback on form() is required. The last argument of form() is $request->getPath() so the encrypted handler matches the POST URL. AJAX replies go through DotApp::DotApp()->ajaxReply($body, $code). Implementing JavaScript Live file: /app/modules/Examples/assets/js/forms2.js. The client API is $dotapp. Wait for the dotapp event if the library is still loading. (function () { var runMe = function ($dotapp) { $dotapp() .form("#example2") .before(function (data, form) { if ($dotapp(form).attr("blocked") == 1) return $dotapp().halt(); $dotapp(form).attr("blocked", "1"); $dotapp("#noteBtn").attr("loading", "true").attr("loader", "dots"); }) .after(function (data, response, form) { var reply = $dotapp().parseReply(response); if (reply && (reply.status == 1 || reply.error == 0)) { $dotapp(".output").html("Form submitted successfully!" + (reply.text || reply.message || "")); } else if (reply && (reply.error_txt || reply.message)) { $dotapp("#error-message").attr("hide", "false").html(reply.error_txt || reply.message); } $dotapp(form).attr("blocked", "0"); }); }; if (window.$dotapp) runMe(window.$dotapp); else window.addEventListener("dotapp", function () { runMe(window.$dotapp); }, { once: true }); })(); Using the Crypto Facade Template encryption and PHP decryption must use the same extra key. Failed decryption returns false — compare with === false, never treat it as a string. $categoryVal = Crypto::decrypt((string) ($payload['category'] ?? ''), 'additionalKey'); if ($categoryVal === false) { // tampered or wrong key } Live Demo /documentation/examples/run/forms2 --- # DotBridge URL: https://dotapp.dev/documentation/examples/dotbridge Examples Introduction Prerequisites Creating the View Adding Styles Creating the Layout DotBridge Inputs DotBridge Filters DotBridge Events DotBridge JavaScript Functions Configuring Routes Updating the Controller Implementing JavaScript Error Codes Live Demo DotBridge Example Open the live demo at /documentation/examples/run/bridge (also available at /documentation/examples/run/forms3). Introduction This example is the live Examples:Forms@index3! / @submit3! page. Template tags call PHP over an encrypted AJAX POST to the current page URL. Inputs can be filtered, encrypted, rate-limited, and bound to a specific URL. Prerequisites Start from the named-forms example so the Examples module and Forms controller already exist. Forms Example DotBridge Creating the View The live demo uses a standalone view at /app/modules/Examples/views/bridge.view.php. The file is a complete HTML document. {{ var: $title }} — DotApp PHP Framework 2.0
    Documentation DotBridge

    DotBridge

    Each button calls PHP over the current page URL.

    Submit only once
    Submit 2× per minute
    Submit no limit
    Submit with the first button to see the result
    Submit with the second button to see the result
    Submit with the third button to see the result
    Adding Styles Shared example chrome lives at /app/modules/Examples/assets/css/examples.css and is served as /assets/modules/Examples/css/examples.css. Creating the Layout setView('bridge') renders the full HTML document. Modules can also wrap views with {{ layout:name }}. This demo keeps the view self-contained. DotBridge Inputs Mark fields with {{ dotbridge:input="name" }} or the short form dotbridge="name". Nested names such as email.address arrive in PHP as $request->data(true)['data']['email.address']. Built-in filters include email, url, phone, password, date, time, creditcard, username, and ipv4. Register more with Bridge::addFilter($name, $callback). DotBridge Filters The email field uses email.address(email, 6, email_ok, email_bad): the email filter, a minimum length of 6, and optional success/failure callbacks. Category options are encrypted in the template with {{ enc(additionalKey): "AdminVal" }} and decrypted in PHP with the same extra key. DotBridge Events Bind a click (or another event) with {{ dotbridge:on(click)="functionName(arg1, arg2)" }}. Modifiers on the same tag control reuse and limits: oneTimeUse — the key is consumed after one successful call. regenerateId — a fresh id is issued after the call. rateLimit(seconds,count) — you can stack several windows, e.g. rateLimit(60,2) rateLimit(3600,5). url(/path) — POST target for that call. An invalid bound URL returns HTTP 403 with error_code 6. DotBridge JavaScript Functions Pair each template function name with $dotapp().bridge(name, event). Hooks: before, onValueError, after, and onResponseCode. Wait for the dotapp event if the library is still loading. Configuring Routes Register the page GET and bind each bridge function to the same URLs in initialize($dotApp): $bridgePages = array_merge($pair($p . '/forms3'), $pair($p . '/bridge')); Router::get($bridgePages, 'Examples:Forms@index3!', Router::STATIC_ROUTE); foreach (['example.showEmailCategory', 'example.showEmailCategory2', 'example.showEmailCategory3'] as $fn) { Router::bridge($bridgePages, $fn, 'Examples:Forms@submit3!', Router::STATIC_ROUTE); } Router::bridge($urls, $functionName, $handler, Router::STATIC_ROUTE) scopes the handler to those page URLs. Closures use Bridge::listen() with the same argument order. Updating the Controller Methods are public static and take $request. The GET action renders the view. The bridge action reads the payload and returns a string (or an array), which becomes the JSON body. public static function index3($request) { return self::view('bridge', [ 'title' => 'DotBridge demo', 'docsUrl' => '/documentation/examples/dotbridge', ]); } public static function submit3($request) { $payload = $request->data(true)['data'] ?? []; $email = (string) ($payload['email.address'] ?? ''); $category = Crypto::decrypt((string) ($payload['category'] ?? ''), 'additionalKey'); if ($category === false) { $category = '(invalid category)'; } return 'This is reply from submit3() function. Email: ' . $email . ', category: ' . $category; } Failed decryption returns false — compare with === false. Implementing JavaScript Live file: /app/modules/Examples/assets/js/bridge.js. The client API is $dotapp. (function () { var runMe = function ($dotapp) { function before(selector) { var categoryInput = $dotapp('[dotbridge-input="category"]').val(); if (categoryInput === null || categoryInput === "") { alert("Select a category."); return $dotapp().halt(); } $dotapp(selector).html("Loading...").removeClass("error").removeClass("success").addClass("loading"); } function after(selector, body) { var text = ""; if (typeof body === "string") text = body; else if (body && body.body) text = body.body; else if (body) text = String(body); if (text) { $dotapp(selector).html(text).removeClass("error").removeClass("loading").addClass("success"); } } function onResponse(selector, data) { var reply = $dotapp().parseReply(data); var text = (reply && reply.status_txt) ? reply.status_txt : "Request failed."; $dotapp(selector).html(text).addClass("error").removeClass("loading").removeClass("success"); } $dotapp() .bridge("example.showEmailCategory", "click") .before(function () { return before("#output1"); }) .onValueError(function (inputname) { if (inputname == "email.address") alert("Enter a valid email address."); }) .after(function (body) { after("#output1", body); }) .onResponseCode(function (status, text) { onResponse("#output1", text); }, 429); }; if (window.$dotapp) runMe(window.$dotapp); else window.addEventListener("dotapp", function () { runMe(window.$dotapp); }, { once: true }); })(); Repeat the same .bridge() chain for example.showEmailCategory2 and example.showEmailCategory3 with their output selectors. Error Codes Success is HTTP 200 with JSON { status: 1, body: } and the header X-Answered-By: dotbridge. HTTP 400, error_code 1: CRC check failed. HTTP 403, error_code 2: bridge key mismatch. HTTP 404, error_code 3: function not registered or not callable. HTTP 429, error_code 4: rate limit, invalid id, or missing valid key. HTTP 403, error_code 5: CSRF referer mismatch. HTTP 403, error_code 6: invalid dotbridge-url. Live Demo /documentation/examples/run/bridge and /documentation/examples/run/forms3 render the same page. --- # Users module URL: https://dotapp.dev/documentation/examples/users-module Examples Introduction Prerequisites Module Creation Database Setup Configuration Controllers Middleware Routes View Layouts Assets JavaScript Functionality Live Demo Users Module Example Build the live DotApp 2.0 Users module: standalone login, registration, two-factor confirmation, a protected app page, and AJAX forms powered by dotapp.js. Open the live demo. Overview The live module lives in /app/modules/Users. DotApp routes requests directly to public static controller methods such as Users:Login@page!, Users:Login@save!, and Users:Register@save!. The default URL prefix is configured with Config::module('Users', 'prefix') and defaults to /documentation/examples/run/users. The tutorial below uses those /users routes. Prerequisites This example assumes the secure forms flow is familiar: DotApp signs forms, validates submissions with $request->crcCheck(), and returns JSON through DotApp::DotApp()->ajaxReply($body, $code). Secure Forms with dotapp.js A MySQL database configured in /app/config.php. Framework auth tables prepared with php dotapper.php --prepare-database. Module Creation Create the module, two controllers, and the route gate middleware with DotApper: php dotapper.php --create-module=Users php dotapper.php --module=Users --create-controller=Login php dotapper.php --module=Users --create-controller=Register php dotapper.php --module=Users --create-middleware=AuthGate The live module contains Controllers/Login.php, Controllers/Register.php, Middleware/AuthGate.php, standalone views in views/, and the frontend script at assets/js/users.js. Database Setup Users and authentication tables are framework tables generated by the database preparation command. They use your configured database prefix. php dotapper.php --prepare-database The demo needs MySQL to be configured before registration or login will work. If the configured database list is empty, calls such as Auth::createUser() and Auth::login() fail and the controllers return a friendly unavailable message. Configuration Configure your database in /app/config.php. The exact credentials depend on your local environment: Config::db('driver', 'pdo'); Config::addDatabase('main', '127.0.0.1', 'Username', 'Password', 'DBNAME', 'UTF8', 'MYSQL', 'pdo'); The module initializes its prefix when it starts: Config::module('Users', 'prefix') ?? Config::module('Users', 'prefix', '/documentation/examples/run/users'); $p = rtrim((string) Config::module('Users', 'prefix'), '/'); Override Users.prefix in project configuration when you want the same module mounted somewhere else. Controllers DotApp calls public static controller methods. The trailing ! in route targets marks methods such as page(), save(), twoFactorPage(), twoFactorSave(), app(), and logout(). These methods do not use dependency injection. Views are rendered with Renderer::new()->module('Users')->setView($name), followed by any setViewVar() calls. If the renderer returns an empty string, the live controllers return new Response(500, 'Template error'). Login page and save flow --- # AJAX lists URL: https://dotapp.dev/documentation/examples/ajax-lists Examples Introduction Routes PHP list + pager JavaScript Live demo AJAX lists Open the live demo at /documentation/examples/run/lists. Introduction A growing table is a backend problem and a frontend problem. PHP paginates with paginate(), encrypts row ids, and returns HTML fragments. dotapp.js patches #listInner after $dotapp().load(). Row toggle and delete are buttons, not one per row. Secure forms — the save form on the same page Custom JS library — reusable widgets around the same load() transport Routes GET renders the page. POST endpoints return ajaxReply JSON for the list, save, delete, and toggle actions: Router::get($pair($p . '/lists'), 'Examples:Lists@page!', Router::STATIC_ROUTE); Router::post($pair($p . '/lists/list'), 'Examples:Lists@list!', Router::STATIC_ROUTE); Router::post($pair($p . '/lists/save'), 'Examples:Lists@save!', Router::STATIC_ROUTE); Router::post($pair($p . '/lists/delete'), 'Examples:Lists@delete!', Router::STATIC_ROUTE); Router::post($pair($p . '/lists/toggle'), 'Examples:Lists@toggle!', Router::STATIC_ROUTE); PHP list + pager Search runs from three characters. Ids leave PHP as Crypto::encrypt($id, 'Examples.item.id'). Decrypt with the same extra key. Failure is === false. $result = DB::module('RAW')->q(function ($qb) use ($q, $useSearch) { $qb->select(['id', 'title', 'active', 'created_at'])->from('examples_items')->orderBy('id', 'DESC'); if ($useSearch) { $esc = str_replace(['\\', '%', '_'], ['\\\\', '\%', '\_'], $q); $qb->where('title', 'LIKE', '%' . $esc . '%'); } })->paginate(10, $page); The fragment view lists-inner.view.php is what JS puts into #listInner. Return it in html next to status and message. JavaScript Cover the list with .ex_busy until load() finishes. Patch a child, not the overlay wrapper. Delete uses a graphical dialog — never alert() or window.confirm(). $dotapp().load(listUrl, "POST", { page: page, q: q }, function (raw) { var reply = $dotapp().parseReply(raw); if (reply && reply.html) $dotapp("#listInner").html(reply.html); listDone(); }, function () { listDone(); } ); The page script lives at /app/modules/Examples/assets/js/lists.js. Live demo /documentation/examples/run/lists needs MySQL in app/config.php. Without a database the page explains that the demo is not ready. --- # Custom JS library URL: https://dotapp.dev/documentation/examples/js-library Examples Introduction Boot events Registering fn() Calling PHP from the widget CSS Live demo Custom $dotapp library Open the live demo at /documentation/examples/run/library. Introduction DotApp is not “PHP plus a random jQuery file”. The same request model (load, CRC, parseReply) is how you extend the frontend. You write a vanilla widget, then hang it on $dotapp with $dotapp().fn('name', fn). Put the file in your module assets. Never edit app/parts/js/. The live Examples module ships exNotify (factory toasts), exCopy (per-button clipboard), exStepper (one node → API), and exPing (widget that POSTs to PHP). Boot events Libraries register on dotapp-register. Page scripts run on dotapp. Load order: Guard double registration with isRegistered and catch an error message that contains already registered. Registering fn() Factory (empty $dotapp()): toasts and dialogs. Per-element: one node returns the widget API, many nodes return this. (function () { var isRegistered = false; var runMe = function ($dotapp) { if (isRegistered) return; isRegistered = true; $dotapp().fn("exNotify", function (opts) { if (opts && typeof opts === "object") { return ExNotify.show(opts); } return this; }); $dotapp().fn("exStepper", function (options) { var els = this.getElements(); if (els.length !== 1) { throw new Error("exStepper requires exactly one element"); } var el = els[0]; if (!el._exStepper) el._exStepper = new ExStepper(el, options || {}); return el._exStepper; }); }; if (window.$dotapp) runMe(window.$dotapp); else window.addEventListener("dotapp-register", function () { runMe(window.$dotapp); }, { once: true }); })(); Page code then does $dotapp().exNotify({ title: "Saved", text: "…" }) and $dotapp("#qty").exStepper(). Do not wrap $.fn.plugin. Rewrite in vanilla DOM. Calling PHP from the widget Inside the library, HTTP goes through this.load + parseReply. Never fetch a DotApp endpoint. ExPing.prototype.send = function () { var self = this; this.dotApp.load(this.settings.url, "POST", {}, function (raw) { var reply = self.dotApp.parseReply(raw); ExNotify.show({ title: "PHP", text: (reply && reply.message) ? reply.message : "No reply" }); } ); }; PHP answers with crcCheck() and ajaxReply: public static function ping($request) { if (!$request->crcCheck()) { return DotApp::DotApp()->ajaxReply(['status' => 0, 'message' => 'Bad request'], 400); } return DotApp::DotApp()->ajaxReply([ 'status' => 1, 'message' => 'PHP answered at ' . date('H:i:s'), ], 200); } CSS Keep widget classes on {modulename}_* or a shared demo sheet. The live styles are in /app/modules/Examples/assets/css/examples.css and are served as /assets/modules/Examples/css/examples.css. Toast stack, stepper, and snippet block live there so you can copy the look into your module. Live demo /documentation/examples/run/library — toasts, copy, stepper, and a PHP ping. Source: ex-ui.js, library.js, Controllers/Library.php. --- # Reactivity URL: https://dotapp.dev/documentation/examples/reactivity Examples Introduction variable, databind, computed Quote from PHP Official reactive add-on Live demo Reactivity Open the live demo at /documentation/examples/run/live. Introduction The live demo is a price quote: product, quantity, destination, and an optional coupon. Core dotapp.js already has variable, databind, and computed. The line item updates in the browser. VAT, shipping, stock, and coupon rules stay in PHP and return over the same secure $dotapp().load() used by lists and forms. That is the complete loop: catalog and tax tables on the server, ajaxReply on the wire, variables in the browser. Empty #error-message / #status bars stay hidden until PHP actually has an error or an info line. variable, databind, computed $dotapp().variable("qty", "1"); $dotapp().variable("name", "Workshop seat"); $dotapp().variable("unit", "49.00"); $dotapp().variable("total", "0.00"); var qty = $dotapp().getVariable("qty"); var total = $dotapp().getVariable("total"); total.bindToElement(document.getElementById("bindTotal")); var line = $dotapp().computed(function () { var q = $dotapp().getVariable("qty").value; var n = $dotapp().getVariable("name").value; var u = $dotapp().getVariable("unit").value; return q + " × " + n + " @ €" + u; }); qty.onChange(function () { line.invalidate(); document.getElementById("bindLine").textContent = line.value; }); // after parseReply: total.value = String(reply.total); Assigning .value on the singleton variable updates every element bound with bindToElement / databind on that same $dotapp() instance. Variables are not shared across $dotapp('#x') clones. Bind from $dotapp().getVariable('total').bindToElement(el). The coupon field uses two-way binding with { live: true }, so typing updates the variable on input and a debounced load() follows. Quote from PHP POST with CRC. Cover the quote while the request runs. Write the payload into variables — do not replace the whole card unless you need a fragment. $dotapp().load(quoteUrl, "POST", { id: encId, qty: qty, country: country, coupon: coupon }, function (raw) { var reply = $dotapp().parseReply(raw); total.value = String(reply.total); stock.value = String(reply.stock); if (reply.error) $dotapp("#error-message").attr("hide", "false").html(reply.error); else if (reply.message) $dotapp("#status").attr("hide", "false").html(reply.message); else { $dotapp("#error-message").attr("hide", "hide").html(""); $dotapp("#status").attr("hide", "hide").html(""); } } ); PHP decrypts the product id, applies VAT by country, shipping (free over €80 on physical goods), stock, and coupon DOTAPP10. DSM counts how many quotes this session actually requested: $sku = Crypto::decrypt($id, 'Examples.live.id'); $dsm = DSM::use('Examples'); $hits = (int) ($dsm->get('live_quotes') ?? 0); $hits++; $dsm->set('live_quotes', $hits); return DotApp::DotApp()->ajaxReply([ 'status' => 1, 'total' => $total, 'stock' => $stock, 'error' => $error, 'message' => $message, 'hits' => $hits, ], 200); Out of stock and a bad coupon use the red bar. A valid coupon or free shipping uses the blue bar. Both stay hide="hide" until then. Official reactive add-on Optional dotapp.reactive.js adds HTML attributes (reactive-api, reactive-interval, reactive-trigger, reactive-variable, reactive-template) and $dotapp().reactive(url, config). Load it after dotapp.js when your build serves /assets/dotapp/dotapp.reactive.js. The live demo on this site uses the core APIs so it always runs: they are already inside dotapp.js.
    Live demo /documentation/examples/run/live — live quote, bound totals, computed line, coupon and stock messages. Files: Controllers/Live.php, live.view.php, live.js. --- # Storefront URL: https://dotapp.dev/documentation/examples/storefront Examples Introduction Markup and CSS Catalog, cart, DSM Search, add, checkout Live demo Storefront Open the live demo at /documentation/examples/run/store. Introduction A shop is the shortest way to show that DotApp is a full stack. PHP owns the catalog and the cart. CSS lays out the grid. dotapp.js searches, adds lines, and submits checkout without reloading the page. Product ids are encrypted. Session state lives in DSM::use('Examples'), never in $_SESSION. Custom JS library — toasts when a product is added Secure forms — the checkout fo-rm AJAX lists — the same overlay + parseReply pattern NOVA shop — Pro example: catalog cache, cart, checkout, orders desk Markup and CSS The page is a complete HTML document in store.view.php. Grid and cart fragments are separate views so AJAX can replace them. Shared look: /assets/modules/Examples/css/examples.css (.ex-products, .ex-product, .ex-cart-panel, .ex-toast-stack).
    {{ var: $gridHtml }}
    Catalog, cart, DSM Encrypted sku on every card and cart line: $enc = Crypto::encrypt($item['sku'], 'Examples.product.id'); $sku = Crypto::decrypt((string) ($data['id'] ?? ''), 'Examples.product.id'); if ($sku === false) { return DotApp::DotApp()->ajaxReply(['status' => 0, 'message' => 'Invalid product.'], 200); } Cart in DSM: $cart = DSM::use('Examples')->get('cart'); if (!is_array($cart)) { $cart = []; } DSM::use('Examples')->set('cart', $cart); Search is a lookup over a small in-memory catalog (six merch items). It still follows the list rule: debounce, fire from three characters, overlay, patch #gridInner. Checkout is a real fo-rm on the same URL as the page. Search, add, checkout $dotapp().live("click", ".js-store-add", function (btn) { var id = btn.getAttribute("data-item"); $dotapp().load(addUrl, "POST", { id: id }, function (raw) { var reply = $dotapp().parseReply(raw); if (reply && reply.html) $dotapp("#cartInner").html(reply.html); if (reply && reply.message) $dotapp().exNotify({ title: "Cart", text: reply.message }); }); }); $dotapp().live() calls the handler as (element, event). The first argument is the matched node, not the browser event — read data-item from that button. Remove confirms in a modal. Checkout uses $dotapp().form("#checkoutForm") with a blocked-form guard and loaders. After success the cart HTML is replaced and the form fields are cleared — no location.reload(). Page script: /app/modules/Examples/assets/js/store.js. Widgets: ex-ui.js. Live demo /documentation/examples/run/store — search “mug”, add items, checkout. Cart survives a refresh because it is stored in DSM. --- # CMS Studio URL: https://dotapp.dev/documentation/examples/studio Examples What you are building Module map Live URLs Files on disk Scaffold Routes Renderer Schema Public site Administration dotapp.js Checklist Live demo CMS Studio A complete CMS module: public front end, administration desk, templates, schema, AJAX lists, and dotapp.js. Live studio: /documentation/examples/run/studio. Locked desk: /documentation/examples/run/studio/admin. What you are building A CMS is not a contact form. It is two products that share one module: a public website people read, and a desk editors use to change that website. This walkthrough builds both inside app/modules/Studio. The live example is a Bratislava software house called Lumen Press — home, services, insights, about, contact, plus an administration shell with articles, pages, menu, media, and settings. This public demo cannot sign in The administration login is a real fo-rm. PHP validates the payload, then always rejects it. Auth::login() is never called. Every save, delete, reorder, and settings POST is stopped by DeskGate@write. Nobody can insert content into the public demo. The desk pages are a read-only preview so you can still see the UI. Use this page as the recipe for a real CMS on your own project. Swap the in-memory catalog for studio_* tables, put the desk behind Auth::isLogged(), and keep the same templates. Titles and JSON-LD Every live Studio page starts its title with Example: and ships JSON-LD as TechArticle + LearningResource. There is no NewsArticle or Product schema. Users module — real login, register, 2FA, AuthGate Secure forms — fo-rm, formName, CRC AJAX lists — paginate(), encrypted ids, search DotApper CLI — scaffold the module, never hand-create the skeleton Module map One module owns the whole CMS. Do not split “frontend app” and “admin app” into two modules unless they are genuinely separate products. PiecePathRole Routesmodule.init.phpPublic URLs + /admin/*. Write routes use ->before('#Studio:DeskGate@write!'). Public siteControllers/Site.phpHome, services, insights, article, about, contact. DeskControllers/Admin.phpLogin (always fails here), dashboard, articles, pages, menu, media, settings. GateMiddleware/DeskGate.phpDemo: reject every write. Production: require Auth::isLogged() + Auth::can(). CatalogLibraries/Press.phpDemo content in PHP. Production: DB::module('RAW') on studio_*. SchemaInstallation.phpVersioned tables: articles, pages, topics, menus, media, settings. Templatesviews/*.view.phpFull HTML documents + fragments for AJAX lists. No Blade, no Twig, no include. JSassets/js/studio.js, admin.js$dotapp().form and $dotapp().load. Not jQuery. Live URLs URLWhat you see /documentation/examples/run/studioPublic studio home /documentation/examples/run/studio/article/{slug}One insight /topicsServices /insightsInsight index /adminLogin that always fails /admin/deskRead-only desk preview /admin/articlesPaginated AJAX list (search “cloud”) Prefix is Config::module('Studio', 'prefix'), default /documentation/examples/run/studio. On your site use / for the public site and /admin for the desk. Files on disk After DotApper, fill these paths. The live demo reads articles, pages, topics, and the nav from Libraries/Press.php. Installation.php is the production schema — it is listed in full later on this page. It is not executed on the public demo. app/modules/Studio/ module.init.php routes Installation.php studio_* tables (full file below) Libraries/View.php Renderer helper Libraries/Press.php demo catalog + menu() Libraries/Mark.php Example SEO Controllers/Site.php public site Controllers/Admin.php desk Middleware/DeskGate.php write lock views/site.view.php public chrome (nav loops $menu) views/site-*.view.php public fragments views/admin.view.php desk chrome views/admin-*.view.php desk fragments assets/css/studio.css assets/js/studio.js assets/js/admin.js assets/img/ logo, hero, practices, team Scaffold with DotApper Never hand-create the module skeleton. Generate it, then fill in routes and classes. php dotapper.php --create-module=Studio php dotapper.php --module=Studio --create-controller=Site php dotapper.php --module=Studio --create-controller=Admin php dotapper.php --module=Studio --create-middleware=DeskGate --module= must appear before --create-controller / --create-middleware. That creates app/modules/Studio/ with Controllers/, Middleware/, Libraries/, views/, assets/, and module.init.php. DotApper also drops placeholder files you can ignore or replace: Api/Api.php, Controllers/Controller.php, views/clean.view.php, views/layouts/example.layout.php, module.listeners.php. What you then write by hand (this walkthrough): FileYou write module.init.phpPrefix + every public and desk route Installation.phpAll studio_* tables (full file in the Schema section) Libraries/View.phpRenderer helper: document vs fragment Libraries/Press.phpDemo catalog (production: DB queries) Libraries/Mark.phpExample titles + TechArticle JSON-LD Controllers/Site.phpPublic site Controllers/Admin.phpDesk (login always fails here) Middleware/DeskGate.phpReject every write on the public demo views/*.view.phpOne chrome document + inner fragments assets/css/studio.css, assets/js/*.jsLook and $dotapp behaviour Routes: the complete module.init.php Static controllers: 'Studio:Site@home!'. Trailing ! is required. Pair each path with and without a trailing slash. Dynamic article slugs are not STATIC_ROUTE. Register exact admin paths before /admin/articles/{slug:s} so /admin/articles is not swallowed. Write POSTs attach ->before('#Studio:DeskGate@write!'). There is no Laravel Route::group(). File: app/modules/Studio/module.init.php — copy this whole file Schema: the complete Installation.php A CMS is not “one articles table”. The public site, the desk, the menu, media, and settings each need a table the module owns. Every table is studio_*. Never unprefixed names, never dotapp_* for studio data. There is no working DB::migrate(). You write versioned SQL in Installation.php, which extends Installer. The public demo does not run this installer. Live pages read PHP arrays from Libraries/Press.php so a visitor cannot INSERT a row. On your own project you copy this file, then call Installation::module('Studio')->install() from initialize() once a database is configured. What each table is for — this is the composition the snippet in older docs hid: TableWho fills itWho reads it studio_topicsDesk → Topics (or a seed)Public topic index, article topic_id studio_articlesDesk → Articles editorHome, article URL, topic listing studio_pagesDesk → Pages (About, masthead, legal)/about and other static documents studio_menusDesk → Menu (one row per nav, code = primary)Join to items studio_menu_itemsDesk → Menu rows (label, href, pos)site.view.php loops this into