Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Clause Helpers

Clause helpers provide safe, whitelist-based handling of user input for SELECT, ORDER BY, GROUP BY, and pagination.

Why Clause Helpers?

User input in ORDER BY clauses can be dangerous:

// DANGEROUS - SQL injection possible!
$order = $_GET['sort'];  // Could be "name; DROP TABLE users;--"
$builder->orderBy($order);

Clause helpers validate against a whitelist:

// SAFE - only whitelisted columns allowed
$byClause = new ByClause(['name', 'email', 'created_at']);
$builder->orderBy($byClause->input($_GET['sort']));

SelectClause

Control which columns can be selected:

use Sqlx\SelectClause;

// Define allowed columns
$select = new SelectClause(['id', 'name', 'email', 'status', 'created_at']);

// User requests specific columns
$userColumns = ['name', 'email', 'invalid_column'];

$users = $driver->builder()
    ->select($select->input($userColumns))  // Only 'name' and 'email' are used
    ->from('users')
    ->queryAll();

Dynamic Column Selection

$select = new SelectClause(['id', 'name', 'email', 'bio', 'avatar']);

// Basic fields only
$basic = $select->input(['id', 'name']);

// Full profile
$full = $select->input(['id', 'name', 'email', 'bio', 'avatar']);

$users = $driver->builder()
    ->select($includeProfile ? $full : $basic)
    ->from('users')
    ->queryAll();

ByClause

For ORDER BY and GROUP BY with whitelisting:

use Sqlx\ByClause;

// Define allowed columns
$orderBy = new ByClause(['name', 'email', 'created_at', 'status']);

// User input: ['name' => 'ASC', 'created_at' => 'DESC']
$userSort = $_GET['sort'] ?? ['created_at' => 'DESC'];

$users = $driver->builder()
    ->select('*')
    ->from('users')
    ->orderBy($orderBy->input($userSort))
    ->queryAll();

With Default Order

$orderBy = new ByClause(['name', 'email', 'created_at']);

// If user doesn't specify, use default
$sort = !empty($_GET['sort'])
    ? $orderBy->input($_GET['sort'])
    : $orderBy->input(['created_at' => 'DESC']);

For GROUP BY

$groupBy = new ByClause(['status', 'role', 'department']);

$stats = $driver->builder()
    ->select(['status', 'COUNT(*) AS count'])
    ->from('users')
    ->groupBy($groupBy->input($_GET['group']))
    ->queryAll();

PaginateClause

Safe pagination with configurable limits:

use Sqlx\PaginateClause;

$paginate = new PaginateClause();
$paginate
    ->perPage(20)      // Default items per page
    ->minPerPage(1)    // Minimum allowed
    ->maxPerPage(100); // Maximum allowed

// User requests page 3 with 50 items
$page = (int) ($_GET['page'] ?? 1);
$perPage = (int) ($_GET['per_page'] ?? 20);

$users = $driver->builder()
    ->select('*')
    ->from('users')
    ->paginate($paginate($page, $perPage))
    ->queryAll();

Pagination Values

The clause calculates LIMIT and OFFSET:

$paginate = new PaginateClause();
$paginate->perPage(10);

$result = $paginate(1, null);  // Page 1: LIMIT 10 OFFSET 0
$result = $paginate(2, null);  // Page 2: LIMIT 10 OFFSET 10
$result = $paginate(3, 25);    // Page 3 with 25/page: LIMIT 25 OFFSET 50

Enforcing Limits

$paginate = new PaginateClause();
$paginate->maxPerPage(50);

// User tries to request 1000 items
$result = $paginate(1, 1000);  // Actually uses LIMIT 50

Combining Helpers

Use all helpers together:

use Sqlx\SelectClause;
use Sqlx\ByClause;
use Sqlx\PaginateClause;

$select = new SelectClause(['id', 'name', 'email', 'status', 'created_at']);
$orderBy = new ByClause(['name', 'email', 'created_at', 'status']);
$paginate = (new PaginateClause())->perPage(20)->maxPerPage(100);

$columns = $_GET['fields'] ?? ['id', 'name', 'email'];
$sort = $_GET['sort'] ?? ['created_at' => 'DESC'];
$page = (int) ($_GET['page'] ?? 1);
$perPage = (int) ($_GET['per_page'] ?? 20);

$users = $driver->builder()
    ->select($select->input($columns))
    ->from('users')
    ->orderBy($orderBy->input($sort))
    ->paginate($paginate($page, $perPage))
    ->queryAll();

API-Style Response

function listUsers(array $params): array
{
    $select = new SelectClause(['id', 'name', 'email', 'status']);
    $orderBy = new ByClause(['name', 'email', 'created_at']);
    $paginate = (new PaginateClause())->perPage(20)->maxPerPage(100);

    $page = (int) ($params['page'] ?? 1);
    $perPage = (int) ($params['per_page'] ?? 20);

    // Get total count
    $total = $driver->queryValue("SELECT COUNT(*) FROM users");

    // Get paginated results
    $users = $driver->builder()
        ->select($select->input($params['fields'] ?? ['id', 'name']))
        ->from('users')
        ->orderBy($orderBy->input($params['sort'] ?? ['id' => 'ASC']))
        ->paginate($paginate($page, $perPage))
        ->queryAll();

    return [
        'data' => $users,
        'meta' => [
            'total' => $total,
            'page' => $page,
            'per_page' => $perPage,
            'total_pages' => ceil($total / $perPage),
        ]
    ];
}

Invalid Input Handling

By default, invalid columns are silently ignored:

$select = new SelectClause(['id', 'name']);
$result = $select->input(['id', 'password', 'secret']);
// Only 'id' is included, 'password' and 'secret' are ignored

If you need stricter validation, check before using:

$allowed = ['id', 'name', 'email'];
$requested = $_GET['fields'];

$invalid = array_diff($requested, $allowed);
if (!empty($invalid)) {
    throw new \InvalidArgumentException(
        'Invalid fields: ' . implode(', ', $invalid)
    );
}