Part of Jardis — the Domain-Driven Design platform for PHP. You model your domain; Jardis generates the production-ready hexagonal code (DTOs, Command/Query handlers, repositories, persistence). This package is part of the open-source foundation that generated code runs on.
An implementation of the repository pattern for PHP: a generic CRUD repository operating on raw data — no entities, no ORM, just arrays in and out. Built-in read/write splitting routes queries to readers and mutations to the writer. Three primary key strategies cover autoincrement, generated integers, and application-supplied keys. It is the persistence building block that the repositories in generated Jardis code rely on.
- Raw Data — arrays in, arrays out; no entity mapping, no hydration overhead
- Read/Write Splitting — queries automatically route to a dedicated reader; mutations go to the writer
- 3 PK Strategies —
PkStrategy::AUTOINCREMENT,PkStrategy::INTEGER,PkStrategy::NONEfor all insert patterns - ConnectionPool Integration — accepts a
ConnectionPoolInterfaceor a plainPDOinstance - Query Builder Support —
findByQuery()accepts anyDbQueryBuilderInterfacefor complex SELECT statements - Exists Check —
exists()avoids full row fetches when only presence matters - Batch Delete —
deleteAll()removes multiple rows in a single call - Conditional Writes — optional
$expectedonupdate()/delete()guards against stale reads (optimistic concurrency); returnsfalsewhen the row no longer matches - Lazy Connection Initialization — reader and writer connections are opened only when first used
composer require jardissupport/repositoryuse JardisSupport\Repository\Repository;
use JardisSupport\Contract\Repository\PrimaryKey\PkStrategy;
$repository = new Repository($pdo);
// Insert a row — returns the new autoincrement id
$id = $repository->insert('orders', 'id', [
'customer_id' => 42,
'total' => 199.99,
'status' => 'pending',
]);
// Fetch by primary key
$row = $repository->findById('orders', 'id', $id);
// Update
$repository->update('orders', 'id', $id, ['status' => 'confirmed']);
// Delete
$repository->delete('orders', 'id', $id);use JardisSupport\Repository\Repository;
use JardisSupport\DbQuery\DbQuery;
use JardisSupport\Contract\Repository\PrimaryKey\PkStrategy;
// Read/write splitting via a connection pool
$repository = new Repository($connectionPool);
// Application-supplied UUID key (PkStrategy::NONE — no last-insert-id lookup)
$uuid = $uuidGenerator->generate();
$repository->insert('products', 'uuid', ['uuid' => $uuid, 'name' => 'Widget'], PkStrategy::NONE);
// Complex query via DbQuery builder
$query = (new DbQuery())
->select('o.id, o.total, c.email')
->from('orders', 'o')
->innerJoin('customers', 'o.customer_id = c.id', 'c')
->where('o.status')->eq('pending')
->and('o.total')->gte(100)
->orderBy('o.created_at', 'DESC')
->limit(20);
$rows = $repository->findByQuery($query);
// Batch delete
$repository->deleteAll('sessions', 'id', [101, 102, 103]);
// Existence check without fetching the row
if ($repository->exists('users', 'id', $userId)) {
// ...
}public function update(
string $table, string $pkColumn, int|string $id, array $values,
array $expected = [], // column => value the row must still carry; null => IS NULL
): bool;
public function delete(
string $table, string $pkColumn, int|string $id,
array $expected = [],
): bool;$expected pins the write to the values a caller already read — one extra AND condition per column. If the row no longer carries those values, the statement touches 0 rows and the call returns false instead of silently overwriting a stale read.
// Two readers fetch the same row: ['status' => 'pending', ...]
$row = $repository->findById('orders', 'id', $id);
// Reader A writes first — the row still matches, write succeeds
$repository->update('orders', 'id', $id, ['status' => 'confirmed'], ['status' => 'pending']); // true
// Reader B writes second, unaware A already won — the row no longer matches
$repository->update('orders', 'id', $id, ['status' => 'cancelled'], ['status' => 'pending']); // false
nullin$expectedmaps toIS NULL, not= NULL.falsemeans the row no longer carries the expected values — or theiddoesn't exist. For a conflict check both are the same signal: the read this write was based on is no longer valid.$expectedvalues must bescalar|null; an object or array throwsInvalidArgumentException.- An empty
$valuescombined with a non-empty$expectedthrowsInvalidArgumentExceptiontoo — the existing "nothing to write" shortcut would otherwise skip the check silently and returntrueunchecked. Useexists()/findById()for a pure read-side check.- MySQL note:
rowCount()counts changed rows — an update that would set identical values reports0. This doesn't fire for repositories built from changed-field diffs, but it's a caveat if$valuesis assembled differently.
Full documentation, guides, and API reference:
docs.jardis.io/en/support/repository
This package is licensed under the MIT License.
Jardis · Documentation · Headgent
This package ships with a skill for Claude Code, Cursor, Continue, and Aider. Install it in your consuming project:
composer require --dev jardis/dev-skillsMore details: https://docs.jardis.io/en/skills