This guide provides a complete and practical overview of how to use the
maatify/data-repository library.
It is intentionally written in a clear, simple, and developer-friendly manner.
No internals, no unnecessary theory — just everything you need to use the library effectively.
- Introduction
- Installation
- Core Concepts
- Adapter Initialization
- Creating Repositories
- MySQL Repository Usage
- MongoDB Repository Usage
- Redis Repository Usage
- Filters (Full Usage Guide)
- Sorting
- Limits & Offsets
- Pagination
- DTOs & Hydration
- Error Handling
- Raw Driver Access
- Using Fake Adapters for Testing
- Best Practices
- Common Mistakes
- Final Notes
maatify/data-repository provides a unified, adapter-agnostic repository layer
that works consistently across:
- MySQL (PDO / DBAL)
- MongoDB
- Redis
- Fake in-memory drivers (for tests)
You write one repository class, and it works with real and fake adapters without modification.
composer require maatify/data-repositorycomposer require maatify/data-adapters
data-repositoryis adapter-agnostic and does not require specific drivers at installation time. For real MySQL, MongoDB, or Redis usage, installmaatify/data-adapters.
composer require maatify/data-fakesYour data-access layer. Provides CRUD, filtering, sorting, pagination, hydration, etc.
A wrapper around an actual database driver.
In-memory adapters for deterministic testing.
An expressive array syntax automatically converted into SQL/Mongo conditions.
Converts raw arrays into typed PHP objects.
Maatify Data Repository works only with adapters provided by
maatify/data-adapters.
All adapters share:
- A unified configuration system (
EnvironmentConfig) - Connection builders (
MySqlConfigBuilder,MongoConfigBuilder,RedisConfigBuilder) - A DSN-first strategy
- Multi-profile support (
main,logs,analytics, …)
Because configuration resolution happens inside BaseAdapter,
adapters cannot be manually instantiated with raw drivers.
They must be created through DatabaseResolver.
DatabaseResolver loads configuration from .env using EnvironmentConfig.
It then instantiates the correct adapter and injects the proper profile.
This is the only supported and recommended method.
MYSQL_MAIN_DSN=mysql:host=127.0.0.1;dbname=test;charset=utf8mb4
MYSQL_MAIN_USER=root
MYSQL_MAIN_PASS=secret
MYSQL_DBAL_MAIN_URL=mysql://root:secret@127.0.0.1/test
MONGO_MAIN_DSN=mongodb://127.0.0.1:27017
MONGO_MAIN_DATABASE=testdb
REDIS_MAIN_HOST=127.0.0.1
REDIS_MAIN_PORT=6379
PREDIS_MAIN_URI=redis://127.0.0.1:6379use Maatify\DataAdapters\Core\EnvironmentConfig;
use Maatify\DataAdapters\Core\DatabaseResolver;
$config = new EnvironmentConfig(__DIR__);
$resolver = new DatabaseResolver($config);
// Each adapter is resolved by a profile name
$mysql = $resolver->resolve('mysql.main');
$mysqlDbal = $resolver->resolve('mysql_dbal.main');
$mongo = $resolver->resolve('mongo.main');
$redis = $resolver->resolve('redis.main');
$predis = $resolver->resolve('predis.main');$users = new UserRepository($mysql);
$logs = new LogRepository($mongo);
if (! $mongo->healthCheck()) {
throw new RuntimeException('MongoDB is not reachable');
}Adapters cannot be directly instantiated like:
new MySQLAdapter($pdo); // ❌ invalid
new MongoAdapter($client); // ❌ invalid
new RedisAdapter($redis); // ❌ invalidBecause each adapter requires:
- Profile name injection
- EnvironmentConfig instance
- Builder resolution (DSN → registry → legacy fallback)
- Automatic authentication options
- Connection validation
- Internal config mapping via ConnectionConfigDTO
All of this is handled inside BaseAdapter, not in user land.
You must always use DatabaseResolver to construct adapters.
| Adapter | Backend | Notes |
|---|---|---|
| MySQLAdapter | PDO | Lightweight, simple SQL workloads |
| MySQLDbalAdapter | Doctrine DBAL | Advanced SQL, portability |
| MongoAdapter | MongoDB\Client | DSN-first, full builder support |
| RedisAdapter | phpredis | Best performance |
| PredisAdapter | Predis\Client | Pure PHP, fallback when extension unavailable |
- ✔ Adapters must be created through DatabaseResolver
- ✔ Manual instantiation is not allowed
- ✔ All configuration flows through EnvironmentConfig + Builders
- ✔ Repositories accept any adapter implementing AdapterInterface
- ✔ Internal logic (DSN mode, fallback mode, health check, reconnect) is automatically handled
Your custom repository extends one of:
BaseMySQLRepositoryBaseMongoRepositoryBaseRedisRepository
Use Generics to define the entity type handled by the repository.
/**
* @extends BaseMySQLRepository<UserDTO>
*/
class UserRepository extends BaseMySQLRepository
{
protected string $tableName = 'users';
}/**
* @extends BaseMongoRepository<LogDTO>
*/
class LogRepository extends BaseMongoRepository
{
protected string $collectionName = 'logs';
}/**
* @extends BaseRedisRepository<CacheItemDTO>
*/
class CacheRepository extends BaseRedisRepository
{
protected string $keyPrefix = 'cache:';
}$userRepo = new UserRepository($mysqlAdapter);$user = $userRepo->find(10);$rows = $userRepo->findBy(['active' => 1]);$id = $userRepo->insert([
'name' => 'Ahmed',
'email' => 'ahmed@example.com',
]);$userRepo->update($id, ['active' => 0]);$userRepo->delete($id);$total = $userRepo->count(['active' => 1]);$logRepo = new LogRepository($mongoAdapter);$logRepo->insert([
'type' => 'error',
'message' => 'Something happened',
]);$logs = $logRepo->findBy(['type' => 'error']);$count = $logRepo->count(['type' => 'error']);Redis Repositories treat data as a collection of items stored as JSON values.
The id field is required and used to generate the Redis key.
$cacheRepo = new CacheRepository($redisAdapter);$cacheRepo->insert([
'id' => 'user:1',
'name' => 'Ahmed',
'role' => 'admin',
]);
// Stores as JSON in key: 'cache:user:1'$data = $cacheRepo->findOneBy(['id' => 'user:1']);$cacheRepo->delete('user:1');['active' => 1]['id IN' => [1, 2, 3]]['status NOT IN' => ['deleted', 'archived']]['email LIKE' => '%gmail.com']['age >' => 18, 'age <=' => 55]['deleted_at IS' => null]
['verified_at IS NOT' => null]$orderBy = [
'created_at' => 'DESC',
'id' => 'ASC',
];
$rows = $repo->findBy(['active' => 1], $orderBy);$rows = $repo->findBy(
['active' => 1],
['id' => 'DESC'],
limit: 50,
offset: 100
);Repositories return a PaginationResultDTO which contains the data and metadata.
$result = $userRepo->paginate(
page: 2,
perPage: 20,
orderBy: ['created_at' => 'DESC']
);
// Accessing Data
$items = $result->getData(); // Array of rows or objects
// Accessing Metadata
$meta = $result->getPagination();
echo $meta->totalItems;
echo $meta->totalPages;
echo $meta->page;
echo $meta->perPage;You can automatically convert database rows into DTOs by setting a Hydrator.
class UserDTO {
public function __construct(
public int $id,
public string $name,
public string $email,
) {}
}/**
* @implements HydratorInterface<UserDTO>
*/
class UserHydrator implements HydratorInterface {
public function hydrate(array $row): UserDTO {
return new UserDTO((int)$row['id'], $row['name'], $row['email']);
}
}$userRepo->setHydrator(new UserHydrator());
// Returns UserDTO[]
$users = $userRepo->findObjectsBy(['active' => 1]);
// Returns ?UserDTO
$user = $userRepo->findObject(1);
// Returns PaginationResultDTO containing UserDTO[]
$result = $userRepo->paginateObjects(page: 1);All repository methods throw Maatify\DataRepository\Exceptions\RepositoryException on failure.
try {
$repo->findBy([], [], limit: -1);
} catch (RepositoryException $e) {
// Handle error (e.g., log it or show a user-friendly message)
echo $e->getMessage();
}By default, the underlying driver (PDO, MongoDB\Client, Redis) is protected inside the repository. If you need public access to the raw driver, you must explicitly expose it in your child repository.
class UserRepository extends BaseMySQLRepository {
public function getRawPdo(): PDO {
return $this->getDriver(); // Protected method in BaseRepository
}
}
// Usage
$pdo = $userRepo->getRawPdo();Fake adapters simulate database behavior in memory, allowing for fast, deterministic unit tests.
$storage = new FakeStorageLayer();
$fake = new FakeMySQLAdapter($storage);
// Seed data
$storage->seed('users', [
['id' => 1, 'name' => 'Test', 'active' => 1],
]);
$repo = new UserRepository($fake);
$result = $repo->findBy(['active' => 1]);- One repository per table/collection
- Keep filters simple
- Avoid business logic inside repositories
- Use DTOs for domain-rich structures
- Use FakeAdapters for fast and isolated tests
- Validate input in services before passing it to the repository
❌ Invalid limits/offsets (must be positive)
❌ Putting business logic inside repositories
❌ Over-using Redis for complex queries (it performs full scans for filtering)
❌ Forgetting to seed FakeAdapters in tests
❌ Overcomplicating filters
For more comprehensive and phase-specific examples, please check the examples/phase29/ directory.
maatify/data-repository provides a clean, unified, framework-agnostic API for
interacting with MySQL, MongoDB, Redis, and fake adapters with consistent behavior.
Use this guide as your main reference for building clean, reusable, and testable data-access layers.