v1.0
Docs / Infrastructure

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

RequirementVersionNotes
PHP≥ 8.3Required for typed properties, enums, match expressions, and modern PHP features
CodeIgniter≥ 4.0Framework foundation
MySQL≥ 8.0Required for JSON functions (JSON_CONTAINS)
Redis≥ 6.0For caching, queues, and real-time metrics
Composer≥ 2.0Dependency management

Optional Dependencies

PackagePurpose
dompdf/dompdfPDF export functionality
phpoffice/phpspreadsheetExcel 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
TypeDescription
DatabaseMySQL Repository extending BaseRepository
CacheRedis cache implementing CacheInterface
MailEmail sender implementing MailerInterface
ExportExcel/CSV export service
HttpExternal API client
QueueRedis job queue
MediaImage/media processor
LoggingCustom logger with audit support
NotificationNotification channel
MonitoringPerformance metrics storage
AuthAuthentication service
RbacRole-based access control service

Table of Contents

  1. Why Separate Infrastructure?
  2. Infrastructure Overview
  3. Database (Repositories)
  4. Cache (Redis)
  5. Auth (Session Context)
  6. RBAC Services
  7. Import (Excel/CSV)
  8. Export (Excel/PDF)
  9. Mail (SMTP)
  10. Notification Service
  11. Queue (Redis)
  12. HTTP (External APIs)
  13. Media (Image Processing)
  14. Logging
  15. Monitoring Storage
  16. 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 SaysInfrastructure 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

  1. Swappable: Change MySQL to PostgreSQL without touching Domain
  2. Testable: Use mock implementations for unit tests
  3. 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/ # RecaptchaProvider
Engine Folders: Every module is organized by implementation engine (e.g. Cache/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.
FolderFilesPurpose
Auth/JwtJwtServiceJWT token generation & validation
Auth/SessionSessionUserContextRetrieves user context from session
Auth/SSOGoogleProvider, MicrosoftProviderOAuth SSO providers (implements Domain/Auth/Contracts/SSOProviderInterface)
Cache/RedisRedisCacheRedis-backed caching (implements Domain/Shared/Contracts/CacheInterface)
Database/MySQLBaseRepository, *RepositoryMySQL data access layer (organized by context)
Export/CustomExportServiceExcel/PDF generation via Spreadsheet & Dompdf
Http/CustomExternalApiClient, WebhookNotificationExternal API integration
Import/CustomImportServiceExcel/CSV import wizard
Logging/CustomAppLogger, CustomFileHandlerEnhanced logging
Mail/SmtpSmtpMailerEmail sending via SMTP (implements Domain/Shared/Contracts/MailerInterface)
Media/CustomImageOptimizer, JxlConverter, WebpConverterImage optimization & conversion
Monitoring/MySQLMySQLMetricStoragePersistent metrics in MySQL
Monitoring/RedisRedisMetricStorageReal-time metrics in Redis
Notification/CustomNotificationDispatcherMulti-channel notification dispatch
Queue/RedisRedisQueueRedis-backed job queue
Queue/DatabaseDatabaseQueueMySQL-backed job queue
Rbac/CustomPermissionChecker, RoleManager, MenuBuilder, etc.Custom RBAC implementation
Security/Captcha/MathMathCaptchaProviderMath-based CAPTCHA
Security/Captcha/RecaptchaRecaptchaProviderGoogle 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

RepositoryTableDomain
UserRepositoryusersUser
RoleRepositoryrolesRbac
PermissionRepositorypermissionsRbac
MenuRepositorymenusRbac
NotificationRepositorynotificationsNotification
ApprovalRepositoryapproval_*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

ServicePurpose
PermissionCheckerCheck if user has permission
RoleManagerManage user roles and permissions
MenuBuilderBuild navigation menus based on permissions
PermissionDiscoveryScan controllers for permission annotations
RbacCacheCache 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:

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


8. Export (Excel/PDF/CSV)

The Export system provides a unified way to generate reports. It supports:

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)

ServicePurpose
ImageOptimizerCompress and resize images
WebpConverterConvert images to WebP format
JxlConverterConvert 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 rotation

15. Monitoring Storage

Two storage backends for performance metrics:

StoragePurpose
RedisMetricStorageReal-time metrics (short-term)
MySQLMetricStorageHistorical 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 hours

Redis 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.

Graceful Fallback

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

BehaviorDescription
App continues normallyNo crashes or errors shown to users
Warning logged onceSingle log entry indicating Redis unavailable
Metrics silently skippedPerformance data not persisted to Redis
Monitoring dashboardMay show empty/stale data until Redis returns
Auto-recoveryMetrics 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-redis

Step 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)

  1. Decoupling: Your database schema (users.password_hash) can change without breaking your Entity (User::getPasswordHash()).
  2. Data Consistency: Entities guarantee valid state (e.g., valid email) through the constructor. Raw database arrays do not.
  3. Rich Behavior: Arrays are dumb data. Entities have methods like $user->canAccess('admin').
  4. Type Safety: You get User objects, not array, enabling IDE autocompletion and static analysis.

Database Row vs Entity

DATABASE ROW (array): ENTITY (object): [ User { 'id' => 1, -id: 1 'name' => 'John Doe', -name: "John Doe" 'email' => 'john@example.com', →→→ -email: Email("john@example.com") 'password_hash' => '$2y$...', -passwordHash: "$2y$..." 'roles' => '["admin","member"]', -roles: ["admin", "member"] 'created_at' => '2026-01-01...' -createdAt: DateTimeImmutable ] }

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 hours

16. Docker Verification

Check Infrastructure Structure

docker-compose exec php ls -la /var/www/html/app/Infrastructure/

# Output:
# Cache
# Monitoring
# Persistence

Test 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:key

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

Query 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;
ESC

Start typing to search the documentation