solophp / base-repository
Base repository pattern implementation for PHP applications
Requires
- php: ^8.3
- doctrine/dbal: ^4.3
Requires (Dev)
- phpstan/phpstan: ^2.0
- squizlabs/php_codesniffer: ^3.13
README
Lightweight base repository with built-in soft delete and eager loading capabilities.
RepositoryInterface<TModel>
: generic contract for CRUD, aggregates, pagination, and transactions.BaseRepository<TModel>
: ready-to-extend base with criteria parsing, sorting, mapping, soft delete, and eager loading.
Installation
composer require solophp/base-repository
Requirements
- PHP 8.3+
- Doctrine DBAL (
doctrine/dbal
^4.3)
Quick Start
use Solo\BaseRepository\BaseRepository; use Doctrine\DBAL\Connection; // Basic repository (no additional features) class LogRepository extends BaseRepository { public function __construct(Connection $connection) { parent::__construct($connection, Log::class, 'logs'); } } // Repository with soft delete class UserRepository extends BaseRepository { protected string $deletedAtColumn = 'deleted_at'; // Enable soft delete public function __construct(Connection $connection) { parent::__construct($connection, User::class, 'users'); } } // Repository with soft delete and eager loading class PostRepository extends BaseRepository { protected string $deletedAtColumn = 'deleted_at'; // Enable soft delete protected array $relationConfig = [ // Enable eager loading 'user' => ['belongsTo', 'userRepository', 'user_id', 'setUser'], 'comments' => ['hasMany', 'commentRepository', 'post_id', 'setComments'] ]; public function __construct(Connection $connection, UserRepository $userRepo, CommentRepository $commentRepo) { parent::__construct($connection, Post::class, 'posts'); $this->userRepository = $userRepo; $this->commentRepository = $commentRepo; } }
Features
Auto-Configuration
Features are automatically enabled based on configuration:
- Soft Delete: Define
protected string $deletedAtColumn
to enable - Eager Loading: Define
protected array $relationConfig
to enable - Custom IDs: Set
protected bool $useAutoIncrement = false
to use custom IDs instead of auto-increment
Constructor
__construct( protected Connection $connection, string $modelClass, string $table, ?string $tableAlias = null, string $mapperMethod = 'fromArray' )
Configurable Properties
Property | Type | Default | Description |
---|---|---|---|
$primaryKey |
string | 'id' |
Primary key column |
$tableAlias |
?string | null |
Table alias (defaults to first letter of table name) |
$table |
string | - | Database table name (constructor parameter) |
$modelClass |
string | - | Model class name (constructor parameter) |
$mapperMethod |
string | 'fromArray' |
Static method for mapping array to model |
$connection |
Connection | - | Doctrine DBAL connection (constructor parameter) |
$deletedAtColumn |
?string | null |
Soft-delete timestamp column (enables soft delete) |
$relationConfig |
array | [] |
Relations configuration (enables eager loading) |
$useAutoIncrement |
bool | true |
Whether to use auto-increment IDs or custom IDs |
Criteria Syntax
Pattern | Example | SQL |
---|---|---|
Equality | ['status' => 'active'] |
status = ? |
Null | ['deleted_at' => null] |
deleted_at IS NULL |
IN (list) | ['id' => [1,2,3]] |
id IN (?, ?, ?) |
Operator | ['age' => ['>', 18]] |
age > ? |
Search | ['search' => ['name' => 'John', 'email' => 'example']] |
name LIKE ? AND email LIKE ? |
Deleted filter | ['deleted' => 'only'] or ['deleted' => 'with'] |
Filter soft-deleted records |
Retrieval Methods
Method | Description |
---|---|
find(int|string $id): ?TModel |
Get model by primary key |
findOneBy(array $criteria, ?array $orderBy = null): ?TModel |
First by criteria and sort |
findAll(): list<TModel> |
All rows |
findBy(array $criteria, ?array $orderBy = null, ?int $perPage = null, ?int $page = null): list<TModel> |
Filtered list, optional pagination |
Mutation Methods
Method | Description |
---|---|
create(array $data): TModel |
Create and return model object |
insertMany(list<array<string,mixed>> $records): int |
Bulk insert, returns affected rows |
update(int|string $id, array $data): TModel |
Update by ID and return model |
updateBy(array $criteria, array $data): int |
Update by criteria |
delete(int|string $id): int |
Soft or hard delete by ID |
deleteBy(array $criteria): int |
Soft or hard delete by criteria |
Existence and Aggregates
Method | Description |
---|---|
exists(array $criteria): bool |
Check existence |
count(array $criteria): int |
Count rows |
Soft Delete
Enable soft delete by defining $deletedAtColumn
:
class UserRepository extends BaseRepository { protected string $deletedAtColumn = 'deleted_at'; // Enables soft delete }
Soft Delete Methods
Method | Description |
---|---|
['deleted' => 'with'] |
Include soft-deleted records |
restore(int|string $id): int |
Restore soft-deleted record |
forceDelete(int|string $id): int |
Hard delete bypassing soft delete |
forceDeleteBy(array $criteria): int |
Hard delete by criteria bypassing soft delete |
Examples
// Safe behavior by default (only active records) $users = $repo->findAll(); // Only active records $repo->delete(1); // Soft delete (sets deleted_at) // Include soft-deleted records $allUsers = $repo->findBy(['deleted' => 'with']); // All including soft-deleted // Hard delete (physical removal) $repo->forceDelete(1); // Physical deletion // Restore soft-deleted records $repo->restore(1); // Sets deleted_at = NULL // API filtering $deleted = $repo->findBy(['deleted' => 'only']); // Only soft-deleted $all = $repo->findBy(['deleted' => 'with']); // All including soft-deleted $active = $repo->findBy(['deleted' => 'without']); // Only active (default)
Custom ID Support
By default, the repository uses auto-increment IDs via lastInsertId()
. For tables with custom IDs (UUIDs, prefixed IDs, etc.), disable auto-increment:
class ProductRepository extends BaseRepository { protected bool $useAutoIncrement = false; // Disable auto-increment public function __construct(Connection $connection) { parent::__construct($connection, Product::class, 'products'); } }
Usage with Custom IDs
// Custom ID must be provided when auto-increment is disabled $product = $repo->create([ 'id' => 'PROD-123', 'name' => 'Custom Product', 'price' => 99.99 ]); // Works with UUIDs too $user = $userRepo->create([ 'id' => 'uuid-4e8c-9f7a-2b1d-3e5a6b7c8d9e', 'email' => 'user@example.com' ]);
Validation
When $useAutoIncrement = false
, the primary key must be provided in the data array, otherwise an InvalidArgumentException
is thrown:
// This will throw an exception if $useAutoIncrement = false $repo->create(['name' => 'Product']); // Missing 'id'
Eager Loading
Enable eager loading by defining $relationConfig
:
class PostRepository extends BaseRepository { protected array $relationConfig = [ 'user' => ['belongsTo', 'userRepository', 'user_id', 'setUser'], 'comments' => ['hasMany', 'commentRepository', 'post_id', 'setComments', ['id' => 'ASC']] ]; public function __construct(Connection $connection, UserRepository $userRepo, CommentRepository $commentRepo) { parent::__construct($connection, Post::class, 'posts'); $this->userRepository = $userRepo; $this->commentRepository = $commentRepo; } }
Relation Configuration Format
'relationName' => [type, repositoryProperty, foreignKey, setterMethod, ?sort]
- type:
'belongsTo'
or'hasMany'
- repositoryProperty: Property name of related repository on current repository
- foreignKey: Foreign key column name
- setterMethod: Method to call on model to set the relation
- sort: Optional sorting for hasMany relations
Usage
// Load single relation $posts = $repo->with(['user'])->findAll(); // Load multiple relations $posts = $repo->with(['user', 'comments'])->findBy(['status' => 'published']); // Nested relations via dot-notation // Example domain: products -> productAttributes (hasMany) -> attribute (belongsTo) $products = $productRepo ->with(['productAttributes', 'productAttributes.attribute']) ->findAll(); // Works with all find methods $post = $repo->with(['user', 'comments'])->find(1); $post = $repo->with(['user'])->findOneBy(['slug' => 'my-post']);
Combining Features
Both soft delete and eager loading can be used together:
class PostRepository extends BaseRepository { protected string $deletedAtColumn = 'deleted_at'; // Enable soft delete protected array $relationConfig = [ // Enable eager loading 'user' => ['belongsTo', 'userRepository', 'user_id', 'setUser'] ]; } // Usage $activePosts = $repo->with(['user'])->findAll(); // Active posts with users $allPosts = $repo->with(['user'])->findBy(['deleted' => 'with']); // All posts with users $deletedPosts = $repo->with(['user'])->findBy(['deleted' => 'only']); // Deleted posts with users
Transactions
Method | Description |
---|---|
beginTransaction(): bool |
Begin transaction |
commit(): bool |
Commit |
rollBack(): bool |
Rollback |
inTransaction(): bool |
Transaction state |
withTransaction(callable $cb): mixed |
Execute callback in transaction |
Example Usage
// Basic filtering and sorting with pagination $users = $repo->findBy( ['status' => 'active', 'age' => ['>', 18]], ['created_at' => 'DESC'], 20, // perPage 1 // page ); // Search queries (LIKE) $filtered = $repo->findBy([ 'search' => ['name' => 'john', 'email' => 'example.com'] ]); // Transactions $repo->withTransaction(function (UserRepository $r) { $user = $r->create(['name' => 'Temp', 'email' => 'temp@example.com']); $r->update($user->id, ['name' => 'Temp Updated']); });
Extending Repositories
Add domain-specific methods using table()
and builder chaining:
final class UserRepository extends BaseRepository { protected string $deletedAtColumn = 'deleted_at'; public function findTopActive(int $limit = 10): array { $rows = $this->table() ->andWhere('status = :status') ->setParameter('status', 'active') ->orderBy('score', 'DESC') ->setMaxResults($limit) ->executeQuery() ->fetchAllAssociative(); return array_map(fn(array $r) => $this->mapRowToModel($r), $rows); } }
Notes
- Features are automatically enabled based on configuration properties
- Soft delete logic integrates seamlessly with criteria syntax
- Eager loading works with soft delete enabled repositories
- Validate user-provided fields against whitelists for security
License
This library is released under the MIT License. See the LICENSE
file for details.