API Reference
On this page
- What you will learn
- How it works
- Helpers
Pionia\Porm\Core\Porm- Configuration chain (non-executing)
columns(string|array $columns = '*')where(array|string $column, mixed $operatorOrValue = null, mixed $value = null)filter(?array $where = [])join(?array $where = null)useIndex(string $index)using(null|string|array|Connection $connection = 'default')- Table-level execution
get(int|array|string|null $where = null, ?string $idField = 'id')getOrThrow(int|array|string|null $where = null, string $message = 'Item not found', ?string $idField = 'id')first(?int $size = 1, ?array $where = [], string $pkField = 'id')all(?callable $callback = null)has(int|array|string|null $where, ?string $pkField = 'id')save(array $data, bool $returnRow = true)saveAll(array $data, ?bool $returning = true)saveOrUpdate(array|Arrayable $data, string $pkField = 'id', bool $nativeUpsert = true)update(array $data, array|int|string $where, ?string $idField = 'id')delete(array|int|string $where, ?string $idField = 'id')deleteById(string|int $id, ?string $idField = 'id')deleteAll(array $where)random(?int $limit = 1, ?array $where = null, ?string $pkField = 'id', string $strategy = 'sample')chunk(int $size, callable $callback, ?array $where = null, string $pkField = 'id')explain(?array $where = null)count(?string $column = null, ?array $where = null)sum(string $column, ?array $where = null)avg(string $column, ?array $where = null)max(string $column, ?array $where = null)min(string $column, ?array $where = null)raw(string $query, ?array $params = [], ?string $using = 'db')inTransaction(callable $callback)lastQuery()getDatabase()lastSaved()info()
Pionia\Porm\Database\Builders\Builderwhere(array|string $column, mixed $operatorOrValue = null, mixed $value = null)orWhere(array|string $column, mixed $operatorOrValue = null, mixed $value = null)whereEquals(string $column, mixed $value)whereIs(string $column, mixed $value)whereNotEqual(string $column, mixed $value)whereStartsWith(string $column, string $value)whereEndsWith(string $column, string $value)whereIncludes(string $column, string $value)whereNotIncludes(string $column, string $value)whereGreaterThan(string $column, mixed $value)whereGreaterThanOrEqual(string $column, mixed $value)whereLessThan(string $column, mixed $value)whereLessThanOrEqual(string $column, mixed $value)whereIn(string $column, array $values)whereNotIn(string $column, array $values)whereNull(string $column)whereNotNull(string $column)whereBetween(string $column, mixed $min, mixed $max)whereNotBetween(string $column, mixed $min, mixed $max)orderBy(string|array $value)group(string|array $group)having(string $column, mixed $value, ?string $needle = null)limit(int $limit)startAt(int $startPoint = 0)match($columns, $keyword, $mode = 'natural')get(array|int|null $where = null)first()all(?callable $callback = null)count(?string $column = null, ?array $where = null)sum(string $column, ?array $where = null)avg(string $column, ?array $where = null)max(string $column, ?array $where = null)min(string $column, ?array $where = null)
Pionia\Porm\Database\Builders\Joininner($table, string|array $on_or_using, ?string $alias = null)innerJoin($table, string|array $on_or_using, ?string $alias = null)left($table, string|array $on_or_using, ?string $alias = null)leftJoin($table, string|array $on_or_using, ?string $alias = null)right($table, string|array $on_or_using, ?string $alias = null)rightJoin($table, string|array $on_or_using, ?string $alias = null)full($table, string|array $on_or_using, ?string $alias = null)fullJoin($table, string|array $on_or_using, ?string $alias = null)filter(?array $where = null)where(array|string $column, mixed $operatorOrValue = null, mixed $value = null)get(array|int|null $where = null)first()all(?callable $callback = null)count(?string $column = '*', ?array $where = null)random(?int $limit = 1, ?array $where = null)orderBy(string|array $value)group(string|array $group)having(string $column, mixed $value, ?string $needle = null)limit(int $limit)startAt(int $startPoint = 0)getJoins()- Join ON helpers (
Pionia\Porm\Database\Builders\JoinOn) JoinOn::map(string $baseColumn, string $joinedColumn)JoinOn::maps(array $pairs)JoinOn::using(string|array $columns)JoinOn::expression(string $sql)JoinOn::columns(string $left, string $right)
Pionia\Porm\Database\Builders\JoinLoaderPionia\Porm\Database\Builders\WhereWhere::builder()and(array $clauses)or(array $clauses)build()where(array|string $column, mixed $operatorOrValue = null, mixed $value = null)orWhere(array|string $column, mixed $operatorOrValue = null, mixed $value = null)whereEquals(string $column, mixed $value)whereIs(string $column, mixed $value)whereNotEqual(string $column, mixed $value)whereStartsWith(string $column, string $value)whereEndsWith(string $column, string $value)whereIncludes(string $column, string $value)whereNotIncludes(string $column, string $value)whereGreaterThan(string $column, mixed $value)whereGreaterThanOrEqual(string $column, mixed $value)whereLessThan(string $column, mixed $value)whereLessThanOrEqual(string $column, mixed $value)whereIn(string $column, array $values)whereNotIn(string $column, array $values)whereNull(string $column)whereNotNull(string $column)whereBetween(string $column, mixed $min, mixed $max)whereNotBetween(string $column, mixed $min, mixed $max)
Pionia\Porm\Database\Aggregation\AggAgg::builder()build()random(string $columnName)sum(string $columnName, string $column)avg(string $columnName, string $column)max(string $columnName, string $column)now(string $columnName)uuid(string $columnName, ?string $uuidString)lt(string $columnName, int $value)lte(string $columnName, int $value)gt(string $columnName, int $value)gte(string $columnName, mixed $value)eq(string $columnName, mixed $value)neq(string $columnName, mixed $value)plus(string $columnName, int $value)minus(string $columnName, int $value)of(string $columnName, int $value)jsonified(string $columnName, array $value)div(string $columnName, int $value)like(string $columnName, string|array $value)notLike(string $columnName, string|array $value)columnsCompare(string $column, string $comparison, string $otherColumn)between($columnName, array $values)notBetween($columnName, array $values)regex($columnName, string $regex)
Pionia\Porm\PaginationCore__construct(?array $req, string $table, ?int $limit = 10, ?int $offset = 0, ?string $db = null, ?string $alias = null)columns(string|array $columns = '*')where(array $where = [])init(callable $callback)paginate(?array $where = null, ?array $joins = null)paginateApproximate(?array $where = null, int $countCacheTtl = 60)
Pionia\Porm\ConnectionManagerPionia\Porm\Driver\ConnectionPionia\Porm\Core\Piql- Exceptions
- Source layout (framework)
- Common mistakes
- What’s next
This reference lists every public Porm method Pionia Shop services use when querying products, projects, and customers. Keep it open while implementing Pionia Shop actions on port 8000 — prose guides explain why; this page is the what.
What you will learn
- Locate helpers,
Porm, Builder, Join, andPaginationCoremethods quickly - See which chain methods are terminal vs composable
- Find related source files under
src/Pionia/Porm/
- Database index — guide map and query modes
- Making queries — CRUD walkthrough with examples
How it works
table() → Porm → filter() → Builder | join() → Join | direct CRUD
↓ ↓
PaginationCore JoinLoader / Agg / WhereNamespaces live under Pionia\Porm\. Global helpers are defined in src/Pionia/Utils/helpers.php.
Helpers
table(string $name, ?string $alias = null, ?string $using = null)
Returns a Porm instance bound to the named table. This is the primary entry point for Pionia Shop queries against products, projects, and customers on the default connection. Pass an alias when you plan to join (for example table('tasks', 't')). Pass $using to target a named connection from settings.ini.
$task = table('products')->get(42);
$project = table('projects', 'p')->columns(['p.id', 'p.name'])->get(1);db(string $name, ?string $alias = null, ?string $using = null)
Alias of table(). Use whichever reads better in a Pionia Shop service — both resolve to the same Porm chain starting at http://127.0.0.1:8000 API handlers.
$members = db('customers')
->filter(['project_id' => 1])
->all();connectionManager()
Returns the process-scoped ConnectionManager that pools PDO handles for FPM and RoadRunner workers. Use it when a Pionia Shop action needs to inspect or register connections outside a single table() chain.
$pdo = connectionManager()->connection('default')->getPdo();
connectionManager()->disconnect(); // worker shutdown only
Pionia\Porm\Core\Porm
Created via table(). Combines table-level CRUD with entry into builder modes.
Configuration chain (non-executing)
columns(string|array $columns = '*')
Sets the SELECT list before any terminal method runs. Supports column aliases (title(task_title)) and type casts (priority[Int]). Call early — after filter() or join() it is locked.
$task = table('products')
->columns(['id', 'title', 'status'])
->get(['status' => 'open']);where(array|string $column, mixed $operatorOrValue = null, mixed $value = null)
Merges Medoo-style array conditions or fluent where(column, operator, value) clauses at the table level before filter() or a terminal read. Conditions accumulate with AND semantics.
$open = table('products')
->where('status', 'open')
->where('project_id', 1)
->all();
$byArray = table('products')->where(['project_id' => 1])->get();filter(?array $where = [])
Enters Builder mode for fluent chaining (orderBy, limit, symbolic operators). Optional initial $where is merged before the builder is returned. Terminal methods on the builder execute SQL.
$rows = table('products')
->filter(['status' => 'open'])
->orderBy('priority')
->limit(20)
->all();join(?array $where = null)
Enters Join mode for multi-table reads. Optional $where is merged first. Returns a Join instance — use inner(), left(), and friends before calling all() or get().
$rows = table('tasks', 't')
->columns(['t.title', 'p.name(project)'])
->join()
->left('projects', ['t.project_id' => 'id'], 'p')
->where('t.status', 'open')
->all();useIndex(string $index)
Hints MySQL to use a named index for the next query on this chain. No-op on SQLite and PostgreSQL. Helpful when EXPLAIN shows a full scan on tasks.status.
$rows = table('products')
->useIndex('idx_tasks_status')
->where('status', 'open')
->all();using(null|string|array|Connection $connection = 'default')
Switches the active connection for this Porm instance. Accepts a connection name, inline config array, or a Connection object. Must be called before filter() or join().
$archive = table('products')
->using('reporting')
->where('status', 'archived')
->count();Table-level execution
get(int|array|string|null $where = null, ?string $idField = 'id')
Fetches a single row. Pass a primary-key scalar, a Medoo-style array, or null to fetch the latest row by $idField descending. Returns null when no row matches.
$task = table('products')->get(7);
$bySlug = table('projects')->get(['slug' => 'pionia-shop-alpha']);
$latest = table('products')->get(); // newest task by id
getOrThrow(int|array|string|null $where = null, string $message = 'Item not found', ?string $idField = 'id')
Same as get() but throws NotFoundException when the row is missing. Use in Pionia Shop actions that must return HTTP 404 for unknown task IDs.
$task = table('products')->getOrThrow(99, 'Task not found');first(?int $size = 1, ?array $where = [], string $pkField = 'id')
Returns the most recent row(s) ordered by $pkField DESC. With $size > 1, returns an array of rows. Useful for “latest open task” widgets on port 8000.
$latest = table('products')->first(1, ['status' => 'open']);
$recentFive = table('products')->first(5, ['project_id' => 1]);all(?callable $callback = null)
Returns every row matching accumulated conditions. Optional callback runs per row without building a full in-memory array first.
$products = table('products')->where('project_id', 1)->all();
table('products')->where('status', 'open')->all(function ($row) {
logger()->info('open task', ['id' => $row->id]);
});has(int|array|string|null $where, ?string $pkField = 'id')
Returns true when at least one row matches. Accepts a scalar PK or a condition array. Throws when no conditions are provided.
$exists = table('products')->has(['id' => 42, 'status' => 'open']);
$memberExists = table('customers')->has('ada@pionia.shop', 'email');save(array $data, bool $returnRow = true)
Inserts one row and returns the saved object (by default re-fetches via get()). Set $returnRow = false to skip the extra SELECT.
$task = table('products')->save([
'project_id' => 1,
'title' => 'Wireframe review',
'status' => 'open',
]);saveAll(array $data, ?bool $returning = true)
Inserts multiple rows inside a transaction. When $returning is true, returns an array of saved objects; when false, returns the raw PDOStatement.
$created = table('customers')->saveAll([
['project_id' => 1, 'email' => 'ada@pionia.shop', 'name' => 'Ada Lovelace'],
['project_id' => 1, 'email' => 'sam@northwind.studio', 'name' => 'Sam Park'],
]);saveOrUpdate(array|Arrayable $data, string $pkField = 'id', bool $nativeUpsert = true)
Inserts when the PK is absent or unknown; updates when the row exists. Uses native UPSERT when the driver supports it and the PK is present in $data.
$task = table('products')->saveOrUpdate([
'id' => 7,
'status' => 'done',
'completed_at' => date('Y-m-d H:i:s'),
]);update(array $data, array|int|string $where, ?string $idField = 'id')
Updates rows matching $where. Scalar $where is treated as a primary-key lookup.
table('products')->update(
['status' => 'in_progress'],
['project_id' => 1, 'status' => 'open']
);
table('products')->update(['assignee' => 'ada@pionia.shop'], 42);delete(array|int|string $where, ?string $idField = 'id')
Deletes rows matching $where. Scalar $where deletes by primary key.
table('products')->delete(['status' => 'draft', 'project_id' => 99]);
table('products')->delete(42);deleteById(string|int $id, ?string $idField = 'id')
Explicit alias for deleting a single row by primary key. Reads clearer than delete($id) in Pionia Shop maintenance actions.
table('products')->deleteById(42);deleteAll(array $where)
Alias of delete() with an array WHERE clause. Deletes every matching row in one statement.
table('products')->deleteAll(['project_id' => 99, 'status' => 'draft']);random(?int $limit = 1, ?array $where = null, ?string $pkField = 'id', string $strategy = 'sample')
Fetches random row(s). Default sample strategy picks random PK values in range; pass native to use ORDER BY RAND().
$spotlight = table('products')->random(1, ['status' => 'open']);
$five = table('customers')->random(5, ['project_id' => 1], 'id', 'native');chunk(int $size, callable $callback, ?array $where = null, string $pkField = 'id')
Processes result sets in pages without loading the full table. Each callback receives up to $size rows. Must be called before filter().
table('products')->chunk(100, function (array $rows) {
foreach ($rows as $task) {
logger()->debug('task', ['id' => $task->id]);
}
}, ['status' => 'open']);explain(?array $where = null)
Returns the database EXPLAIN plan for the current table, columns, and WHERE. Use when a Pionia Shop list endpoint on port 8000 is slow.
$plan = table('products')->explain(['status' => 'open', 'project_id' => 1]);count(?string $column = null, ?array $where = null)
Returns COUNT(*) or COUNT(column) for matching rows. Merges optional $where before counting.
$openCount = table('products')->count('*', ['status' => 'open']);
$perProject = table('products')->count('id', ['project_id' => 1]);sum(string $column, ?array $where = null)
Returns the numeric sum of $column as a string (driver-dependent formatting). Merge $where to scope aggregates.
$totalEstimate = table('products')->sum('estimate_hours', ['project_id' => 1]);avg(string $column, ?array $where = null)
Returns the average of $column for matching rows.
$avgPriority = table('products')->avg('priority', ['status' => 'open']);max(string $column, ?array $where = null)
Returns the maximum value of $column.
$latestSort = table('products')->max('sort_order', ['project_id' => 1]);min(string $column, ?array $where = null)
Returns the minimum value of $column.
$earliest = table('products')->min('created_at', ['project_id' => 1]);raw(string $query, ?array $params = [], ?string $using = 'db')
Executes a parameterized SQL string and returns one object (single row) or an array of rows. Prefer table() chains for routine Pionia Shop CRUD; reserve raw() for reporting queries.
$stats = table('products')->raw(
'SELECT status, COUNT(*) AS cnt FROM tasks WHERE project_id = :pid GROUP BY status',
['pid' => 1]
);inTransaction(callable $callback)
Wraps $callback in a database transaction. Assign results from inside the closure with use (&$var) to read them after commit.
$row = null;
table('products')->inTransaction(function ($porm) use (&$row) {
$row = $porm->save(['project_id' => 1, 'title' => 'Deploy', 'status' => 'open']);
table('projects')->update(['updated_at' => date('Y-m-d H:i:s')], 1);
});lastQuery()
Returns the most recently executed SQL with inlined parameters (human-readable). Useful while debugging Moonlight actions against port 8000.
table('products')->where('status', 'open')->all();
$sql = table('products')->lastQuery();getDatabase()
Returns the underlying Piql engine for advanced SQL. Chain lastPrepared() on the result for placeholder maps.
$piql = table('products')->getDatabase();
$prepared = $piql->lastPrepared(); // ['statement' => '...', 'map' => [...]]
lastSaved()
Returns the auto-increment ID from the last insert on this connection.
table('products')->save(['project_id' => 1, 'title' => 'New', 'status' => 'open']);
$newId = table('products')->lastSaved();info()
Returns metadata about the active connection (driver type, server version, etc.).
$meta = table('products')->info();Pionia\Porm\Database\Builders\Builder
Returned by filter(). Supports fluent WHERE, ordering, limits, and aggregates.
where(array|string $column, mixed $operatorOrValue = null, mixed $value = null)
Adds AND conditions. Pass a Medoo array or fluent (column, value) / (column, operator, value) forms.
table('products')->filter()
->where('status', 'open')
->where('priority', '>', 2)
->all();orWhere(array|string $column, mixed $operatorOrValue = null, mixed $value = null)
Adds OR conditions relative to prior clauses. Use for alternate matches — for example email or name when looking up a customer.
table('products')->filter()
->where('assignee', 'ada@pionia.shop')
->orWhere('assignee', 'sam@northwind.studio')
->all();whereEquals(string $column, mixed $value)
Shorthand for where($column, $value).
table('products')->filter()->whereEquals('status', 'open')->count();whereIs(string $column, mixed $value)
Explicit equality with the is operator alias.
table('customers')->filter()->whereIs('email', 'ada@pionia.shop')->first();whereNotEqual(string $column, mixed $value)
column != value.
table('products')->filter()->whereNotEqual('status', 'archived')->all();whereStartsWith(string $column, string $value)
LIKE 'value%'.
table('products')->filter()->whereStartsWith('title', 'Desk')->all();whereEndsWith(string $column, string $value)
LIKE '%value'.
table('customers')->filter()->whereEndsWith('email', '.studio')->all();whereIncludes(string $column, string $value)
LIKE '%value%' (contains).
table('products')->filter()->whereIncludes('title', 'wireframe')->all();whereNotIncludes(string $column, string $value)
NOT LIKE '%value%'.
table('products')->filter()->whereNotIncludes('title', 'draft')->all();whereGreaterThan(string $column, mixed $value)
column > value.
table('products')->filter()->whereGreaterThan('priority', 2)->all();whereGreaterThanOrEqual(string $column, mixed $value)
column >= value.
table('products')->filter()->whereGreaterThanOrEqual('sort_order', 10)->all();whereLessThan(string $column, mixed $value)
column < value.
table('products')->filter()->whereLessThan('estimate_hours', 8)->all();whereLessThanOrEqual(string $column, mixed $value)
column <= value.
table('products')->filter()->whereLessThanOrEqual('priority', 3)->all();whereIn(string $column, array $values)
column IN (...).
table('products')->filter()->whereIn('status', ['open', 'in_progress'])->all();whereNotIn(string $column, array $values)
column NOT IN (...).
table('products')->filter()->whereNotIn('project_id', [98, 99])->all();whereNull(string $column)
column IS NULL.
table('products')->filter()->whereNull('completed_at')->all();whereNotNull(string $column)
column IS NOT NULL.
table('products')->filter()->whereNotNull('assignee')->all();whereBetween(string $column, mixed $min, mixed $max)
column BETWEEN min AND max.
table('products')->filter()->whereBetween('priority', 1, 3)->all();whereNotBetween(string $column, mixed $min, mixed $max)
column NOT BETWEEN min AND max.
table('products')->filter()->whereNotBetween('sort_order', 0, 5)->all();orderBy(string|array $value)
Sets ORDER BY. Pass a column name (defaults ASC) or an associative array of column => direction.
table('products')->filter()
->where('project_id', 1)
->orderBy(['priority' => 'DESC', 'sort_order' => 'ASC'])
->all();group(string|array $group)
Sets GROUP BY columns.
table('products')->filter()
->columns(['status', 'count' => 'COUNT(*)'])
->group('status')
->all();having(string $column, mixed $value, ?string $needle = null)
Adds a HAVING clause. Append $needle (>, <, !, >=, <=) for comparisons.
table('products')->filter()
->columns(['project_id', 'total' => 'COUNT(*)'])
->group('project_id')
->having('total', 5, '>')
->all();limit(int $limit)
Sets LIMIT. Required before startAt().
table('products')->filter()->where('status', 'open')->limit(25)->all();startAt(int $startPoint = 0)
Sets OFFSET. Must follow limit().
table('products')->filter()
->where('project_id', 1)
->orderBy('id')
->limit(25)
->startAt(50)
->all();match($columns, $keyword, $mode = 'natural')
Adds a MATCH ... AGAINST full-text condition (MySQL). $mode can be natural or boolean.
table('products')->filter()
->match(['title', 'description'], 'wireframe', 'boolean')
->all();get(array|int|null $where = null)
Executes and returns a single row object. Optional $where merges before execution; integer offset uses LIMIT.
$row = table('products')->filter()->where('status', 'open')->get();first()
Returns the first row of the result set (get(0)).
$next = table('products')->filter()
->where('status', 'open')
->orderBy('sort_order')
->first();all(?callable $callback = null)
Executes and returns all matching rows.
$board = table('products')->filter()
->where('project_id', 1)
->orderBy('sort_order')
->all();count(?string $column = null, ?array $where = null)
Returns the row count for the built query.
$open = table('products')->filter()->where('status', 'open')->count();sum(string $column, ?array $where = null)
Aggregate sum on the filtered set.
$hours = table('products')->filter()->where('project_id', 1)->sum('estimate_hours');avg(string $column, ?array $where = null)
Aggregate average on the filtered set.
$avg = table('products')->filter()->where('status', 'done')->avg('estimate_hours');max(string $column, ?array $where = null)
Aggregate maximum on the filtered set.
$peak = table('products')->filter()->where('project_id', 1)->max('priority');min(string $column, ?array $where = null)
Aggregate minimum on the filtered set.
$lowest = table('products')->filter()->where('project_id', 1)->min('sort_order');Pionia\Porm\Database\Builders\Join
Returned by join(). Combines JoinParseTrait, FilterTrait, and FluentWhereTrait.
inner($table, string|array $on_or_using, ?string $alias = null)
Adds an INNER JOIN. $on_or_using is a Medoo map or JoinOn helper.
table('tasks', 't')->join()
->inner('projects', ['t.project_id' => 'id'], 'p')
->all();innerJoin($table, string|array $on_or_using, ?string $alias = null)
Alias of inner().
table('tasks', 't')->join()
->innerJoin('customers', ['t.assignee' => 'email'], 'm')
->all();left($table, string|array $on_or_using, ?string $alias = null)
Adds a LEFT JOIN. Unmatched base rows still appear with NULL joined columns.
table('tasks', 't')->join()
->left('projects', ['t.project_id' => 'id'], 'p')
->where('t.status', 'open')
->all();leftJoin($table, string|array $on_or_using, ?string $alias = null)
Alias of left().
table('projects', 'p')->join()
->leftJoin('tasks', ['p.id' => 'project_id'], 't')
->all();right($table, string|array $on_or_using, ?string $alias = null)
Adds a RIGHT JOIN.
table('customers', 'm')->join()
->right('projects', ['m.project_id' => 'id'], 'p')
->all();rightJoin($table, string|array $on_or_using, ?string $alias = null)
Alias of right().
table('tasks', 't')->join()
->rightJoin('projects', ['t.project_id' => 'id'], 'p')
->all();full($table, string|array $on_or_using, ?string $alias = null)
Adds a FULL OUTER JOIN where the driver supports it.
table('tasks', 't')->join()
->full('projects', ['t.project_id' => 'id'], 'p')
->all();fullJoin($table, string|array $on_or_using, ?string $alias = null)
Alias of full().
table('tasks', 't')->join()
->fullJoin('projects', ['t.project_id' => 'id'], 'p')
->all();filter(?array $where = null)
Merges a Medoo-style WHERE array after joins are declared. Sugar for readability when chaining.
table('tasks', 't')->join()
->left('projects', ['t.project_id' => 'id'], 'p')
->filter(['t.status' => 'open'])
->all();where(array|string $column, mixed $operatorOrValue = null, mixed $value = null)
Fluent or array WHERE on joined queries. Same signatures as Builder where(). Join also exposes every Builder where* helper (orWhere, whereIn, whereNull, and the rest) via the same trait.
table('tasks', 't')->join()
->left('projects', ['t.project_id' => 'id'], 'p')
->where('p.client', 'Pionia Shop')
->where('t.status', 'open')
->all();get(array|int|null $where = null)
Fetches one joined row.
$row = table('tasks', 't')->join()
->left('projects', ['t.project_id' => 'id'], 'p')
->where('t.id', 1)
->get();first()
First row of the joined result set.
$row = table('tasks', 't')->join()
->left('projects', ['t.project_id' => 'id'], 'p')
->orderBy('t.id')
->first();all(?callable $callback = null)
All joined rows.
$rows = table('tasks', 't')->join()
->inner('projects', ['t.project_id' => 'id'], 'p')
->where('p.slug', 'pionia-shop-alpha')
->all();count(?string $column = '*', ?array $where = null)
Counts joined rows (respects JOIN type and WHERE).
$n = table('tasks', 't')->join()
->left('projects', ['t.project_id' => 'id'], 'p')
->where('t.status', 'open')
->count();random(?int $limit = 1, ?array $where = null)
Random joined row(s) via native ORDER BY RAND().
$pick = table('tasks', 't')->join()
->left('projects', ['t.project_id' => 'id'], 'p')
->random(1, ['t.status' => 'open']);orderBy(string|array $value)
Order joined results.
table('tasks', 't')->join()
->left('projects', ['t.project_id' => 'id'], 'p')
->orderBy(['t.priority' => 'DESC'])
->all();group(string|array $group)
GROUP BY on joined queries.
table('tasks', 't')->join()
->inner('projects', ['t.project_id' => 'id'], 'p')
->columns(['p.name', 'total' => 'COUNT(*)'])
->group('p.id')
->all();having(string $column, mixed $value, ?string $needle = null)
HAVING on grouped join queries.
table('tasks', 't')->join()
->inner('projects', ['t.project_id' => 'id'], 'p')
->columns(['p.name', 'cnt' => 'COUNT(*)'])
->group('p.id')
->having('cnt', 3, '>')
->all();limit(int $limit)
LIMIT on joined queries.
table('tasks', 't')->join()
->left('projects', ['t.project_id' => 'id'], 'p')
->limit(10)
->all();startAt(int $startPoint = 0)
OFFSET on joined queries (requires prior limit()).
table('tasks', 't')->join()
->left('projects', ['t.project_id' => 'id'], 'p')
->limit(20)
->startAt(40)
->all();getJoins()
Returns the internal Medoo-style join array for debugging.
$join = table('tasks', 't')->join()
->left('projects', ['t.project_id' => 'id'], 'p');
$debug = $join->getJoins();Join ON helpers (Pionia\Porm\Database\Builders\JoinOn)
Full guide: Relationships & joins.
JoinOn::map(string $baseColumn, string $joinedColumn)
Single equality ON pair: base.{baseColumn} = joined.{joinedColumn}.
table('tasks', 't')->join()
->left('projects', JoinOn::map('project_id', 'id'), 'p')
->all();JoinOn::maps(array $pairs)
Multiple AND equalities.
table('tasks', 't')->join()
->inner('customers', JoinOn::maps(['assignee' => 'email', 'project_id' => 'project_id']), 'm')
->all();JoinOn::using(string|array $columns)
USING (column) form for one or more shared column names.
table('products')->join()
->inner('archived_tasks', JoinOn::using('id'))
->all();JoinOn::expression(string $sql)
Raw SQL ON fragment.
table('tasks', 't')->join()
->left('projects', JoinOn::expression('t.project_id = p.id AND p.client = \'Pionia Shop\''), 'p')
->all();JoinOn::columns(string $left, string $right)
Shorthand equality expression string (left = right).
table('tasks', 't')->join()
->inner('projects', JoinOn::columns('t.project_id', 'p.id'), 'p')
->all();Pionia\Porm\Database\Builders\JoinLoader
eager(array $parents, string $parentFk, string $relatedTable, string $relatedPk = 'id', string $attachAs = 'related', null|string|array $connection = null)
Attaches related rows to each parent in one extra WHERE IN query — avoids N+1 when looping tasks and loading projects.
$products = table('products')->where('project_id', 1)->all();
$products = JoinLoader::eager($products, 'project_id', 'projects', 'id', 'project');
// each $task->project is the matching projects row
Pionia\Porm\Database\Builders\Where
Build nested AND/OR arrays for filter()->where($clause). See WHERE DSL.
Where::builder()
Static factory from ContractBuilder. Starts an empty WHERE tree.
$clause = Where::builder()
->where('status', 'open')
->build();and(array $clauses)
Wraps $clauses in an AND group with a unique internal key (safe for nested trees).
$clause = Where::builder()
->and(['project_id' => 1, 'status' => 'open'])
->build();or(array $clauses)
Wraps $clauses in an OR group.
$clause = Where::builder()
->or(['assignee' => 'ada@pionia.shop', 'assignee' => 'sam@northwind.studio'])
->build();build()
Returns the finished Medoo-style WHERE array. Pass to filter()->where($clause) or table-level where($clause).
$clause = Where::builder()->where('status', 'open')->build();
$products = table('products')->filter()->where($clause)->all();where(array|string $column, mixed $operatorOrValue = null, mixed $value = null)
Adds fluent or array conditions inside the WHERE builder (same operators as Builder).
$clause = Where::builder()
->where('priority', '>', 2)
->where(['status' => 'open'])
->build();orWhere(array|string $column, mixed $operatorOrValue = null, mixed $value = null)
Adds OR branches inside the WHERE builder.
$clause = Where::builder()
->where('project_id', 1)
->orWhere('project_id', 2)
->build();whereEquals(string $column, mixed $value)
Equality helper inside Where::builder().
$clause = Where::builder()->whereEquals('status', 'open')->build();whereIs(string $column, mixed $value)
is operator helper.
$clause = Where::builder()->whereIs('status', 'open')->build();whereNotEqual(string $column, mixed $value)
Inequality helper.
$clause = Where::builder()->whereNotEqual('status', 'done')->build();whereStartsWith(string $column, string $value)
Prefix LIKE helper.
$clause = Where::builder()->whereStartsWith('title', 'Desk')->build();whereEndsWith(string $column, string $value)
Suffix LIKE helper.
$clause = Where::builder()->whereEndsWith('email', '.studio')->build();whereIncludes(string $column, string $value)
Contains LIKE helper.
$clause = Where::builder()->whereIncludes('title', 'Flow')->build();whereNotIncludes(string $column, string $value)
Negative contains helper.
$clause = Where::builder()->whereNotIncludes('title', 'draft')->build();whereGreaterThan(string $column, mixed $value)
> helper.
$clause = Where::builder()->whereGreaterThan('priority', 2)->build();whereGreaterThanOrEqual(string $column, mixed $value)
>= helper.
$clause = Where::builder()->whereGreaterThanOrEqual('sort_order', 5)->build();whereLessThan(string $column, mixed $value)
< helper.
$clause = Where::builder()->whereLessThan('estimate_hours', 4)->build();whereLessThanOrEqual(string $column, mixed $value)
<= helper.
$clause = Where::builder()->whereLessThanOrEqual('priority', 3)->build();whereIn(string $column, array $values)
IN helper.
$clause = Where::builder()->whereIn('status', ['open', 'in_progress'])->build();whereNotIn(string $column, array $values)
NOT IN helper.
$clause = Where::builder()->whereNotIn('project_id', [98, 99])->build();whereNull(string $column)
IS NULL helper.
$clause = Where::builder()->whereNull('completed_at')->build();whereNotNull(string $column)
IS NOT NULL helper.
$clause = Where::builder()->whereNotNull('assignee')->build();whereBetween(string $column, mixed $min, mixed $max)
BETWEEN helper.
$clause = Where::builder()->whereBetween('priority', 1, 3)->build();whereNotBetween(string $column, mixed $min, mixed $max)
NOT BETWEEN helper.
$clause = Where::builder()->whereNotBetween('sort_order', 0, 2)->build();Pionia\Porm\Database\Aggregation\Agg
Expression builder for WHERE, HAVING, and column lists. See Aggregation.
Agg::builder()
Starts an empty aggregate / expression map.
$expr = Agg::builder()->gt('priority', 2)->build();build()
Returns the Medoo-style expression array.
table('products')->filter()->where(Agg::builder()->eq('status', 'open')->build())->all();random(string $columnName)
Select RAND() aliased as $columnName.
table('products')->columns(Agg::builder()->random('pick')->build())->limit(1)->all();sum(string $columnName, string $column)
SUM(column) with result alias.
table('products')->columns(Agg::builder()->sum('total_hours', 'estimate_hours')->build())
->where('project_id', 1)->get();avg(string $columnName, string $column)
AVG(column) with alias.
table('products')->columns(Agg::builder()->avg('avg_pri', 'priority')->build())->get();max(string $columnName, string $column)
MAX(column) with alias.
table('products')->columns(Agg::builder()->max('max_sort', 'sort_order')->build())->get();now(string $columnName)
NOW() timestamp expression.
table('products')->save(Agg::builder()->now('updated_at')->eq('status', 'open')->build());uuid(string $columnName, ?string $uuidString)
UUID() or a fixed UUID string for inserts/updates.
table('projects')->save(Agg::builder()->uuid('public_id', null)->build());lt(string $columnName, int $value)
column[<]value condition.
table('products')->filter()->where(Agg::builder()->lt('priority', 4)->build())->all();lte(string $columnName, int $value)
column[<=]value condition.
table('products')->filter()->where(Agg::builder()->lte('sort_order', 10)->build())->all();gt(string $columnName, int $value)
column[>]value condition.
table('products')->filter()->where(Agg::builder()->gt('priority', 1)->build())->all();gte(string $columnName, mixed $value)
column[>=]value condition.
table('products')->filter()->where(Agg::builder()->gte('estimate_hours', 2)->build())->all();eq(string $columnName, mixed $value)
Equality expression entry.
table('products')->filter()->where(Agg::builder()->eq('status', 'open')->build())->all();neq(string $columnName, mixed $value)
column[!]value inequality.
table('products')->filter()->where(Agg::builder()->neq('status', 'archived')->build())->all();plus(string $columnName, int $value)
Increment column: column[+]value.
table('products')->update(Agg::builder()->plus('sort_order', 1)->build(), 42);minus(string $columnName, int $value)
Decrement column: column[-]value.
table('products')->update(Agg::builder()->minus('estimate_hours', 1)->build(), 42);of(string $columnName, int $value)
Multiply column: column[*]value.
table('products')->update(Agg::builder()->of('estimate_hours', 2)->build(), 42);jsonified(string $columnName, array $value)
JSON-encode a value into a column on write.
table('products')->update(Agg::builder()->jsonified('meta', ['reviewed' => true])->build(), 42);div(string $columnName, int $value)
Divide column: column[/]value.
table('products')->update(Agg::builder()->div('estimate_hours', 2)->build(), 42);like(string $columnName, string|array $value)
column[~]value LIKE condition.
table('products')->filter()->where(Agg::builder()->like('title', '%wireframe%')->build())->all();notLike(string $columnName, string|array $value)
column[!~]value negative LIKE.
table('products')->filter()->where(Agg::builder()->notLike('title', '%draft%')->build())->all();columnsCompare(string $column, string $comparison, string $otherColumn)
Compare two columns (=, >, <, !=). Useful in HAVING or advanced WHERE.
table('tasks', 't')->join()
->inner('projects', ['t.project_id' => 'id'], 'p')
->where(Agg::builder()->columnsCompare('t.updated_at', '>', 'p.created_at')->build())
->all();between($columnName, array $values)
column[<>]values BETWEEN.
table('products')->filter()->where(Agg::builder()->between('priority', [1, 3])->build())->all();notBetween($columnName, array $values)
column[><]values NOT BETWEEN.
table('products')->filter()->where(Agg::builder()->notBetween('sort_order', [0, 5])->build())->all();regex($columnName, string $regex)
column[REGEXP]regex condition (MySQL).
table('customers')->filter()
->where(Agg::builder()->regex('email', '@northwind\\.studio$')->build())
->all();Pionia\Porm\PaginationCore
Moonlight list payloads for Pionia Shop tables. Reads limit / offset from request arrays (PAGINATION, pagination, SEARCH, search, or top-level keys).
__construct(?array $req, string $table, ?int $limit = 10, ?int $offset = 0, ?string $db = null, ?string $alias = null)
Captures pagination parameters from a Moonlight action request body and binds a table name.
$page = new PaginationCore($requestBody, 'tasks', 25, 0);columns(string|array $columns = '*')
Sets SELECT columns for the paginated query.
$page = (new PaginationCore($req, 'tasks'))
->columns(['id', 'title', 'status', 'project_id']);where(array $where = [])
Merges base WHERE conditions applied before init().
$page = (new PaginationCore($req, 'tasks'))
->where(['status' => 'open', 'project_id' => 1]);init(callable $callback)
Builds the underlying Builder or Join via table(). Callback receives a Porm instance and must return Builder or Join.
$payload = (new PaginationCore($req, 'tasks', 20))
->where(['project_id' => 1])
->init(fn ($q) => $q->filter()->orderBy('sort_order'))
->paginate();paginate(?array $where = null, ?array $joins = null)
Executes COUNT + page query and returns results with navigation metadata (has_next, total_count, offsets). Requires init() first.
$result = (new PaginationCore($req, 'tasks'))
->init(fn ($q) => $q->filter()->where('status', 'open'))
->paginate();
// $result['results'], $result['total_count'], $result['has_next'], ...
paginateApproximate(?array $where = null, int $countCacheTtl = 60)
Same as paginate() but caches total_count for $countCacheTtl seconds. Sets approximate_count => true in the payload.
$result = (new PaginationCore($req, 'tasks', 50))
->init(fn ($q) => $q->filter())
->paginateApproximate(null, 120);Pionia\Porm\ConnectionManager
connection(null|string|array $name = 'default')
Returns a pooled Connection. Creates and caches on first use; pings stale handles before reuse.
$conn = connectionManager()->connection('default');register(string $name, Connection $connection)
Injects a pre-built connection into the pool (tests, custom bootstrapping).
connectionManager()->register('testing', Connection::open(['testMode' => true]));disconnect(?string $name = null)
Clears one named pool entry or the entire pool when $name is null. Call on RoadRunner worker shutdown — not per HTTP request.
connectionManager()->disconnect('default');
connectionManager()->disconnect(); // all
has(string $name)
Returns whether a connection is currently cached in the pool.
if (connectionManager()->has('reporting')) {
$pdo = connectionManager()->connection('reporting')->getPdo();
}Pionia\Porm\Driver\Connection
Connection::connect(null|string|array|PDO $connection = 'default')
Resolves a connection through ConnectionManager when the app container is booted; otherwise opens a standalone instance.
$porm = new Porm(Connection::connect('default'));Connection::open(null|string|array|PDO $connection = 'default')
Always creates a new connection without going through the manager pool. Use in tests or one-off scripts.
$conn = Connection::open([
'type' => 'sqlite',
'database' => ':memory:',
'testMode' => false,
]);Pionia\Porm\Core\Piql
Low-level SQL engine (Medoo-compatible). Access via $porm->getDatabase(). Prefer table() for application code; use Piql when extending Porm internals.
Piql::raw(string $string, array $map = [])
Builds a Raw expression with bound parameters. Safe alternative to string concatenation in custom SQL fragments.
$expr = Piql::raw('estimate_hours * :rate', ['rate' => 85]);upsert(string $table, array $data, string $primaryKey = 'id')
Native insert-or-update on conflict for SQLite, MySQL, and PostgreSQL. Returns null when unsupported.
table('products')->getDatabase()->upsert('tasks', [
'id' => 7,
'status' => 'done',
], 'id');lastPrepared()
Returns the last executed statement and placeholder map: ['statement' => '...', 'map' => [...]]. Pair with lastQuery() for debugging Pionia Shop queries on port 8000.
table('products')->where('status', 'open')->all();
$prepared = table('products')->getDatabase()->lastPrepared();Exceptions
| Class | When |
|---|---|
Pionia\Porm\Exceptions\BaseDatabaseException | Query errors |
Pionia\Exceptions\DatabaseException | Wrapper / domain |
Pionia\Exceptions\NotFoundException | getOrThrow() |
Pionia\Exceptions\ValidationException | HTTP 422 — missing/invalid fields in GenericService |
Source layout (framework)
| Path | Role |
|---|---|
src/Pionia/Porm/Core/Porm.php | Facade |
src/Pionia/Porm/Core/Piql.php | SQL engine |
src/Pionia/Porm/Database/Utils/TableLevelQueryTrait.php | CRUD traits |
src/Pionia/Porm/Database/Aggregation/AggregateTrait.php | Aggregates |
src/Pionia/Porm/Database/Utils/FilterTrait.php | ORDER/LIMIT |
src/Pionia/Porm/PaginationCore.php | Pagination |
src/Pionia/Porm/ConnectionManager.php | Pooling |
Prose guides: Database index.
Common mistakes
- Calling non-terminal methods after
all()orcount()— start a freshtable('products')chain for the next operation. - Using Builder methods on a bare
Pormafterjoin()— join chains only expose Join + FilterTrait terminals. - Assuming
saveOrUpdate()targets joined tables — writes always hit the base table fromtable('products'). - Looking up deprecated
Porm\Pormclass names — v3 lives underPionia\Porm\per this reference.
What’s next
Making queries
Worked CRUD examples on tasks.
WHERE DSL
Array operator reference.
Database index
Full guide map.