Repository
Generic CRUD repository with raw-data principle, read/write splitting and three PK strategies.
Introduction
Most repository implementations couple themselves to an ORM: entities in, entities out, with a layer of magic in between. Anyone who just needs a clean CRUD interface over PDO (without hydration, without unit-of-work, without 50 classes) faces the choice between too much abstraction or too little.
jardissupport/repository is the deliberate middle ground. A lean, table-agnostic repository that works exclusively with raw arrays:
- Raw data only — every method accepts and returns
array<string, mixed>. No entities, no ORM, no magic - Read/write splitting — queries automatically go to the reader connection, mutations to the writer. Transparent to the caller
- Three PK strategies —
AUTOINCREMENT,INTEGER(MAX+1 with retry) andNONE(provided by the caller, e.g. UUID) - Consistent exception handling — every write error is thrown as
PersistException. No rawPDOExceptionhandling in calling code - DbQuery integration — flexible queries via the fluent SQL builder from
jardissupport/dbquery - Lazy handlers — every handler is instantiated only on first call. Constructing a repository costs virtually nothing
Installation
composer require jardissupport/repositoryGitHub: jardisSupport/repository
Dependencies:
| Package | Purpose |
|---|---|
jardissupport/dbquery | SQL builder for findByQuery() |
jardissupport/contracts | Interfaces and exceptions |
Basic Usage
With simple PDO connection
use JardisSupport\Repository\Repository;
$pdo = new PDO('mysql:host=localhost;dbname=myapp', 'user', 'pass');
$repository = new Repository($pdo);
// Insert
$id = $repository->insert('users', 'id', [
'name' => 'John Doe',
'email' => 'john@example.com',
]);
// $id = 1 (autoincrement)
// Read
$user = $repository->findById('users', 'id', $id);
// ['id' => 1, 'name' => 'John Doe', 'email' => 'john@example.com']
// Update
$repository->update('users', 'id', $id, ['name' => 'Jane Doe']);
// true
// Delete
$repository->delete('users', 'id', $id);
// true
// Exists
$repository->exists('users', 'id', $id);
// falseWith Connection Pool (Read/Write Splitting)
use JardisAdapter\DbConnection\ConnectionPool;
use JardisSupport\Repository\Adapter\PdoConnection;
// ConnectionPool expects DbConnectionInterface instances —
// raw PDOs are wrapped with PdoConnection.
$pool = new ConnectionPool(
writer: new PdoConnection($writerPdo),
readers: [new PdoConnection($reader1Pdo), new PdoConnection($reader2Pdo)],
);
$repository = new Repository($pool);
// findById → goes to reader
$user = $repository->findById('users', 'id', 42);
// insert → goes to writer
$id = $repository->insert('users', 'id', ['name' => 'John']);The writer vs. reader decision is completely transparent: the caller needs to configure nothing.
PK Strategies
Three strategies for different primary key models:
AUTOINCREMENT (Default)
For tables with an auto-increment column. The PK value is not passed in $values:
use JardisSupport\Contract\Repository\PrimaryKey\PkStrategy;
$id = $repository->insert('users', 'id', [
'name' => 'Alice',
'email' => 'alice@example.com',
], PkStrategy::AUTOINCREMENT);
// $id = 1 (int, from PDO::lastInsertId())INTEGER (MAX+1 with Retry)
For tables without a database-side auto-increment sequence. The PK is generated via SELECT MAX(pk) + 1:
$id = $repository->insert('legacy_table', 'id', [
'name' => 'Bob',
], PkStrategy::INTEGER);
// $id = 1 (or next free value)On a duplicate key error, the operation is automatically retried up to 3 times with a freshly generated PK. After that, PersistException is thrown.
NONE (Caller provides PK)
For UUID-based or externally generated IDs. The PK must be included in $values:
$id = $repository->insert('orders', 'id', [
'id' => '550e8400-e29b-41d4-a716-446655440000',
'total' => 299.99,
], PkStrategy::NONE);
// $id = '550e8400-e29b-41d4-a716-446655440000' (string)If the PK is missing from $values or is neither int nor string, a PersistException is thrown immediately.
Flexible Queries with DbQuery
For everything beyond findById() (search, filtering, sorting, aggregation) findByQuery() accepts a prepared query from the DbQuery builder:
use JardisSupport\DbQuery\DbQuery;
$query = (new DbQuery())
->select('*')
->from('users')
->where('status')->equals('active')
->where('age')->between(25, 35)
->orderBy('name', 'ASC')
->limit(10);
$users = $repository->findByQuery($query);
// Array of associative arraysMore Query Examples
// LIKE
$query = (new DbQuery())->select('*')->from('users')
->where('name')->like('Alice%');
// IN
$query = (new DbQuery())->select('*')->from('users')
->where('status')->in(['active', 'pending']);
// IS NULL
$query = (new DbQuery())->select('*')->from('users')
->where('email')->isNull();
// OR condition
$query = (new DbQuery())->select('*')->from('users')
->where('age')->lower(25)
->or('status')->equals('inactive');
// COUNT
$query = (new DbQuery())->select('COUNT(*) as total')->from('users')
->where('status')->equals('active');
$result = $repository->findByQuery($query);
// [['total' => 42]]Batch Delete
$repository->deleteAll('users', 'id', [1, 3, 5]);
// Deletes all three rows in a single DELETE ... WHERE id IN (1, 3, 5)
// Empty array → no-op
$repository->deleteAll('users', 'id', []);Error Handling
All write operations catch PDOException and throw PersistException instead:
use JardisSupport\Contract\Repository\Exception\PersistException;
try {
$repository->insert('users', 'id', ['name' => 'John']);
} catch (PersistException $e) {
// Consistent exception type for all write errors
echo $e->getMessage();
}| Method | Error case | Exception |
|---|---|---|
insert | Empty values, DB error, missing PK with NONE, wrong PK type with NONE | PersistException |
update | DB error | PersistException |
delete | DB error | PersistException |
deleteAll | DB error | PersistException |
findByQuery | Query not prepared | InvalidArgumentException |
Update Return Values
// Row found and updated
$repository->update('users', 'id', 1, ['name' => 'Jane']); // true
// Row not found
$repository->update('users', 'id', 9999, ['name' => 'Ghost']); // false
// Empty values → no-op, returns true
$repository->update('users', 'id', 1, []); // truePDO Adapters
For existing PDO instances that should be used as contract interfaces:
use JardisSupport\Repository\Adapter\PdoConnection;
use JardisSupport\Repository\Adapter\PdoConnectionPool;
// Single connection (DbConnectionInterface)
$connection = new PdoConnection($pdo);
$connection->beginTransaction();
$connection->commit();
$connection->rollback();
$connection->inTransaction(); // bool
$connection->getDriverName(); // 'mysql', 'pgsql', 'sqlite'
$connection->getDatabaseName(); // database name (multi-driver-aware)
// Connection pool (ConnectionPoolInterface) — reader = writer
$pool = new PdoConnectionPool($pdo);PdoConnection automatically detects the database driver and uses the correct method for getDatabaseName():
- MySQL:
SELECT DATABASE() - PostgreSQL:
current_database() - SQLite:
PRAGMA database_list
Architecture
The repository follows the Closure-Orchestrator-Pattern with lazily initialized handlers:
Repository ← Orchestrator (facade)
├── ConnectionPool ← Writer/reader routing
│ ├── QueryExecutor (Writer) ← SQL execution (mutations)
│ └── QueryExecutor (Reader) ← SQL execution (queries)
├── InsertHandler ← PK strategy dispatch
│ └── IntegerPkGenerator ← MAX+1 with retry
├── UpdateHandler ← UPDATE by PK
├── DeleteHandler ← DELETE single row
├── DeleteAllHandler ← DELETE IN (ids)
├── FindByIdHandler ← SELECT WHERE pk = id
└── ExistsHandler ← SELECT 1 LIMIT 1Directory Structure
src/
├── Repository.php ← Orchestrator
├── Adapter/
│ ├── PdoConnection.php ← PDO → DbConnectionInterface
│ └── PdoConnectionPool.php ← PDO → ConnectionPoolInterface
└── Handler/
├── QueryExecutor.php ← SQL execution + dialect detection
├── InsertHandler.php ← Insert with PK strategy
├── UpdateHandler.php ← Update by PK
├── DeleteHandler.php ← Delete single row
├── DeleteAllHandler.php ← Delete batch
├── FindByIdHandler.php ← Select by PK
├── ExistsHandler.php ← Existence check
└── IntegerPkGenerator.php ← MAX+1 generatorAPI Reference
Repository
| Method | Signature | Description |
|---|---|---|
__construct | __construct(ConnectionPoolInterface|PDO $connection) | Pool or single PDO |
insert | insert(string $table, string $pk, array $values, PkStrategy $strategy = AUTOINCREMENT): int|string | Insert with PK strategy |
update | update(string $table, string $pk, int|string $id, array $values): bool | Update |
delete | delete(string $table, string $pk, int|string $id): bool | Delete single row |
deleteAll | deleteAll(string $table, string $pk, array $ids): void | Batch delete |
findById | findById(string $table, string $pk, int|string $id): ?array | Load row by PK |
findByQuery | findByQuery(DbQueryBuilderInterface $query): array | Flexible query |
exists | exists(string $table, string $pk, int|string $id): bool | Check existence |
Complete Example
A typical use case with UUID PKs, read/write splitting and DbQuery:
use JardisSupport\Repository\Repository;
use JardisSupport\Contract\Repository\PrimaryKey\PkStrategy;
use JardisSupport\Data\Identity;
use JardisSupport\DbQuery\DbQuery;
$repository = new Repository($connectionPool);
$identity = new Identity();
// Create new record with UUID
$orderId = $identity->generateUuid7();
$repository->insert('orders', 'id', [
'id' => $orderId,
'customer_id' => $customerId,
'total' => 299.99,
'status' => 'pending',
'created_at' => date('Y-m-d H:i:s'),
], PkStrategy::NONE);
// Insert order lines as batch
foreach ($items as $item) {
$repository->insert('order_lines', 'id', [
'id' => $identity->generateUuid7(),
'order_id' => $orderId,
'product_id' => $item['product_id'],
'quantity' => $item['quantity'],
'price' => $item['price'],
], PkStrategy::NONE);
}
// Load active orders for a customer (→ reader connection)
$query = (new DbQuery())
->select('*')
->from('orders')
->where('customer_id')->equals($customerId)
->where('status')->in(['pending', 'confirmed'])
->orderBy('created_at', 'DESC')
->limit(20);
$orders = $repository->findByQuery($query);
// Update single order (→ writer connection)
$repository->update('orders', 'id', $orderId, [
'status' => 'confirmed',
'updated_at' => date('Y-m-d H:i:s'),
]);
// Check existence without loading data
if ($repository->exists('orders', 'id', $orderId)) {
// ...
}