# Helpers — `app/Helpers/`

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

## Convention

```php
// 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

```php
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

```php
// 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

```php
// 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

```php
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

```php
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

```php
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:

```php
// 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.
