DotApp Recommended Practices
This section guides you through the best practices for developing with the DotApp PHP Framework
. Learn how to create portable, shareable modules that adapt seamlessly to any session or database driver, ensuring compatibility across diverse server environments.
Recommended Practices
This section outlines the recommended practices for developing modules and applications with the DotApp PHP Framework
. By adhering to these practices, you ensure that your modules are portable, shareable across applications, and adaptable to any server configuration, including different session drivers (e.g., Redis, file-based, database) and database drivers (PDO, MySQLi). Following the framework's philosophy guarantees consistent outputs regardless of the underlying drivers.
Philosophy Overview
The DotApp framework is designed to create portable and maintainable modules that work seamlessly across different environments. By following these practices, your modules will adapt to the user's session driver (e.g., Redis, database) and database driver (PDO, MySQLi) without requiring code changes. This ensures that your application remains flexible and shareable, aligning with DotApp’s core philosophy of modularity and adaptability.
A key aspect of DotApp’s philosophy is its approach to input security. Unlike traditional frameworks that rely on developers to sanitize all inputs, DotApp takes the opposite approach to enhance security and reduce human error. By default, all inputs are automatically protected against common vulnerabilities such as Cross-Site Scripting (XSS) and similar attacks. This design ensures that even if a developer forgets to sanitize a single input, the application remains secure. If you need to access the original, unprotected input—such as when storing HTML code—you can use the DotApp::DotApp()->unprotect($variable)
method. This function accepts a string or an array as a reference, recursively removing protection from the variable or all elements in the array. For example:
use \Dotsystems\App\DotApp;
$variable = $_POST['variable'];
DotApp::DotApp()->unprotect($variable); // $variable now contains the original, unprotected value
Note that unprotect
modifies the variable by reference, so you should call it as DotApp::DotApp()->unprotect($variable)
without reassigning the result (i.e., avoid $variable = DotApp::DotApp()->unprotect($variable)
). This approach reinforces DotApp’s philosophy: developers don’t need to protect variables manually, as they are secure by default, but they have the flexibility to retrieve unprotected values when explicitly required.
Accessing DotApp Instance
The DotApp
instance is the core of the framework, providing access to all its components. To use it globally, import the DotApp
class and access the singleton instance anywhere in your code.
use \Dotsystems\App\DotApp;
$dotApp = DotApp::DotApp();
$dotApp->someMethod(); // Access any DotApp method
In specific contexts, such as within modules, you can access the DotApp instance more conveniently. In module.init.php
and module.listeners.php
files, the instance is available via $this->dotApp
. For example:
// In module.init.php or module.listeners.php
$this->dotApp->someMethod(); // Access DotApp methods directly
In controllers (located in a module’s /Controllers
directory), which are static classes, you can access the DotApp instance using self::dotApp()
. For example:
// In a controller class
public static function someAction($request) {
self::dotApp()->someMethod(); // Access DotApp methods
}
These shortcuts simplify access to the DotApp instance within modules and controllers, while the global DotApp::DotApp()
method remains available everywhere in your application or module. This ensures you can interact with the framework’s features (e.g., router, renderer, database) efficiently from any context.
Using Facades
Facades provide a cleaner, more readable way to interact with DotApp components. While not mandatory, using facades is a recommended practice for writing elegant code.
For example, these two lines achieve the same result:
DotApp::DotApp()->renderer->module(self::moduleName())->setView("dotapper-cli.eng")->setViewVar("variables", $viewVars)->renderView();
Renderer::new()->module(self::moduleName())->setView("dotapper-cli.eng")->setViewVar("variables", $viewVars)->renderView();
The second example uses the Renderer
facade, making the code more concise. Similarly, for custom renderers:
DotApp::DotApp()->renderer->addRenderer("DotAppDoc.code.replace", function($code) { /* logic */ });
Renderer::add("DotAppDoc.code.replace", function($code) { /* logic */ });
Common Facades
Renderer::new()
: Returns a resettable renderer object.Renderer::add()
: Adds a custom renderer.Router::get()
: Defines a GET route, e.g.,Router::get([$this->get_data("base_url")."intro(?:/{language})?"], "DotAppDoc:Page@documentation", Router::DYNAMIC_ROUTE);
.
Using facades improves code readability and aligns with DotApp’s philosophy of clean, maintainable code.
Dependency Injection
Dependency Injection (DI) is a recommended practice in DotApp to manage dependencies cleanly. The framework predefines several bindings for common classes:
$this->bind(Response::class, function() { return new Response($this->request); });
$this->bind(Renderer::class, function() { return new Renderer($this); });
$this->singleton(RouterObj::class, function() { return $this->router; });
$this->singleton(RequestObj::class, function() { return $this->request; });
$this->singleton(Auth::class, function() { return $this->request->auth; });
$this->singleton(Logger::class, function() { return $this->logger; });
For example, in a controller, you can inject the Renderer
class to access it directly:
public static function documentation($request, Renderer $renderer) {
$jazyk = $request->matchData()['language'] ?? "";
$viewVars = array();
$viewVars['last_update'] = "2025-05-04";
// Use $renderer directly
}
Using DI reduces manual instantiation and makes your code more modular and testable.
Database Practices
To ensure your modules are portable and driver-agnostic, DotApp’s philosophy requires using the DB::module()
facade for database access. This facade uses configuration settings to automatically select the configured driver and database, ensuring consistency across the application.
Using DB::module()
Use DB::module("ORM")
or DB::module("RAW")
for database queries:
DB::module("RAW")->q(function ($qb) use ($token) {
$qb
->select('user_id', Config::get("db","prefix").'users_rmtokens')
->where('token', '=', $token);
})->execute();
Alternatively, you can use the longer but more explicit approach:
DotApp::DotApp()->DB->driver(Config::db('driver'))->return("RAW")->selectDb(Config::db('maindb'))->q(function ($qb) use ($token) {
$qb
->select('user_id', Config::get("db","prefix").'users_rmtokens')
->where('token', '=', $token);
})->execute();
Using Callbacks for Driver-Agnostic Code
To ensure portability, always use success
and error
callbacks in the execute
method, especially for RAW queries. This avoids driver-specific handling of results (e.g., MySQLi vs. PDO objects). The execute
method accepts two optional callbacks:
- Success Callback:
function($result, $db, $debug)
- Handles the query results. The$result
parameter is always an array in RAW mode, ensuring consistency across drivers. - Error Callback:
function($error, $db, $debug)
- Handles any errors that occur during query execution, preventing uncaught exceptions.
Incorrect Example (Driver-Specific):
$returned = DotApp::DotApp()->DB->driver('mysqli')->return("RAW")->selectDb(Config::db('maindb'))->q(function ($qb) use ($token) {
$qb
->select('user_id', Config::get("db","prefix").'users_rmtokens')
->where('token', '=', $token);
})->execute();
while ($row = $returned->fetch_assoc()) {
// Process row, e.g., $row['user_id']
}
This code fails if the user switches to PDO, as fetch_assoc()
is MySQLi-specific. Instead, use callbacks to handle results consistently:
DB::module("RAW")->q(function ($qb) use ($token) {
$qb
->select('user_id', Config::get("db","prefix").'users_rmtokens')
->where('token', '=', $token);
})->execute(
function ($result, $db, $debug) use (&$data) {
if ($result === null || $result === []) {
$data = [];
setcookie('dotapp_'.Config::get("app","name_hash"), "", [
'expires' => time() - 3600,
'path' => Config::session("path"),
]);
} else {
$db->q(function ($qb) use (&$data, $result) {
$qb
->select(['username', 'password'], Config::get("db","prefix").'users')
->where('id', '=', $result['user_id']);
})->execute(function ($result, $db, $debug) use (&$data) {
$data['username'] = $result[0]['username'];
$data['passwordHash'] = $result[0]['password'];
$data['stage'] = 0;
$this->login($data, true, true);
}, function ($error, $db, $debug) {
// Handle error, e.g., log or display error message
$data['error'] = $error->getMessage();
});
}
},
function ($error, $db, $debug) {
// Handle initial query error
error_log("Database error: " . $error->getMessage());
}
);
In this example:
- The
success
callback processes the$result
array, which is driver-agnostic (e.g.,$result[0]['user_id']
). - The nested query uses another
execute
with its ownsuccess
anderror
callbacks to handle results or errors. - The
error
callback logs or handles any database errors, preventing uncaught exceptions.
If callbacks lead to complex code (callback hell), you can store results in a variable to simplify logic:
$dbreturn = null;
DB::module("RAW")->q(function ($qb) use ($token) {
$qb
->select('user_id', Config::get("db","prefix").'users_rmtokens')
->where('token', '=', $token);
})->execute(
function ($result, $db, $debug) use (&$dbreturn) {
$dbreturn = $result;
},
function ($error, $db, $debug) {
error_log("Database error: " . $error->getMessage());
}
);
// Continue logic with $dbreturn
Important: Avoid returning raw driver objects (e.g., $returnDB = DB::module("RAW")->q(...)->execute()
), as they are driver-specific (MySQLi or PDO). Using callbacks ensures your module works with any driver, aligning with DotApp’s philosophy.
Session Management with DSM
The DotApp Session Manager (DSM) is a required component for session handling, replacing raw $_SESSION
usage. DSM abstracts the underlying session driver (e.g., default, file, database, Redis), ensuring your application or module remains portable across different environments.
Using DSM
Import and use DSM as follows:
use \Dotsystems\App\Parts\DSM;
$dsm = new DSM("MyModuleStorage");
$dsm->load();
$dsm->set('variable1', "hello");
Alternatively, use the DSM facade for cleaner code (recommended):
DSM::use("MyModuleStorage")->set('variable1', "hello");
echo DSM::use("MyModuleStorage")->get('variable1'); // Outputs: hello
Each module should create its own storage (e.g., MyModuleStorage
) to avoid conflicts with other modules. Variables in different storages can share the same name without collisions.
Key DSM Methods
set($name, $value)
: Sets a session variable.get($name)
: Retrieves a session variable.delete($name)
: Removes a session variable.clear()
: Clears all variables in the storage.start()
: Automatically called in the constructor.destroy()
: Destroys the storage (optional).session_id()
: Returns the session ID.load()
: Loads the session (not needed with facade).save()
: Saves the session (automatic on destruction).
The most commonly used methods are:
DSM::use("MyModuleStorage")->set('variable1', "hello");
DSM::use("MyModuleStorage")->get('variable1');
DSM::use("MyModuleStorage")->delete('variable1');
DSM::use("MyModuleStorage")->clear();
Why DSM? Using DSM instead of $_SESSION
ensures your module is independent of the session driver. The facade approach eliminates the need for manual load()
calls, making code cleaner and more maintainable.
See Examples
To see practical examples of these recommended practices, including database queries with DB::module()
and session management with DSM, visit the Examples section. These examples demonstrate how to apply these practices in real-world scenarios.