v1.0
Docs / Architecture Overview

Architecture Overview

This page explains the high-level architecture: the layers, how they connect, and how a request flows through the system.

Table of Contents

  1. The Three Main Layers
  2. Project Structure
  3. Request Flow (Step-by-Step)
  4. CodeIgniter 4 Lifecycle
  5. Dependency Direction
  6. Contracts & Interfaces
  7. Verifying Your Setup

1. The Three Main Layers

Goal: Understand the high-level separation of concerns. By the end of this section, you should know where to put a Controller, a Service, and a Repository.

Technical Concept

The architecture is organized into three main layers, each with a specific responsibility:

┌─────────────────────────────────────────────────────────────────────┐ │ PRESENTATION LAYER │ │ (app/Modules/) │ │ │ │ Contains: Controllers, Views, Layouts, Components │ │ Purpose: Handle HTTP requests, render HTML, format JSON │ │ Analogy: The "Waiter" - takes orders, serves food │ │ Context: This is where your features live (Admin, Web, Api) │ └─────────────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────────┐ │ DOMAIN LAYER │ │ (app/Domain/) │ │ │ │ Contains: Entities, Value Objects, Services, Policies, Interfaces │ │ Purpose: Pure business logic, validation, rules │ │ Knows: Business rules ("users must have valid emails") │ │ Doesn't: Know about HTTP, databases, or frameworks │ └─────────────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────────┐ │ INFRASTRUCTURE LAYER │ │ (app/Infrastructure/) │ │ │ │ Contains: Repositories, Cache adapters, Mail services │ │ Purpose: Connect to databases, APIs, external services │ │ Knows: How to read/write data, make API calls │ │ Doesn't: Know business logic (just stores/retrieves data) │ └─────────────────────────────────────────────────────────────────────┘

Simple Analogy

Think of it like a restaurant:

LayerRestaurant AnalogyIn Code
PresentationWaiter: Takes orders, serves food, interacts with customersControllers, Views
DomainChef: Knows recipes, cooking rules, food combinationsEntities, Services, Policies
InfrastructurePantry/Suppliers: Stores ingredients, gets deliveriesRepositories, Cache, APIs

The waiter doesn't cook. The chef doesn't serve tables. The pantry doesn't know recipes. Each has one job.


2. Project Structure

Full Directory Layout

app/ ├── Attributes/ # PHP 8 Attributes │ ├── Route.php # #[Route('/path')] │ ├── Middleware.php # #[Middleware(['auth'])] │ ├── Roles.php # #[Roles(['admin'])] │ ├── Trace.php # #[Trace('name')] │ └── Monitor.php # #[Monitor(threshold: 100)] │ ├── Commands/ # CLI spark commands │ ├── MonitorAnalyze.php # php spark monitor:analyze │ └── MonitorFlush.php # php spark monitor:flush │ ├── Config/ # CI4 configuration │ ├── Filters.php # HTTP filter registration │ └── Routes.php # Route scanner initialization │ ├── Controllers/ # Base controllers │ └── BaseController.php │ ├── Core/ # Framework extensions │ ├── Routing/ │ │ ├── RouteScanner.php # Scans for #[Route] attributes │ │ ├── RouteCache.php # Caches discovered routes │ │ └── ModuleChangeDetector.php │ └── Monitoring/ │ ├── Profiling/ServiceProfiler.php │ ├── Tracing/SpanManager.php │ ├── Metrics/MetricCollector.php │ └── Export/PrometheusExporter.php │ ├── Domain/ # PURE BUSINESS LOGIC (Reusable) │ ├── User/ │ │ ├── Entities/User.php # Data structure (not just DB table) │ │ ├── ValueObjects/UserId.php │ │ ├── Repositories/UserRepositoryInterface.php │ │ ├── Services/UserService.php │ │ ├── Policies/PasswordPolicy.php │ │ └── Exceptions/UserNotFoundException.php │ └── Shared/ │ └── ValueObjects/Email.php │ ├── Filters/ # HTTP middleware │ ├── TracingFilter.php # Request tracing │ └── PerformanceFilter.php # Performance recording │ ├── Infrastructure/ # EXTERNAL INTEGRATIONS │ ├── Persistence/ │ │ └── UserRepository.php # Implements UserRepositoryInterface │ ├── Cache/ │ │ └── RedisCache.php │ └── Monitoring/ │ ├── RedisMetricStorage.php │ └── MySQLMetricStorage.php │ └── Modules/ # FEATURE PACKAGES ├── Admin/ │ ├── Dashboard/ │ │ ├── Controllers/DashboardController.php │ │ ├── Services/DashboardService.php │ │ └── Views/index.php │ ├── User/ │ │ ├── Controllers/UserController.php │ │ └── Views/... │ └── Shared/ │ ├── Layouts/admin.php │ └── Components/sidebar.php │ ├── Web/ │ ├── Home/Controllers/HomeController.php │ ├── Docs/Controllers/DocsController.php │ └── Shared/Layouts/public.php │ ├── Member/ │ └── Account/Controllers/AccountController.php │ └── Api/ └── Controllers/MetricsController.php

What Goes Where?

Common Confusion: Modules vs Domain

I'm Writing...Put It In...ExampleWhy?
A new page/screenModules/{Feature}/...Modules/Admin/User/Controllers/UserController.phpIt's part of the UI/Interaction layer.
A complex business ruleDomain/{Context}/Services/Domain/Order/Services/OrderService.phpTo keep it reusable and testable.
A data structureDomain/{Context}/Entities/Domain/User/Entities/User.phpTo define what a "User" looks like in your code.
Database queryInfrastructure/Persistence/UserRepository.phpTo keep SQL separate from logic.

FAQ: Clarifications

1. What is a "Service"?

A Service is a class that orchestrates a business operation. It is the "Command Center" for a specific task.

2. Is an Entity the same as a Database Table?

Not exactly.

Often they look similar (1:1 mapping), but an Entity might have extra methods (e.g., `User::getFullName()`) or combine data from multiple tables.

3. Why use Domain Layer? (Reuse)

By keeping logic in `Domain`, you can reuse it easily:

// In Web Controller
$userService->register($data);

// In API Controller
$userService->register($data);

// In CLI Command
$userService->register($data);

The logic (validation, hashing password, sending email) is written once in the Service, not duplicated in 3 controllers.


3. Request Flow (Step-by-Step)

Scenario: User visits /admin/users

Let's trace exactly what happens:

STEP 1: Browser sends GET /admin/users │ ▼ STEP 2: Nginx receives request │ nginx.conf routes to PHP-FPM ▼ STEP 3: CodeIgniter boots (index.php) │ Loads Config, Services, etc. ▼ STEP 4: Filters run (before) │ TracingFilter::before() creates trace span │ Auth filter checks user is logged in ▼ STEP 5: RouteScanner finds matching route │ #[Route('/admin/users')] on UserController::index ▼ STEP 6: Controller method executes │ │ ┌─────────────────────────────────────────┐ │ │ UserController::index() │ │ │ │ │ │ $users = $this->userService->getAll();│ │ │ │ │ │ return view('...', ['users' => $users]);│ │ └─────────────────────────────────────────┘ │ │ │ ▼ │ ┌─────────────────────────────────────────┐ │ │ UserService::getAll() │ │ │ (Domain layer - business logic) │ │ │ │ │ │ return $this->userRepo->findAll(); │ │ └─────────────────────────────────────────┘ │ │ │ ▼ │ ┌─────────────────────────────────────────┐ │ │ UserRepository::findAll() │ │ │ (Infrastructure - database query) │ │ │ │ │ │ return $this->db->table('users') │ │ │ ->get()->getResult(); │ │ └─────────────────────────────────────────┘ │ │ │ ▼ (Data flows back up) │ STEP 7: View renders HTML │ admin.php layout + index.php view ▼ STEP 8: Filters run (after) │ PerformanceFilter::after() records: 85ms, 12MB │ TracingFilter::after() closes span ▼ STEP 9: Response sent to browser │ HTTP 200, HTML content ▼ STEP 10: Browser displays page

Docker: Seeing This in Action

# Watch PHP logs while making a request
docker-compose logs -f php

# Make a request
curl http://localhost:81/admin/users

# You'll see logs like:
# [INFO] Route matched: Admin\User\Controllers\UserController::index
# [DEBUG] TracingFilter: Started span for GET /admin/users
# [DEBUG] PerformanceFilter: Recorded 85ms for Admin::UserController::index

4. CodeIgniter 4 Lifecycle

Goal: Visualize the exact path a request takes from the browser to the response. This is critical for debugging "where did my request die?".

Understanding the strict lifecycle order helps debugging:


Browser Request
 ↓
index.php (Public)
 ↓
Boot Framework
 ↓
Pre-System Hooks
 ↓
Routing (file, attributes, or automatic)
 ↓
Before Filters (Auth, Role, etc.)
 ↓
Controller
 ↓
Service
 ↓
Repository
 ↓
Infrastructure (DB, Redis, Mail)
 ↓
Return Response
 ↓
After Filters
 ↓
Post-System Hooks
 ↓
Output to Browser

4. Dependency Direction

Goal: Learn the "Golden Rule" of dependency: Inner layers (Domain) must NEVER know about outer layers (Controllers/Infrastructure).

Technical Concept

A key rule: dependencies point inward. Outer layers know about inner layers, but not vice versa.

Presentation (Controllers) │ │ knows about ▼ Domain (Services, Entities) │ │ knows about (only interfaces!) ▼ Infrastructure (implements interfaces)

What This Means in Code

// CORRECT: Controller knows about Domain
namespace App\Modules\Admin\User\Controllers;
use App\Domain\User\Services\UserService;

// CORRECT: Domain defines interfaces (doesn't know implementations)
namespace App\Domain\User\Services;
use App\Domain\User\Repositories\UserRepositoryInterface;

// CORRECT: Infrastructure implements Domain interfaces
namespace App\Infrastructure\Persistence;
use App\Domain\User\Repositories\UserRepositoryInterface;
class UserRepository implements UserRepositoryInterface { }

// WRONG: Domain knowing about Infrastructure
namespace App\Domain\User\Services;
use App\Infrastructure\Persistence\UserRepository;  // Dependency Violation

// WRONG: Domain knowing about Presentation
namespace App\Domain\User\Entities;
use CodeIgniter\HTTP\Request;  // Dependency Violation

Why This Matters

BenefitExample
TestabilityTest Domain without database (use mock repository)
FlexibilitySwitch from MySQL to PostgreSQL without changing Domain
ClarityBusiness rules are in one place, not scattered

6. Contracts & Interfaces

Technical Concept

We place global interfaces under app/Contracts and domain-specific interfaces in app/Domain/{Context}/Repositories.

Rule: Always program to interfaces, not concrete classes.

// Bad: Coupled to specific implementation
public function __construct(RedisCache $cache) { }

// Good: Coupled to contract (can swap Redis for File/Memcached)
public function __construct(CacheInterface $cache) { }

5. Verifying Your Setup

Check Architecture Integrity (Automated Tests)

Your project includes custom spark commands to automatically verify all files adhere to the strict architecture rules:

# Check DDD layer dependencies (Domain vs Infrastructure vs Modules)
docker-compose exec php php spark arch:test

# Check Component guidelines (PHP helpers & JS IIFE namespaces)
docker-compose exec php php spark comp:test

# Check File Naming conventions (PascalCase, snake_case, kebab-case)
docker-compose exec php php spark naming:test

Check Folder Structure

# List main directories
docker-compose exec php ls -la /var/www/html/app/

# Expected output:
# Attributes
# Commands
# Config
# Controllers
# Core
# Domain
# Filters
# Infrastructure
# Modules

Check Routes Are Scanning

# View all registered routes
docker-compose exec php php spark routes

# Output should include:
# GET    /                   HomeController::index
# GET    /admin/dashboard    DashboardController::index
# GET    /docs               DocsController::index
# etc.

Check Database Connection

# Run migrations (if not done)
docker-compose exec php php spark migrate

# Check tables
docker-compose exec mysql mysql -u root -p -e "SHOW TABLES;" ci4_database

Check Redis Connection

# Ping Redis
docker-compose exec redis redis-cli ping

# Output: PONG
ESC

Start typing to search the documentation