Infrastructure Layer
The Infrastructure layer implements interfaces defined in Domain. It handles databases, cache, external APIs, and more. This page explains how to build Infrastructure classes with step-by-step examples.
Minimum Requirements
| Requirement | Version | Notes |
|---|---|---|
| PHP | ≥ 8.3 | Required for typed properties, enums, match expressions, and modern PHP features |
| CodeIgniter | ≥ 4.0 | Framework foundation |
| MySQL | ≥ 8.0 | Required for JSON functions (JSON_CONTAINS) |
| Redis | ≥ 6.0 | For caching, queues, and real-time metrics |
| Composer | ≥ 2.0 | Dependency management |
Optional Dependencies
| Package | Purpose |
|---|---|
dompdf/dompdf | PDF export functionality |
phpoffice/phpspreadsheet | Excel export functionality |
CLI Generator
Use the make:infrastructure command to generate infrastructure templates:
# Basic usage
php spark make:infrastructure <type> <name>
# Examples
php spark make:infrastructure Database Product # Creates ProductRepository.php
php spark make:infrastructure Cache UserPrefs # Creates UserPrefs.php cache
php spark make:infrastructure Mail Sendgrid # Creates Sendgrid.php mailer
php spark make:infrastructure Export Invoice # Creates Invoice.php exporter
php spark make:infrastructure Http PaymentGateway # Creates PaymentGateway.php client
php spark make:infrastructure Queue Email # Creates Email.php queue
php spark make:infrastructure Media Thumbnail # Creates Thumbnail.php processor
php spark make:infrastructure Logging Audit # Creates Audit.php logger
php spark make:infrastructure Notification Sms # Creates Sms.php channel
php spark make:infrastructure Monitoring Api # Creates Api.php metric storage
php spark make:infrastructure Auth Token # Creates Token.php auth service
php spark make:infrastructure Rbac Custom # Creates Custom.php RBAC service
# Overwrite existing files
php spark make:infrastructure Database Product --force| Type | Description |
|---|---|
Database | MySQL Repository extending BaseRepository |
Cache | Redis cache implementing CacheInterface |
Mail | Email sender implementing MailerInterface |
Export | Excel/CSV export service |
Http | External API client |
Queue | Redis job queue |
Media | Image/media processor |
Logging | Custom logger with audit support |
Notification | Notification channel |
Monitoring | Performance metrics storage |
Auth | Authentication service |
Rbac | Role-based access control service |
Table of Contents
- Why Separate Infrastructure?
- Infrastructure Overview
- Database (Repositories)
- Cache (Redis)
- Auth (Session Context)
- RBAC Services
- Import (Excel/CSV)
- Export (Excel/PDF)
- Mail (SMTP)
- Notification Service
- Queue (Redis)
- HTTP (External APIs)
- Media (Image Processing)
- Logging
- Monitoring Storage
- Docker Verification
1. Why Separate Infrastructure?
Technical Concept
Domain defines what operations are needed (interfaces). Infrastructure defines how they're done (implementations).
// Domain: "I need to find users by email"
interface UserRepositoryInterface {
public function findByEmail(Email $email): ?User;
}
// Infrastructure: "Here's how I do it with MySQL"
class UserRepository implements UserRepositoryInterface {
public function findByEmail(Email $email): ?User {
$row = $this->db->table('users')
->where('email', $email->getValue())
->get()->getRowArray();
return $row ? $this->hydrate($row) : null;
}
}Simple Analogy
| Domain Says | Infrastructure Does |
|---|---|
| "Save this order" | Writes to MySQL database |
| "Get user preferences" | Fetches from Redis cache |
| "Send confirmation email" | Calls SendGrid API |
| "Store metrics" | Writes to Redis and MySQL |
Benefits
- Swappable: Change MySQL to PostgreSQL without touching Domain
- Testable: Use mock implementations for unit tests
- Clear boundaries: All external code in one place
2. Infrastructure Overview
The Infrastructure layer contains 12 service categories:
app/Infrastructure/
├── Auth/
│ ├── Jwt/ # JwtService
│ ├── Session/ # SessionUserContext
│ └── SSO/ # GoogleProvider, MicrosoftProvider, SSOUser
├── Cache/
│ └── Redis/ # RedisCache
├── Database/
│ └── MySQL/ # BaseRepository + context-organized repos
│ ├── Rbac/ # MenuRepository, PermissionRepository, RoleRepository
│ ├── User/ # UserRepository
│ ├── Approval/ # ApprovalRepository
│ ├── Notification/ # NotificationRepository
│ ├── ActivityLog/ # ActivityLogRepository
│ └── Import/ # ImportJobRepository
├── Export/
│ └── Custom/ # ExportService (Spreadsheet/Dompdf)
├── Http/
│ └── Custom/ # ExternalApiClient, WebhookNotification
├── Import/
│ └── Custom/ # ImportService
├── Logging/
│ └── Custom/ # AppLogger, CustomFileHandler
├── Mail/
│ └── Smtp/ # SmtpMailer
├── Media/
│ └── Custom/ # ImageOptimizer, JxlConverter, WebpConverter
├── Monitoring/
│ ├── MySQL/ # MySQLMetricStorage
│ └── Redis/ # RedisMetricStorage
├── Notification/
│ └── Custom/ # NotificationDispatcher
├── Queue/
│ ├── Database/ # DatabaseQueue
│ ├── Redis/ # RedisQueue
│ └── QueueManager.php # Orchestrator (engine-agnostic)
├── Rbac/
│ └── Custom/ # PermissionChecker, RoleManager, MenuBuilder, etc.
└── Security/
└── Captcha/
├── Math/ # MathCaptchaProvider
└── Recaptcha/ # RecaptchaProviderCache/Redis/, Mail/Smtp/, Queue/Redis/). To swap an implementation, add a sibling folder (e.g. Cache/Memcached/, Mail/SendGrid/, Notification/Firebase/) and implement the same Domain interface.
| Folder | Files | Purpose |
|---|---|---|
Auth/Jwt | JwtService | JWT token generation & validation |
Auth/Session | SessionUserContext | Retrieves user context from session |
Auth/SSO | GoogleProvider, MicrosoftProvider | OAuth SSO providers (implements Domain/Auth/Contracts/SSOProviderInterface) |
Cache/Redis | RedisCache | Redis-backed caching (implements Domain/Shared/Contracts/CacheInterface) |
Database/MySQL | BaseRepository, *Repository | MySQL data access layer (organized by context) |
Export/Custom | ExportService | Excel/PDF generation via Spreadsheet & Dompdf |
Http/Custom | ExternalApiClient, WebhookNotification | External API integration |
Import/Custom | ImportService | Excel/CSV import wizard |
Logging/Custom | AppLogger, CustomFileHandler | Enhanced logging |
Mail/Smtp | SmtpMailer | Email sending via SMTP (implements Domain/Shared/Contracts/MailerInterface) |
Media/Custom | ImageOptimizer, JxlConverter, WebpConverter | Image optimization & conversion |
Monitoring/MySQL | MySQLMetricStorage | Persistent metrics in MySQL |
Monitoring/Redis | RedisMetricStorage | Real-time metrics in Redis |
Notification/Custom | NotificationDispatcher | Multi-channel notification dispatch |
Queue/Redis | RedisQueue | Redis-backed job queue |
Queue/Database | DatabaseQueue | MySQL-backed job queue |
Rbac/Custom | PermissionChecker, RoleManager, MenuBuilder, etc. | Custom RBAC implementation |
Security/Captcha/Math | MathCaptchaProvider | Math-based CAPTCHA |
Security/Captcha/Recaptcha | RecaptchaProvider | Google reCAPTCHA v2/v3 |
3. Database (Repositories)
Repository implementations that bridge Domain interfaces to MySQL.
Base Repository
// Infrastructure/Database/MySQL/BaseRepository.php
use CodeIgniter\Database\BaseConnection;
use CodeIgniter\Database\ConnectionInterface;
abstract class BaseRepository
{
// Using BaseConnection (not interface) to expose methods like insertID()
protected BaseConnection $db;
protected string $table;
public function __construct(?ConnectionInterface $db = null)
{
$this->db = $db ?? \Config\Database::connect();
}
public function transaction(callable $callback): mixed
{
$this->db->transBegin();
try {
$result = $callback();
$this->db->transCommit();
return $result;
} catch (\Throwable $e) {
$this->db->transRollback();
throw $e;
}
}
}Available Repositories
| Repository | Table | Domain |
|---|---|---|
UserRepository | users | User |
RoleRepository | roles | Rbac |
PermissionRepository | permissions | Rbac |
MenuRepository | menus | Rbac |
NotificationRepository | notifications | Notification |
ApprovalRepository | approval_* | Approval |
Hydration Pattern
// Convert database row to Domain Entity
private function hydrate(array $row): User
{
return User::reconstitute(
id: (int) $row['id'],
name: $row['name'],
email: new Email($row['email']),
passwordHash: $row['password_hash'],
roles: json_decode($row['roles'] ?? '[]', true),
createdAt: new \DateTimeImmutable($row['created_at'])
);
}4. Cache (Redis)
// Domain/Shared/Contracts/CacheInterface.php
interface CacheInterface
{
public function get(string $key): mixed;
public function set(string $key, mixed $value, int $ttl = 3600): bool;
public function delete(string $key): bool;
public function has(string $key): bool;
}
// Infrastructure/Cache/RedisCache.php
class RedisCache implements CacheInterface // implements Domain interface
{
public function __construct(?string $host = null, int $port = 6379)
{
$this->redis = new \Redis();
$this->redis->connect($host ?? getenv('REDIS_HOST') ?: 'localhost', $port);
}
public function get(string $key): mixed
{
$value = $this->redis->get($key);
return $value !== false ? unserialize($value) : null;
}
}5. Auth (Session Context)
// Infrastructure/Auth/SessionUserContext.php
class SessionUserContext
{
public function getUserId(): ?int
{
return session()->get('user_id') ?? session()->get('id') ?? null;
}
public function getUserRoles(): array
{
$roles = session()->get('user_roles') ?? session()->get('role') ?? [];
return is_string($roles) ? [$roles] : $roles;
}
public function isAuthenticated(): bool
{
return $this->getUserId() !== null;
}
public function getContext(): array
{
return [
'user_id' => $this->getUserId(),
'roles' => $this->getUserRoles(),
'authenticated' => $this->isAuthenticated(),
];
}
}6. RBAC Services
| Service | Purpose |
|---|---|
PermissionChecker | Check if user has permission |
RoleManager | Manage user roles and permissions |
MenuBuilder | Build navigation menus based on permissions |
PermissionDiscovery | Scan controllers for permission annotations |
RbacCache | Cache RBAC data in Redis |
// Example: Check permission
use App\Infrastructure\Rbac\Custom\PermissionChecker;
$checker = new PermissionChecker($roleManager, $cache);
if ($checker->can($userId, 'users.create')) {
// Allow action
}7. Import (Excel/CSV)
The Import system provides a robust, reusable wizard for importing data. It handles:
- Excel (.xlsx) and CSV file parsing
- Interactive column mapping and validation
- Preview with editable cells
- Sync and Async processing
- Dynamic master data lookups (dropwdowns) in preview
Implementation
Extend BaseImportController and define your config.
// Modules/Admin/User/Controllers/UserImportController.php
class UserImportController extends BaseImportController
{
protected string $importType = 'users';
// 1. Define Config
protected function getImportConfig(): array
{
return [
'name' => [
'header' => 'Name',
'rules' => 'required',
'example' => 'John Doe'
],
'role' => [
'header' => 'Role',
'rules' => 'permit_empty',
'lookup' => [ // Adds dropdown in preview + reference sheet in template
'table' => 'roles',
'value' => 'name',
'label' => 'name',
'search' => ['name']
]
]
];
}
// 2. Process Row
protected function processRow(array $row): mixed
{
$user = new User($row['name'], ...);
return $this->userRepository->save($user);
}
}Features
- Template Generator: Automatically generates Excel templates with validation rules and reference sheets for lookup columns.
- Validation: Validates rows before import. Errors are shown in the preview grid with tooltips and a summary panel.
- Editable Preview: Users can fix validation errors directly in the browser before submitting.
- Dynamic Lookups: Columns with `lookup` config render as dropdowns in the editable preview, fetching data from the database.
- Pagination: The preview table supports pagination, search, and sorting for large datasets.
8. Export (Excel/PDF/CSV)
The Export system provides a unified way to generate reports. It supports:
- Excel (.xlsx): Utilizing
phpoffice/phpspreadsheet. - PDF: Utilizing
dompdf/dompdf. - CSV: Native PHP stream generation (fast and memory efficient).
1. Using the Service Directly (Composition)
Preferred for most modules (like Activity Logs). Inject the service and use it.
// Controllers/ActivityLogExportController.php
public function __construct() {
$this->exportService = new ExportService();
}
public function export() {
$data = $this->repo->findAll();
$config = [
'name' => ['header' => 'Name', 'column' => 'name'],
'date' => ['header' => 'Date', 'function' => fn($r) => $r->created_at]
];
// Generate content
$content = $this->exportService->exportToCsv($data, $config, 'report');
// Download
return $this->response->download('report.csv', $content);
}2. UI Component
Use the shared component to render the export dropdown automatically.
<?= view('Modules/Admin/Shared/Views/export_buttons', [
'baseUrl' => '/admin/activity-logs/export',
'formats' => ['csv', 'excel', 'pdf'],
'filters' => $filters // Pass current search/filter params
]) ?>The component handles the UI, loading state (SweetAlert), and file download trigger.
9. Mail (SMTP)
// Domain/Shared/Contracts/MailerInterface.php
interface MailerInterface
{
public function to(string|array $to): self;
public function subject(string $subject): self;
public function body(string $body): self;
public function html(string $html): self;
public function attach(string $filePath, string $fileName = ''): self;
public function send(): bool;
}
// Infrastructure/Mail/SmtpMailer.php
class SmtpMailer implements MailerInterface // implements Domain interface
{
public function __construct(?Email $email = null)
{
$this->email = $email ?? \Config\Services::email();
$this->email->initialize([
'protocol' => 'smtp',
'SMTPHost' => getenv('SMTP_HOST'),
'SMTPPort' => getenv('SMTP_PORT'),
'SMTPUser' => getenv('SMTP_USER'),
'SMTPPass' => getenv('SMTP_PASS'),
]);
}
}
// Usage
$mailer = new SmtpMailer();
$mailer->to('user@example.com')
->subject('Welcome!')
->html('Hello!
')
->send();10. Notification Service
// Infrastructure/Notification/NotificationService.php
class NotificationService implements NotificationServiceInterface
{
public function __construct(
private NotificationRepositoryInterface $repository,
private WebhookNotification $webhookService
) {}
public function send(Notification $notification, array $channels): void
{
foreach ($channels as $channel) {
match ($channel) {
NotificationChannel::IN_APP => $this->sendInApp($notification),
NotificationChannel::WEBHOOK => $this->sendWebhook($notification),
NotificationChannel::PUSH => null, // TODO: Firebase
};
}
}
public function sendToAll(Notification $notification): void
{
$available = array_filter(
NotificationChannel::cases(),
fn($ch) => $ch->isAvailable()
);
$this->send($notification, $available);
}
}11. Queue (Redis)
// Infrastructure/Queue/RedisQueue.php
class RedisQueue
{
public function push(string $queue, array $payload): bool
{
$job = json_encode([
'id' => uniqid('job_', true),
'payload' => $payload,
'created_at' => date('c'),
]);
return $this->redis->rpush($this->prefix . $queue, $job) !== false;
}
public function pop(string $queue, int $timeout = 0): ?array
{
$result = $this->redis->blpop($this->prefix . $queue, $timeout);
return $result ? json_decode($result[1], true) : null;
}
public function length(string $queue): int
{
return (int) $this->redis->llen($this->prefix . $queue);
}
}
// Usage
$queue = new RedisQueue();
$queue->push('emails', ['to' => 'user@example.com', 'subject' => 'Hello']);
// Worker (run via CLI)
while ($job = $queue->pop('emails', 10)) {
// Process job
}12. HTTP (External APIs)
// Infrastructure/Http/ExternalApiClient.php
class ExternalApiClient
{
private CURLRequest $client;
private array $defaultHeaders = [];
public function setBearerToken(string $token): self
{
$this->defaultHeaders['Authorization'] = 'Bearer ' . $token;
return $this;
}
public function get(string $url, array $query = []): array
{
$response = $this->client->get($url, [
'headers' => $this->defaultHeaders,
'query' => $query,
]);
return $this->parseResponse($response);
}
public function post(string $url, array $data = []): array
{
$response = $this->client->post($url, [
'headers' => array_merge($this->defaultHeaders, ['Content-Type' => 'application/json']),
'json' => $data,
]);
return $this->parseResponse($response);
}
}
// Usage
$api = new ExternalApiClient();
$api->setBearerToken('abc123');
$result = $api->get('https://api.example.com/users');13. Media (Image Processing)
| Service | Purpose |
|---|---|
ImageOptimizer | Compress and resize images |
WebpConverter | Convert images to WebP format |
JxlConverter | Convert images to JPEG XL format |
// Example usage
$optimizer = new ImageOptimizer();
$optimizer->optimize('/path/to/image.jpg', [
'quality' => 80,
'maxWidth' => 1920,
'maxHeight' => 1080,
]);
$webp = new WebpConverter();
$webp->convert('/path/to/image.jpg', '/path/to/image.webp');14. Logging
// Infrastructure/Logging/AppLogger.php
class AppLogger
{
public function log(string $level, string $message, array $context = []): void
{
log_message($level, $message . ' ' . json_encode($context));
}
public function audit(string $action, array $data = []): void
{
$this->log('info', "[AUDIT] {$action}", array_merge($data, [
'user_id' => session()->get('user_id'),
'ip' => service('request')->getIPAddress(),
]));
}
}
// Infrastructure/Logging/CustomFileHandler.php
// Extends CI4's FileHandler with custom formatting and rotation15. Monitoring Storage
Two storage backends for performance metrics:
| Storage | Purpose |
|---|---|
RedisMetricStorage | Real-time metrics (short-term) |
MySQLMetricStorage | Historical analysis (long-term) |
// Infrastructure/Monitoring/RedisMetricStorage.php
class RedisMetricStorage
{
public function store(array $metric): void
{
$key = $this->buildKey($metric);
$this->redis->rpush($key, json_encode($metric));
$this->redis->expire($key, 86400); // 24 hours
}
public function getMetrics(string $module, string $service, string $method, int $minutes = 60): array
{
$key = $this->buildKey([
'module' => $module,
'service' => $service,
'method' => $method,
]);
return array_map(
fn($v) => json_decode($v, true),
$this->redis->lrange($key, 0, -1)
);
}
}
// Infrastructure/Monitoring/MySQLMetricStorage.php
class MySQLMetricStorage
{
public function store(array $metric): void
{
$this->db->table('performance_logs')->insert([
'module' => $metric['module'],
'service' => $metric['service'],
'method' => $metric['method'],
'duration_ms' => $metric['duration_ms'],
'memory_bytes' => $metric['memory_bytes'] ?? null,
'created_at' => date('Y-m-d H:i:s'),
]);
}
public function getStats(string $module, int $minutes = 60): array
{
return $this->db->table('performance_logs')
->select('service, method, AVG(duration_ms) as avg_ms, COUNT(*) as count')
->where('module', $module)
->where('created_at >=', date('Y-m-d H:i:s', strtotime("-{$minutes} minutes")))
->groupBy(['service', 'method'])
->get()->getResultArray();
}
}Flush Redis to MySQL
# Flush performance data from Redis to MySQL
docker exec ci4-php php spark monitor:flush
# Analyze historical data
docker exec ci4-php php spark monitor:analyze 1440 # Last 24 hoursRedis Graceful Degradation
RedisMetricStorage implements graceful degradation when Redis is unavailable. This ensures the application continues to function even if Redis is not installed or unreachable.
When Redis is unavailable, the application will continue to work normally. Only metrics collection is affected.
How It Works
class RedisMetricStorage
{
private bool $enabled = false;
private function tryConnect(): void
{
try {
$this->redis = $this->createConnection();
$this->enabled = true;
} catch (\Throwable $e) {
$this->enabled = false;
log_message('warning', 'Redis not available: ' . $e->getMessage());
}
}
public function store(...): void
{
if (!$this->enabled) return; // Skip silently
// ... actual storage logic
}
}Effects When Redis is Unavailable
| Behavior | Description |
|---|---|
| App continues normally | No crashes or errors shown to users |
| Warning logged once | Single log entry indicating Redis unavailable |
| Metrics silently skipped | Performance data not persisted to Redis |
| Monitoring dashboard | May show empty/stale data until Redis returns |
| Auto-recovery | Metrics resume automatically when Redis reconnects |
Testing Redis Fallback
# Stop Redis to test fallback
docker stop ci4-redis
# Visit any page (e.g., /docs) - should work without errors
curl http://localhost:81/docs
# Check logs for warning
tail -f src/writable/logs/log-*.log
# Restart Redis
docker start ci4-redisStep 5: Implement save
public function save(User $user): void
{
$this->db->table($this->table)->insert([
'name' => $user->getName(),
'email' => $user->getEmail()->getValue(),
'password_hash' => $user->getPasswordHash(),
'roles' => json_encode($user->getRoles()),
'created_at' => date('Y-m-d H:i:s'),
]);
// Optionally set the ID back on the entity
$insertId = $this->db->insertID();
$this->setEntityId($user, $insertId);
}
private function setEntityId(User $user, int $id): void
{
$reflect = new \ReflectionClass($user);
$property = $reflect->getProperty('id');
$property->setAccessible(true);
$property->setValue($user, $id);
}Step 6: Implement update and delete
public function update(User $user): void
{
$this->db->table($this->table)
->where('id', $user->getId())
->update([
'name' => $user->getName(),
'email' => $user->getEmail()->getValue(),
'roles' => json_encode($user->getRoles()),
]);
}
public function delete(int $id): void
{
$this->db->table($this->table)
->where('id', $id)
->delete();
}
public function emailExists(Email $email): bool
{
return $this->db->table($this->table)
->where('email', $email->getValue())
->countAllResults() > 0;
}3. Understanding Hydration
Technical Concept
Hydration is converting database rows (arrays) into Entity objects. This is where Infrastructure "translates" between storage format and Domain format.
/**
* Convert database row to User entity
*/
private function hydrate(array $row): User
{
// 1. Create the entity with required constructor args
$user = new User(
$row['name'],
new Email($row['email']),
$row['password_hash']
);
// 2. Set the ID (not in constructor, set by database)
$this->setEntityId($user, (int) $row['id']);
// 3. Restore roles
$roles = json_decode($row['roles'] ?? '[]', true);
foreach ($roles as $role) {
$user->assignRole($role);
}
// 4. Set created_at if needed (using reflection)
if (isset($row['created_at'])) {
$reflect = new \ReflectionClass($user);
$property = $reflect->getProperty('createdAt');
$property->setAccessible(true);
$property->setValue($user, new \DateTimeImmutable($row['created_at']));
}
return $user;
}
return $user;
}
Why do we need Hydration? (Benefits)
- Decoupling: Your database schema (
users.password_hash) can change without breaking your Entity (User::getPasswordHash()). - Data Consistency: Entities guarantee valid state (e.g., valid email) through the constructor. Raw database arrays do not.
- Rich Behavior: Arrays are dumb data. Entities have methods like
$user->canAccess('admin'). - Type Safety: You get
Userobjects, notarray, enabling IDE autocompletion and static analysis.
Database Row vs Entity
4. Building a Cache Adapter
Step 1: Define the Interface (optional)
File: app/Infrastructure/Cache/CacheInterface.php
<?php
namespace App\Infrastructure\Cache\Redis;
interface CacheInterface
{
public function get(string $key): mixed;
public function set(string $key, mixed $value, int $ttl = 3600): void;
public function delete(string $key): void;
public function has(string $key): bool;
public function flush(): void;
}Step 2: Implement Redis Cache
File: app/Infrastructure/Cache/RedisCache.php
<?php
namespace App\Infrastructure\Cache\Redis;
class RedisCache implements CacheInterface
{
private \Redis $redis;
public function __construct(?\Redis $redis = null)
{
if ($redis !== null) {
$this->redis = $redis;
} else {
$this->redis = new \Redis();
$this->redis->connect(
getenv('REDIS_HOST') ?: 'redis',
(int) (getenv('REDIS_PORT') ?: 6379)
);
}
}
public function get(string $key): mixed
{
$value = $this->redis->get($key);
if ($value === false) {
return null;
}
return unserialize($value);
}
public function set(string $key, mixed $value, int $ttl = 3600): void
{
$this->redis->setex($key, $ttl, serialize($value));
}
public function delete(string $key): void
{
$this->redis->del($key);
}
public function has(string $key): bool
{
return $this->redis->exists($key) > 0;
}
public function flush(): void
{
$this->redis->flushDB();
}
}Usage Example
$cache = new RedisCache();
// Store user preferences
$cache->set('user:123:prefs', ['theme' => 'dark', 'lang' => 'id'], 3600);
// Retrieve (returns null if not found)
$prefs = $cache->get('user:123:prefs');
// Check existence
if ($cache->has('user:123:prefs')) {
// Use cached data
}5. Monitoring Storage
RedisMetricStorage (Short-term)
File: app/Infrastructure/Monitoring/RedisMetricStorage.php
<?php
namespace App\Infrastructure\Monitoring;
class RedisMetricStorage
{
private \Redis $redis;
private int $ttl;
public function __construct(?\Redis $redis = null, int $ttl = 3600)
{
$this->ttl = $ttl;
if ($redis !== null) {
$this->redis = $redis;
} else {
$this->redis = new \Redis();
$this->redis->connect(
getenv('REDIS_HOST') ?: 'redis',
(int) (getenv('REDIS_PORT') ?: 6379)
);
}
}
/**
* Store performance data
*
* @param string $module e.g., "Admin"
* @param string $service e.g., "UserController"
* @param string $method e.g., "index"
* @param float $durationMs Duration in milliseconds
* @param float $memoryMb Memory usage in MB
*/
public function store(
string $module,
string $service,
string $method,
float $durationMs,
float $memoryMb
): void {
$key = $this->buildKey($module, $service, $method);
$record = json_encode([
'd' => round($durationMs, 2), // duration
'm' => round($memoryMb, 2), // memory
't' => time(), // timestamp
]);
$this->redis->rPush($key, $record);
$this->redis->expire($key, $this->ttl);
}
/**
* Build time-bucketed key
*/
private function buildKey(string $module, string $service, string $method): string
{
$bucket = date('YmdHi'); // Per-minute bucket
return "perf:{$module}:{$service}:{$method}:{$bucket}";
}
/**
* Get all performance keys
*/
public function getAllKeys(): array
{
return $this->redis->keys('perf:*');
}
/**
* Get data for a specific key
*/
public function getData(string $key): array
{
$data = $this->redis->lRange($key, 0, -1);
return array_map(fn($json) => json_decode($json, true), $data);
}
}MySQL Metric Storage (Long-term)
The MySQL storage persists data for historical analysis. Data is flushed from Redis to MySQL periodically.
// Flush command moves data from Redis to MySQL
php spark monitor:flush
// Analyze historical data
php spark monitor:analyze 1440 // Last 24 hours16. Docker Verification
Check Infrastructure Structure
docker-compose exec php ls -la /var/www/html/app/Infrastructure/
# Output:
# Cache
# Monitoring
# PersistenceTest Redis Connection
# Connect to Redis
docker-compose exec redis redis-cli
# Ping
PING
# Output: PONG
# Set a test value
SET test:key "hello"
# Output: OK
# Get the value
GET test:key
# Output: "hello"
# Delete it
DEL test:keyTest Repository
# Open PHP console
docker-compose exec php php spark tinker
# Test UserRepository
> $repo = new \App\Infrastructure\Persistence\UserRepository();
> $users = $repo->findAll(5);
> print_r($users);Check Performance Keys in Redis
# List all performance keys
docker-compose exec redis redis-cli KEYS "perf:*"
# View data in a key
docker-compose exec redis redis-cli LRANGE "perf:Admin:UserController:index:202601041830" 0 -1Query MySQL Performance Logs
docker-compose exec mysql mysql -u root -p ci4_database
# Count records
SELECT COUNT(*) FROM performance_logs;
# View recent logs
SELECT module, service, method, duration_ms, created_at
FROM performance_logs
ORDER BY created_at DESC
LIMIT 10;