seedproject-web/api/app/Helpers/helpers.md
Carlos Arias 1559ce017d chore: scaffold SeedProject base (Phase 1)
Clean-room copy of the reusable engines from comiida, with all
instance data, secrets, dependencies, and build output excluded:
- app/         Astro theme skeleton (no comiida blog posts; hero image -> placeholder)
- api/         SeedProject PHP framework (no vendor/.env/config.php)
- content-pipeline/  engine only (scripts/admin/prompts; empty runtime state)
- astroagent.config.json + app/.astroagent/skills

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SYHWLHihq3v9nxNwoPCKSn
2026-07-04 22:53:10 +00:00

5.7 KiB

Helpers — app/Helpers/

All reusable, stateless utility functions live here as public static methods. Never duplicate logic as private controller methods.

Convention

// Always call as static — no instance needed
HelperClass::methodName($arg);

If no existing class fits, create a new PascalCase.php static class here.


DB.php — Database

All queries use prepared statements. Never interpolate values into SQL.

Methods

Db::select($sql, $params)                        // returns array of rows
Db::getRow($sql, $params)                        // returns single row or null
Db::insert($table, $data)                        // returns last insert ID
Db::update($table, $data, $where, $whereParams)  // returns affected row count
Db::delete($table, $where, $whereParams)         // returns affected row count
Db::execute($sql, $params)                       // for custom queries (UPDATE/INSERT/etc.)
Db::beginTransaction()
Db::commit()
Db::rollback()
Db::useConnection('legalwriter')                 // switch DB connection
Db::useConnection('default')                     // switch back

Examples

// SELECT multiple rows
$users = Db::select("SELECT * FROM sp_users WHERE active = :active", [':active' => 1]);

// SELECT single row
$user = Db::getRow("SELECT * FROM sp_users WHERE id = :id", [':id' => $id]);
if (!$user) { die('Not found'); }

// INSERT
$userId = Db::insert('sp_users', [
    'name'       => 'Jane Doe',
    'email'      => 'jane@example.com',
    'created_at' => date('Y-m-d H:i:s')
]);

// UPDATE
Db::update('sp_users',
    ['name' => 'Jane Smith', 'updated_at' => date('Y-m-d H:i:s')],
    'id = :id',
    [':id' => $userId]
);

// DELETE
Db::delete('sp_users', 'id = :id', [':id' => $userId]);

// Soft delete (preferred)
Db::update('sp_users', ['deleted_at' => date('Y-m-d H:i:s')], 'id = :id', [':id' => $userId]);

// Custom query
Db::execute("UPDATE sp_users SET last_login = NOW() WHERE id = :id", [':id' => $id]);

// Transaction
Db::beginTransaction();
try {
    $orderId = Db::insert('sp_orders', ['user_id' => $userId, 'total' => 100]);
    Db::execute("UPDATE sp_users SET balance = balance - 100 WHERE id = :id", [':id' => $userId]);
    Db::commit();
} catch (Exception $e) {
    Db::rollback();
    throw $e;
}

Complex Query Patterns

// Aggregation
$stats = Db::getRow("SELECT COUNT(*) as total, SUM(amount) as revenue FROM sp_orders WHERE status = 'completed'");
echo $stats['total'];

// Subquery
$users = Db::select("
    SELECT u.*,
           (SELECT COUNT(*) FROM sp_orders WHERE user_id = u.id) as order_count
    FROM sp_users u WHERE u.active = 1
");

// Dynamic filter building
$query  = "SELECT * FROM sp_clients WHERE 1=1";
$params = [];
if (!empty($_GET['q'])) {
    $query .= " AND (name LIKE :q OR email LIKE :q)";
    $params[':q'] = '%' . $_GET['q'] . '%';
}
if (!empty($_GET['status'])) {
    $query .= " AND status = :status";
    $params[':status'] = $_GET['status'];
}
$clients = Db::select($query, $params);

// Pagination
$page    = max(1, (int)($_GET['page'] ?? 1));
$perPage = 20;
$offset  = ($page - 1) * $perPage;
$total   = Db::getRow("SELECT COUNT(*) as count FROM sp_clients");
$rows    = Db::select("SELECT * FROM sp_clients LIMIT :limit OFFSET :offset",
    [':limit' => $perPage, ':offset' => $offset]
);

// Use EXISTS instead of COUNT for existence checks (faster)
$exists = Db::getRow("SELECT 1 FROM sp_users WHERE email = :email LIMIT 1", [':email' => $email]);
if ($exists) { ... }

Auth.php — Authentication

Auth::handleLogin()                     // redirect to /login if not authenticated
Auth::can('users.create')              // returns bool — check permission
Auth::requirePermission('users.delete') // halts with 403 JSON if denied
Auth::isLoggedIn()                      // returns bool
Auth::getUserId()                       // returns current user ID from session
Auth::getUser()                         // returns current user row
Auth::login($userId)                    // set session login state
Auth::logout()                          // destroy session

Session.php — Session Management

Session::start()
Session::set($key, $value)
Session::get($key, $default = null)
Session::has($key)
Session::delete($key)
Session::destroy()
Session::flash($key, $value)    // set one-time flash message
Session::getFlash($key)         // get and delete flash message

Validator.php — Input Validation

Validator::required($value)           // not empty
Validator::email($value)              // valid email format
Validator::minLength($value, $min)
Validator::maxLength($value, $max)
Validator::numeric($value)
Validator::alpha($value)
Validator::alphanumeric($value)
Validator::url($value)
Validator::date($value)

Functions.php — General Purpose

Key reusable helpers:

// Decrypt tokenized org slug → fetch active org row (dies on invalid)
Functions::resolveOrg($slug)

// Decrypt tokenized user slug → fetch user+role row (dies on invalid)
Functions::resolveUser($slug)

// AES-256-CBC URL-safe encrypt / decrypt
Functions::encryptData($data, $key)
Functions::decryptData($token, $key)

Hash.php — Cryptography

Crypto utilities (hashing, token generation, etc.).


Adding New Helpers

  1. Find the most semantically appropriate existing class.
  2. Add a public static function to it.
  3. If nothing fits, create app/Helpers/NewHelper.php as a static class.
  4. Call from any controller or other helper — no instantiation needed.