Skip to content

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 strategiesAUTOINCREMENT, INTEGER (MAX+1 with retry) and NONE (provided by the caller, e.g. UUID)
  • Consistent exception handling — every write error is thrown as PersistException. No raw PDOException handling 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

bash
composer require jardissupport/repository

GitHub: jardisSupport/repository

Dependencies:

PackagePurpose
jardissupport/dbquerySQL builder for findByQuery()
jardissupport/contractsInterfaces and exceptions

Basic Usage

With simple PDO connection

php
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);
// false

With Connection Pool (Read/Write Splitting)

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

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

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

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

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

More Query Examples

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

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

php
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();
}
MethodError caseException
insertEmpty values, DB error, missing PK with NONE, wrong PK type with NONEPersistException
updateDB errorPersistException
deleteDB errorPersistException
deleteAllDB errorPersistException
findByQueryQuery not preparedInvalidArgumentException

Update Return Values

php
// 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, []);  // true

PDO Adapters

For existing PDO instances that should be used as contract interfaces:

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

Directory 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 generator

API Reference

Repository

MethodSignatureDescription
__construct__construct(ConnectionPoolInterface|PDO $connection)Pool or single PDO
insertinsert(string $table, string $pk, array $values, PkStrategy $strategy = AUTOINCREMENT): int|stringInsert with PK strategy
updateupdate(string $table, string $pk, int|string $id, array $values): boolUpdate
deletedelete(string $table, string $pk, int|string $id): boolDelete single row
deleteAlldeleteAll(string $table, string $pk, array $ids): voidBatch delete
findByIdfindById(string $table, string $pk, int|string $id): ?arrayLoad row by PK
findByQueryfindByQuery(DbQueryBuilderInterface $query): arrayFlexible query
existsexists(string $table, string $pk, int|string $id): boolCheck existence

Complete Example

A typical use case with UUID PKs, read/write splitting and DbQuery:

php
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)) {
    // ...
}