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.

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 own success and error callbacks to handle results or errors.
  • The error callback logs or handles any database errors, preventing uncaught exceptions.

If callbacks lead to complex code (callback hell), you can store results in a variable to simplify logic:


$dbreturn = null;
DB::module("RAW")->q(function ($qb) use ($token) {
    $qb
        ->select('user_id', Config::get("db","prefix").'users_rmtokens')
        ->where('token', '=', $token);
})->execute(
    function ($result, $db, $debug) use (&$dbreturn) {
        $dbreturn = $result;
    },
    function ($error, $db, $debug) {
        error_log("Database error: " . $error->getMessage());
    }
);
// Continue logic with $dbreturn
    

Important: Avoid returning raw driver objects (e.g., $returnDB = DB::module("RAW")->q(...)->execute()), as they are driver-specific (MySQLi or PDO). Using callbacks ensures your module works with any driver, aligning with DotApp’s philosophy.

Session Management with DSM

The DotApp Session Manager (DSM) is a required component for session handling, replacing raw $_SESSION usage. DSM abstracts the underlying session driver (e.g., default, file, database, Redis), ensuring your application or module remains portable across different environments.

Using DSM

Import and use DSM as follows:


use \Dotsystems\App\Parts\DSM;

$dsm = new DSM("MyModuleStorage");
$dsm->load();
$dsm->set('variable1', "hello");
    

Alternatively, use the DSM facade for cleaner code (recommended):


DSM::use("MyModuleStorage")->set('variable1', "hello");
echo DSM::use("MyModuleStorage")->get('variable1'); // Outputs: hello
    

Each module should create its own storage (e.g., MyModuleStorage) to avoid conflicts with other modules. Variables in different storages can share the same name without collisions.

Key DSM Methods

  • set($name, $value): Sets a session variable.
  • get($name): Retrieves a session variable.
  • delete($name): Removes a session variable.
  • clear(): Clears all variables in the storage.
  • start(): Automatically called in the constructor.
  • destroy(): Destroys the storage (optional).
  • session_id(): Returns the session ID.
  • load(): Loads the session (not needed with facade).
  • save(): Saves the session (automatic on destruction).

The most commonly used methods are:


DSM::use("MyModuleStorage")->set('variable1', "hello");
DSM::use("MyModuleStorage")->get('variable1');
DSM::use("MyModuleStorage")->delete('variable1');
DSM::use("MyModuleStorage")->clear();
    

Why DSM? Using DSM instead of $_SESSION ensures your module is independent of the session driver. The facade approach eliminates the need for manual load() calls, making code cleaner and more maintainable.

See Examples

To see practical examples of these recommended practices, including database queries with DB::module() and session management with DSM, visit the Examples section. These examples demonstrate how to apply these practices in real-world scenarios.