Zum Inhalt springen

AI blog · DotApp PHP Framework 2.0

How to use the database in DotApp PHP Framework

New Shop code talks to SQL through DB::module('RAW'), a query callback, and a terminal method. There is no DB::table() and no DB::get(). Tables this module owns are named shop_items — never items, never dotapp_* for module data. This article is the copy-paste contract: all(), guarded singles, execute($ok, $err), paginate(), transactions, and Config::addDatabase.

Common mistakes

Wrong Right
DB::table('shop_items')->get() or DB::get() Those methods do not exist. Use DB::module('RAW')->q(...)->all().
->first() on a query that might be empty ->all() then $rows[0] ?? null. Unguarded first() is never safe.
->execute($ok) with no error callback Omitting $err makes a failed query throw. Always pass both callbacks.
Dump shop_items with ->all() into a catalog view paginate() on first ship. The browser pager is AJAX, not ?page=.
Tables named items or dotapp_items Module tables are {lowercase_modulename}_* — here shop_items.
DB::migrate() Declared but not implemented. Versioned DDL belongs in Installation.php.

Canonical query

q() and qb() are aliases. Both return a query object. The terminal call decides the result. Prefer RAW so each row is an associative array.


use Dotsystems\App\Parts\DB;

$rows = DB::module('RAW')
    ->q(function ($qb) use ($limit) {
        $qb->select(['id', 'title', 'price'])
           ->from('shop_items')
           ->where('active', '=', 1)
           ->orderBy('id', 'DESC')
           ->limit($limit);
    })
    ->all();
    

all() on an empty match is []. That is always safe to foreach. There is no ->count() on the chain. Count with select('COUNT(*) as total') plus all(), or read paginate()['total'].

Terminal methods

Method Success Empty / failure
all() Array of assoc rows []
first() One row array Unsafe — undefined index. Do not call it unguarded.
execute($ok, $err) Driver result With $err: returns false. Without $err: throws.
exists() / doesntExist() bool bool
paginate($perPage, $page) Array of ten keys data => []

One row: never unguarded first()


$rows = DB::module('RAW')->q(function ($qb) use ($id) {
    $qb->select('*')->from('shop_items')->where('id', '=', $id)->limit(1);
})->all();

$row = $rows[0] ?? null;
if ($row === null) {
    return Response::json(['status' => 0, 'message' => 'Not found'], 404);
}
    
Existence without the row: ->exists() returns a bool (same for doesntExist()).

Writes: execute($ok, $err)

Insert, update, and delete end with execute(). The success callback receives $execution_data: affected_rows, insert_id, num_rows, result, query, bindings. On a cache hit that array is empty — always use ??. Passing null as the success callback is fine. Passing null as the error callback is not: a failure then throws.


$newId = null;
DB::module('RAW')->q(function ($qb) use ($title) {
    $qb->insert('shop_items', [
        'title' => $title,
        'active' => 1,
        'created_at' => date('Y-m-d H:i:s'),
    ]);
})->execute(
    function ($result, $db, $execution_data) use (&$newId) {
        $newId = $execution_data['insert_id'] ?? $db->inserted_id();
    },
    function ($error, $db, $execution_data) {
        Logger::use()->error('shop_items insert failed', $error);
    }
);
if ($newId === null) {
    return Response::json(['status' => 0, 'message' => 'Save failed'], 500);
}
    

Update uses update('shop_items')->set([...])->where('id', '=', $id). Delete uses delete('shop_items')->where('id', '=', $id). Read $exec['affected_rows'] ?? 0 in the success callback. Zero can mean the row was missing or the values did not change.

Pagination

Keys returned by paginate($perPage = 15, $page = 1): data, current_page, per_page, total, last_page, from, to, has_more_pages, prev_page, next_page.


$page = DB::module('RAW')
    ->q(fn($qb) => $qb->select('*')->from('shop_items')->orderBy('id', 'DESC'))
    ->paginate(20, $currentPage);

foreach ($page['data'] as $row) { /* ... */ }
$last = $page['last_page'];
    

Users, logs, items, orders, messages — any list that can grow — must call paginate() on the first ship. “There are only three rows now” is not an exception. Do not ->all() the table into a view. The pager in the browser is interactive AJAX (type="button" + $dotapp().load()), not <a href="?page=2"> and not a full reload. Walkthrough: How to build AJAX lists with pagination in DotApp PHP Framework.

Transactions


DB::module('RAW')->transaction();
try {
    DB::module('RAW')->q(function ($qb) use ($orderId) {
        $qb->insert('shop_orders', ['id' => $orderId, 'created_at' => date('Y-m-d H:i:s')]);
    })->execute(null, function ($error) {
        Logger::use()->error('order insert', $error);
        throw new \RuntimeException('order insert failed');
    });
    DB::module('RAW')->q(function ($qb) use ($orderId, $itemId) {
        $qb->insert('shop_order_items', ['order_id' => $orderId, 'item_id' => $itemId]);
    })->execute(null, function ($error) {
        Logger::use()->error('order item insert', $error);
        throw new \RuntimeException('order item insert failed');
    });
    DB::module('RAW')->commit();
} catch (\Throwable $e) {
    DB::module('RAW')->rollback();
    Logger::use()->error('order rolled back', ['msg' => $e->getMessage()]);
    return Response::json(['status' => 0, 'message' => 'Could not create order'], 500);
}
    

transaction(), commit(), and rollback() return $this. Callback form: transact($work, $onCommit, $onRollback).

Complete CRUD controller

File: app/modules/Shop/Controllers/Items.php. Browser posts still run crcCheck() when they arrive through load() or <fo-rm>. JSON bodies use Response::json.


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

use Dotsystems\App\Parts\Config;
use Dotsystems\App\Parts\DB;
use Dotsystems\App\Parts\Logger;
use Dotsystems\App\Parts\Response;

class Items extends \Dotsystems\App\Parts\Controller
{
    public static function index($request)
    {
        $pageNum = (int) ($request->query()['page'] ?? $request->data()['page'] ?? 1);
        if ($pageNum < 1) { $pageNum = 1; }
        $perPage = (int) (Config::module('Shop', 'itemsPerPage') ?? 20);
        $page = DB::module('RAW')->q(function ($qb) {
            $qb->select(['id', 'title', 'price', 'active'])
               ->from('shop_items')->orderBy('id', 'DESC');
        })->paginate($perPage, $pageNum);
        return Response::json(['status' => 1, 'page' => $page]);
    }

    public static function show($request)
    {
        $id = (int) ($request->matchData()['id'] ?? 0);
        $rows = DB::module('RAW')->q(function ($qb) use ($id) {
            $qb->select('*')->from('shop_items')->where('id', '=', $id)->limit(1);
        })->all();
        $row = $rows[0] ?? null;
        if ($row === null) {
            return Response::json(['status' => 0, 'message' => 'Not found'], 404);
        }
        return Response::json(['status' => 1, 'item' => $row]);
    }

    public static function save($request)
    {
        if (!$request->crcCheck()) {
            return Response::json(['status' => 0, 'message' => 'Bad request'], 400);
        }
        $title = trim((string) (($request->data(true)['data']['title'] ?? '')));
        if ($title === '') {
            return Response::json(['status' => 0, 'message' => 'Title required'], 422);
        }
        $newId = null;
        DB::module('RAW')->q(function ($qb) use ($title) {
            $qb->insert('shop_items', [
                'title' => $title,
                'active' => 1,
                'created_at' => date('Y-m-d H:i:s'),
            ]);
        })->execute(
            function ($result, $db, $exec) use (&$newId) {
                $newId = $exec['insert_id'] ?? $db->inserted_id();
            },
            function ($error) { Logger::use()->error('insert failed', $error); }
        );
        if ($newId === null) {
            return Response::json(['status' => 0, 'message' => 'Save failed'], 500);
        }
        return Response::json(['status' => 1, 'id' => $newId]);
    }

    public static function update($request)
    {
        if (!$request->crcCheck()) {
            return Response::json(['status' => 0, 'message' => 'Bad request'], 400);
        }
        $id = (int) ($request->matchData()['id'] ?? 0);
        $title = trim((string) (($request->data(true)['data']['title'] ?? '')));
        $affected = 0;
        DB::module('RAW')->q(function ($qb) use ($id, $title) {
            $qb->update('shop_items')->set(['title' => $title])->where('id', '=', $id);
        })->execute(
            function ($result, $db, $exec) use (&$affected) { $affected = $exec['affected_rows'] ?? 0; },
            function ($error) { Logger::use()->error('update failed', $error); }
        );
        return Response::json(['status' => 1, 'affected' => $affected]);
    }

    public static function delete($request)
    {
        if (!$request->crcCheck()) {
            return Response::json(['status' => 0, 'message' => 'Bad request'], 400);
        }
        $id = (int) ($request->matchData()['id'] ?? 0);
        DB::module('RAW')->q(function ($qb) use ($id) {
            $qb->delete('shop_items')->where('id', '=', $id);
        })->execute(
            function ($result, $db, $exec) { /* $exec['affected_rows'] */ },
            function ($error) { Logger::use()->error('delete failed', $error); }
        );
        return Response::json(['status' => 1]);
    }
}
    

Connections in app/config.php


Config::addDatabase('main', '127.0.0.1', 'user', 'pass', 'shopdb', 'UTF8', 'MYSQL', 'pdo');
Config::addDatabase('reporting', '10.0.0.5', 'ro', 'pass', 'reports', 'UTF8', 'MYSQL', 'pdo');
    

Arguments: connection name, host, user, password, database, charset, engine, driver. Config::db keys include prefix (default dotapp_, core auth tables only), driver (pdo), maindb (main), cache (false). A second connection: DB::module('RAW')->selectDb('reporting')->q(...)->all(). Create shop_items with Installation.php, not with DB::migrate().

FAQ

Why not DB::table or DB::get?

Those methods are not on the facade. The documented entry is DB::module('RAW'), then q(), then all() / execute() / paginate() / exists().

Can I use first() after exists()?

You can, but all() plus [0] ?? null is the pattern that never warns on an empty RAW result. Prefer that everywhere.

What if execute has only a success callback?

A SQL error throws \Exception. Always pass the error callback. Log $error['error'] and $error['errno']. Return a structured JSON error to the client — do not leak the exception text.

Why not <a href="?page=2">?

A reload pager is treated as missing. Keep the catalog on the page, overlay the list, POST the page number with $dotapp().load(), and patch rows plus the pager from JSON. Full UI: AJAX lists.

How do I join or run raw SQL?

$qb->from('shop_orders o')->join('dotapp_users u', 'o.user_id', '=', 'u.id') (table, first column, operator, second column). Named bindings or positional ? — never mixed, or QueryBuilder throws. Wrap dynamic SQL in try/catch. Schema changes belong in Installation.php.

See also