Skip to content

AI blog · DotApp PHP Framework 2.0

How to use sessions in DotApp PHP Framework (DSM)

Application state belongs in DSM::use('Shop'). That is the session bag for the Shop module: cart lines, wizard steps, flash flags. Do not call session_start(). Do not read or write $_SESSION from Shop. Cookie flags in app/config.php configure the driver; they do not change this rule. get() returns null when a key is missing — not false. status() returns $this (misleading name: it is not PHP’s session status integer). Drivers: default, file, file2, db, redis, registered in app/config.php. Prefer default or file unless you need shared sessions. Reserved keys belong to the framework. A custom driver must implement twelve methods, not only the six that Config::sessionDriver validates. This article is a complete Shop cart: set, get, delete, save — plus cookie flags and the driver matrix.

Common mistakes

Wrong Right
$_SESSION['cart'] = $items; or session_start(). DSM::use('Shop')->set('cart', $items)->save();
Treat get('cart') miss as false. Miss is null. Use ?? [].
if ($dsm->status()) to see if the session is open. status() returns $this. Do not test it as an int.
Store the cart under _enc_key or _bridge.x. Those names are reserved. Use cart inside DSM::use('Shop').
Switch to Redis with empty redis_password and empty prefix. Redis throws on construct if any required redis_* value is empty. Fill them first.
Call regenerate_id() on the DB driver and assume it is solid. The DB driver has regenerate_id bugs. Prefer default or file.
Register a custom driver with only load/save/get/set/delete/clear. DSM also calls start, destroy, status, regenerate_id, session_id, gc — twelve methods.
Put the public item list in DSM so “it persists”. Shared catalogs go in Cache::use('Shop'). DSM is per visitor.

When to use DSM — and when not

Use DSM for state that belongs to this browser: a cart, a multi-step checkout index, a “just added” flag. Do not use it for data every visitor should share (cache that). Do not use it as a database. Do not use it for secrets that belong in Config::module. Cookie flags (secure, httponly, samesite) belong in app/config.php. They harden the cookie the driver sets. Shop code still never touches $_SESSION.

Must: named bag

Always DSM::use('Shop') (module namespace). A null name is a framework internal bag for ID helpers — not your cart.

Open the Shop bag


use Dotsystems\App\Parts\DSM;

$sess = DSM::use('Shop');
$sess->set('cart', $items);
$cart = $sess->get('cart') ?? [];
$sess->delete('cart');
$sess->save();
    

use($sessname = null) returns a singleton per name and calls load() on first construct. Chain set / save. After delete, still save() if the driver needs an explicit persist (file/db/redis).

API table

Method Args Exact return
DSM::use($sessname = null) Pass 'Shop' DSM singleton
get($name) Key inside the bag Value, or null when missing — not false
set($name, $value) Key + value $this
delete($name) / clear() / gc() Key, or none Driver return (often void / mixed)
load() / save() / start() / destroy() None $this
regenerate_id($deleteOld = false) Whether to drop the old id $this — DB driver is buggy here
session_id() None string
session_id($new) New id Driver result — throws if that ID already exists
status() None $this — not PHP’s session status int

Reserved keys

The framework already stores values in session space. Do not reuse these names inside Shop:

Key / pattern Owner
_enc_key Framework
_bridge.* Bridge store
_router.* Router
_request.auth Request auth
_formCSRF Form CSRF helper
_default_limiter Default limiter

DSM::use('Shop') already namespaces the bag. Still pick plain keys such as cart, checkout.step. Do not start Shop keys with _ plus those prefixes. A lone CSRF token is a narrow guarantee. Pair form posts with crcCheck() and, for Bridge traffic, the Bridge contract — not a raw token check alone. Bridge: How to call PHP from JavaScript with DotBridge. Forms: How to create secure forms in DotApp PHP Framework.

Drivers

default file file2 db redis
Storage Driver-internal PHP session slot for the bag name One file per session File per {id}_{sessname} DB row Redis key
Needs Writable session.file_driver_dir file_driver_dir2 Table session.database_table Redis extension + filled session.redis_*
GC PHP Cron include Cron SQL delete TTL scan
Notes Simplest All names in one file More files, smaller writes regenerate_id bugs Throws if a required redis_* is empty

The default driver’s internals may sit on PHP’s session array. That is the driver’s problem. Shop still uses only DSM::use('Shop'). Register and select in app/config.php before new DotApp():


use Dotsystems\App\Parts\SessionDriverDefault;
use Dotsystems\App\Parts\SessionDriverFile;
use Dotsystems\App\Parts\SessionDriverFile2;
use Dotsystems\App\Parts\SessionDriverDB;
use Dotsystems\App\Parts\SessionDriverRedis;

Config::sessionDriver('default', SessionDriverDefault::driver());
Config::session('driver', 'default');
Config::session('lifetime', 3600);

Config::sessionDriver('file', SessionDriverFile::driver());
Config::session('file_driver_dir', '/app/runtime/SessionDriverFile');

Config::sessionDriver('file2', SessionDriverFile2::driver());
Config::sessionDriver('db', SessionDriverDB::driver());
Config::session('database_table', 'users_sessions');

Config::sessionDriver('redis', SessionDriverRedis::driver());
Config::session('redis_host', '127.0.0.1');
Config::session('redis_port', 6379);
Config::session('redis_timeout', 2);
Config::session('redis_database', 0);
Config::session('redis_prefix', 'session:');
    

Redis construction throws \Exception if any of the required session keys are null or '' (host, port, timeout, database, prefix, plus cookie name / lifetime / path / secure / httponly / samesite). Password may be empty. Fill the rest before you switch Config::session('driver', 'redis'). Config file: How app/config.php works in DotApp PHP Framework.

These flags configure the session driver. They are not an API for $_SESSION.


Config::session('lifetime', 3600);
Config::session('cookie_name', 'dotapp_session');
Config::session('path', '/');
Config::session('secure', true);     // HTTPS
Config::session('httponly', true);
Config::session('samesite', 'Strict');
    

Getter Config::session('lifetime') returns the value or null. Setter returns void. Passing false is a set, not a get (null is the getter).

Custom driver: twelve methods

Config::sessionDriver($name, $driver) only validates that load, save, get, set, delete, clear exist and are callable. Incomplete map → \Exception "Incompatible driver !" or "All driver functions must be callable !". DSM still invokes start, destroy, status, regenerate_id, session_id, gc. Implement all twelve or Shop will fatal when those methods run. Getter Config::sessionDriver($name) returns the array or throws if undefined.


Config::sessionDriver('memory', [
    'load'          => function ($dsm) {},
    'save'          => function ($dsm) {},
    'get'           => function ($name, $dsm) { return null; },
    'set'           => function ($name, $value, $dsm) {},
    'delete'        => function ($name, $dsm) {},
    'clear'         => function ($dsm) {},
    'start'         => function ($dsm) {},
    'destroy'       => function ($dsm) {},
    'status'        => function ($dsm) {},
    'regenerate_id' => function ($deleteOld, $dsm) {},
    'session_id'    => function ($new, $dsm) { return ''; },
    'gc'            => function ($dsm) {},
]);
    

Complete module.init.php


<?php
namespace Dotsystems\App\Modules\Shop;

use Dotsystems\App\Parts\Config;
use Dotsystems\App\Parts\Router;

class Module extends \Dotsystems\App\Parts\Module
{
    public function initialize($dotApp)
    {
        Config::module('Shop', 'prefix') ?? Config::module('Shop', 'prefix', '/shop');

        $p = Config::module('Shop', 'prefix');

        Router::get($p . '/', 'Shop:Cart@index!', Router::STATIC_ROUTE);
        Router::post($p . '/cart/add', 'Shop:Cart@add!', Router::STATIC_ROUTE);
        Router::post($p . '/cart/remove', 'Shop:Cart@remove!', Router::STATIC_ROUTE);
        Router::post($p . '/cart/clear', 'Shop:Cart@clearCart!', Router::STATIC_ROUTE);
    }

    public function initializeRoutes()
    {
        return ['/shop', '/shop/*'];
    }

    public function initializeCondition($routeMatch)
    {
        return $routeMatch;
    }
}

new Module($dotApp);
    

Complete cart: set / get / delete / save

File: app/modules/Shop/Controllers/Cart.php. JSON posts: Router + Response::json + $request->crcCheck(). After a passing check, read fields from $request->data(true)['data'] (unprotected payload). If you also use Input groups, call form() with an error callback — a mismatch returns false / null or throws.


<?php
namespace Dotsystems\App\Modules\Shop\Controllers;

use Dotsystems\App\Parts\Config;
use Dotsystems\App\Parts\DSM;
use Dotsystems\App\Parts\Logger;
use Dotsystems\App\Parts\Renderer;
use Dotsystems\App\Parts\Response;

class Cart extends \Dotsystems\App\Parts\Controller
{
    public static function index($request)
    {
        $cart = DSM::use('Shop')->get('cart') ?? [];

        $html = Renderer::new()
            ->module('Shop')
            ->setView('cart')
            ->setViewVar('title', 'Cart')
            ->setViewVar('cart', $cart)
            ->setViewVar('prefix', Config::module('Shop', 'prefix'))
            ->renderView();

        if ($html === '') {
            Logger::use()->error('Shop cart view produced empty output');
            return new Response(500, 'Template error');
        }
        return $html;
    }

    public static function add($request)
    {
        if (!$request->crcCheck()) {
            return Response::json(['status' => 0, 'message' => 'Bad request'], 400);
        }

        $payload = $request->data(true)['data'] ?? [];
        $id = (int) ($payload['item_id'] ?? 0);
        $qty = (int) ($payload['qty'] ?? 1);
        if ($id < 1 || $qty < 1) {
            return Response::json(['status' => 0, 'message' => 'Invalid item'], 400);
        }

        $sess = DSM::use('Shop');
        $cart = $sess->get('cart') ?? [];
        $cart[$id] = ($cart[$id] ?? 0) + $qty;
        $sess->set('cart', $cart);
        $sess->save();

        return Response::json(['status' => 1, 'cart' => $cart]);
    }

    public static function remove($request)
    {
        if (!$request->crcCheck()) {
            return Response::json(['status' => 0, 'message' => 'Bad request'], 400);
        }

        $payload = $request->data(true)['data'] ?? [];
        $id = (int) ($payload['item_id'] ?? 0);
        $sess = DSM::use('Shop');
        $cart = $sess->get('cart') ?? [];
        unset($cart[$id]);

        if ($cart === []) {
            $sess->delete('cart');
        } else {
            $sess->set('cart', $cart);
        }
        $sess->save();

        return Response::json(['status' => 1, 'cart' => $cart]);
    }

    public static function clearCart($request)
    {
        if (!$request->crcCheck()) {
            return Response::json(['status' => 0, 'message' => 'Bad request'], 400);
        }

        $sess = DSM::use('Shop');
        $sess->delete('cart');
        $sess->save();
        return Response::json(['status' => 1, 'cart' => []]);
    }
}
    

File: app/modules/Shop/views/cart.view.php.


<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8" />
  <title>{{ var: $title }}</title>
</head>
<body>
  <h1>{{ var: $title }}</h1>
  <ul>
    {{ foreach $cart as $id => $qty }}
      <li>Item {{ var: $id }} × {{ var: $qty }}</li>
    {{ /foreach }}
  </ul>
  <script src="/assets/dotapp/dotapp.js"></script>
</body>
</html>
    

Gotchas

  • status() returns $this. Never compare it to PHP_SESSION_ACTIVE.
  • session_id($new) throws if that ID already exists.
  • DB driver: bugs in regenerate_id. Do not pick db only to “be safer” on login rotation.
  • Redis driver: throws in the constructor when a required redis_* (or cookie flag used as required) is empty. It also uses KEYS scans — fine for small installs, costly at scale.
  • Do not confuse DSM with Cache::use('Shop'). Cache misses are null too, but the lifetime and audience are different.
  • Config::session('secure', true) only affects the cookie. HTTPS still has to be real on the host.

FAQ

Why is $_SESSION forbidden if the default driver uses it internally?

The driver owns that mapping. Shop must go through DSM::use('Shop') so reserved keys, namespacing, and driver swaps stay consistent.

What does get() return for a missing cart?

null. Write DSM::use('Shop')->get('cart') ?? [].

How do I read PHP session status?

You do not, from Shop. status() is chainable and returns $this. It is not the PHP status int.

I set redis_host and still get an exception.

Every required Redis session key must be non-empty, including prefix, port, timeout, database, and the cookie flags the driver validates. Empty string counts as missing.

Is regenerate_id safe on the DB driver?

No. That driver has known bugs there. Prefer default or file unless you need shared sessions and accept the gap.

Config::sessionDriver accepted my six callables. Why does start() crash?

Validation only checks six keys. DSM calls twelve. Add the rest.

Can I cache the cart to speed it up?

No. The cart is visitor state. Cache::use('Shop') is for rebuildable shared (or explicitly contextual) data. See How to use cache in DotApp PHP Framework.

Is storing a token in DSM enough for POST safety?

A lone CSRF token is a narrow guarantee. Use crcCheck() on JSON posts, and formName + Bridge where those stacks apply. Do not invent a parallel token protocol.

See also