# 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